diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 79a1e6cfeef17..872ee9961da57 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,9 +1,12 @@ -blank_issues_enabled: true +blank_issues_enabled: false version: 2.1 contact_links: - name: 🤔 Question or Problem about: Ask a question or ask about a problem in GitHub Discussions. - url: https://github.com/langchain-ai/langchain/discussions + url: https://www.github.com/langchain-ai/langchain/discussions/categories/q-a - name: Discord url: https://discord.gg/6adMQxSpJS about: General community discussions + - name: Show and tell + about: Show what you built with LangChain + url: https://www.github.com/langchain-ai/langchain/discussions/categories/show-and-tell diff --git a/.github/ISSUE_TEMPLATE/privileged.yml b/.github/ISSUE_TEMPLATE/privileged.yml new file mode 100644 index 0000000000000..692a5bde60f7d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/privileged.yml @@ -0,0 +1,25 @@ +name: 🔒 Privileged +description: You are a LangChain maintainer, or was asked directly by a maintainer to create an issue here. If not, check the other options. +body: + - type: markdown + attributes: + value: | + Thanks for your interest in LangChain! 🚀 + + If you are not a LangChain maintainer or were not asked directly by a maintainer to create an issue, then please start the conversation in a [Question in GitHub Discussions](https://github.com/langchain-ai/langchain/discussions/categories/q-a) instead. + + You are a LangChain maintainer if you maintain any of the packages inside of the LangChain repository + or are a regular contributor to LangChain with previous merged merged pull requests. + - type: checkboxes + id: privileged + attributes: + label: Privileged issue + description: Confirm that you are allowed to create an issue here. + options: + - label: I am a LangChain maintainer, or was asked directly by a LangChain maintainer to create an issue here. + required: true + - type: textarea + id: content + attributes: + label: Issue Content + description: Add the content of the issue here. diff --git a/.github/workflows/_all_ci.yml b/.github/workflows/_all_ci.yml index 7be66470dd195..a2c4e06d6c15d 100644 --- a/.github/workflows/_all_ci.yml +++ b/.github/workflows/_all_ci.yml @@ -32,7 +32,7 @@ concurrency: cancel-in-progress: true env: - POETRY_VERSION: "1.6.1" + POETRY_VERSION: "1.7.1" jobs: lint: diff --git a/.github/workflows/_compile_integration_test.yml b/.github/workflows/_compile_integration_test.yml index 66c587f1251c3..12cd3d737b2d7 100644 --- a/.github/workflows/_compile_integration_test.yml +++ b/.github/workflows/_compile_integration_test.yml @@ -9,7 +9,7 @@ on: description: "From which folder this pipeline executes" env: - POETRY_VERSION: "1.6.1" + POETRY_VERSION: "1.7.1" jobs: build: diff --git a/.github/workflows/_dependencies.yml b/.github/workflows/_dependencies.yml index af01a7eafa77d..1414f63472fa0 100644 --- a/.github/workflows/_dependencies.yml +++ b/.github/workflows/_dependencies.yml @@ -13,7 +13,7 @@ on: description: "Relative path to the langchain library folder" env: - POETRY_VERSION: "1.6.1" + POETRY_VERSION: "1.7.1" jobs: build: diff --git a/.github/workflows/_integration_test.yml b/.github/workflows/_integration_test.yml index e6c8296c59c19..baf0ccacb05ba 100644 --- a/.github/workflows/_integration_test.yml +++ b/.github/workflows/_integration_test.yml @@ -8,10 +8,11 @@ on: type: string env: - POETRY_VERSION: "1.6.1" + POETRY_VERSION: "1.7.1" jobs: build: + environment: Scheduled testing defaults: run: working-directory: ${{ inputs.working-directory }} @@ -51,6 +52,9 @@ jobs: MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }} TOGETHER_API_KEY: ${{ secrets.TOGETHER_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} + GOOGLE_SEARCH_API_KEY: ${{ secrets.GOOGLE_SEARCH_API_KEY }} + GOOGLE_CSE_ID: ${{ secrets.GOOGLE_CSE_ID }} run: | make integration_tests diff --git a/.github/workflows/_lint.yml b/.github/workflows/_lint.yml index e34305aa0bc54..07978a25cf258 100644 --- a/.github/workflows/_lint.yml +++ b/.github/workflows/_lint.yml @@ -13,7 +13,7 @@ on: description: "Relative path to the langchain library folder" env: - POETRY_VERSION: "1.6.1" + POETRY_VERSION: "1.7.1" WORKDIR: ${{ inputs.working-directory == '' && '.' || inputs.working-directory }} # This env var allows us to get inline annotations when ruff has complaints. diff --git a/.github/workflows/_release.yml b/.github/workflows/_release.yml index ad85a3a25639e..9351196fe5e97 100644 --- a/.github/workflows/_release.yml +++ b/.github/workflows/_release.yml @@ -16,11 +16,12 @@ on: env: PYTHON_VERSION: "3.10" - POETRY_VERSION: "1.6.1" + POETRY_VERSION: "1.7.1" jobs: build: if: github.ref == 'refs/heads/master' + environment: Scheduled testing runs-on: ubuntu-latest outputs: @@ -170,6 +171,9 @@ jobs: MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }} TOGETHER_API_KEY: ${{ secrets.TOGETHER_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} + GOOGLE_SEARCH_API_KEY: ${{ secrets.GOOGLE_SEARCH_API_KEY }} + GOOGLE_CSE_ID: ${{ secrets.GOOGLE_CSE_ID }} run: make integration_tests working-directory: ${{ inputs.working-directory }} diff --git a/.github/workflows/_test.yml b/.github/workflows/_test.yml index 2ae665f536c2d..55c5f79fcbcf1 100644 --- a/.github/workflows/_test.yml +++ b/.github/workflows/_test.yml @@ -13,7 +13,7 @@ on: description: "Relative path to the langchain library folder" env: - POETRY_VERSION: "1.6.1" + POETRY_VERSION: "1.7.1" jobs: build: diff --git a/.github/workflows/_test_release.yml b/.github/workflows/_test_release.yml index 0fc25a751644c..035158bf2034f 100644 --- a/.github/workflows/_test_release.yml +++ b/.github/workflows/_test_release.yml @@ -9,7 +9,7 @@ on: description: "From which folder this pipeline executes" env: - POETRY_VERSION: "1.6.1" + POETRY_VERSION: "1.7.1" PYTHON_VERSION: "3.10" jobs: diff --git a/.github/workflows/scheduled_test.yml b/.github/workflows/scheduled_test.yml index 0130f427d92b8..4ae8b755c146c 100644 --- a/.github/workflows/scheduled_test.yml +++ b/.github/workflows/scheduled_test.yml @@ -6,7 +6,7 @@ on: - cron: '0 13 * * *' env: - POETRY_VERSION: "1.6.1" + POETRY_VERSION: "1.7.1" jobs: build: diff --git a/.github/workflows/templates_ci.yml b/.github/workflows/templates_ci.yml index a4c83f326cbdc..b886fc7aaff81 100644 --- a/.github/workflows/templates_ci.yml +++ b/.github/workflows/templates_ci.yml @@ -24,7 +24,7 @@ concurrency: cancel-in-progress: true env: - POETRY_VERSION: "1.6.1" + POETRY_VERSION: "1.7.1" WORKDIR: "templates" jobs: diff --git a/cookbook/together_ai.ipynb b/cookbook/together_ai.ipynb index 1266073e83cc2..ed6dd906a2fe0 100644 --- a/cookbook/together_ai.ipynb +++ b/cookbook/together_ai.ipynb @@ -82,7 +82,7 @@ "prompt = ChatPromptTemplate.from_template(template)\n", "\n", "# LLM\n", - "from langchain_community.llms import Together\n", + "from langchain_together import Together\n", "\n", "llm = Together(\n", " model=\"mistralai/Mixtral-8x7B-Instruct-v0.1\",\n", diff --git a/docs/api_reference/requirements.txt b/docs/api_reference/requirements.txt index e5829ed5d9cdc..9132e119aef1e 100644 --- a/docs/api_reference/requirements.txt +++ b/docs/api_reference/requirements.txt @@ -6,7 +6,7 @@ pydantic<2 autodoc_pydantic==1.8.0 myst_parser nbsphinx==0.8.9 -sphinx==4.5.0 +sphinx>=5 sphinx-autobuild==2021.3.14 sphinx_rtd_theme==1.0.0 sphinx-typlog-theme==0.8.0 diff --git a/docs/docs/contributing/code.mdx b/docs/docs/contributing/code.mdx index 279bdc05303cc..d3f957d1f8902 100644 --- a/docs/docs/contributing/code.mdx +++ b/docs/docs/contributing/code.mdx @@ -32,7 +32,7 @@ For a [development container](https://containers.dev/), see the [.devcontainer f ### Dependency Management: Poetry and other env/dependency managers -This project utilizes [Poetry](https://python-poetry.org/) v1.6.1+ as a dependency manager. +This project utilizes [Poetry](https://python-poetry.org/) v1.7.1+ as a dependency manager. ❗Note: *Before installing Poetry*, if you use `Conda`, create and activate a new Conda env (e.g. `conda create -n langchain python=3.9`) @@ -75,7 +75,7 @@ make test If during installation you receive a `WheelFileValidationError` for `debugpy`, please make sure you are running Poetry v1.6.1+. This bug was present in older versions of Poetry (e.g. 1.4.1) and has been resolved in newer releases. -If you are still seeing this bug on v1.6.1, you may also try disabling "modern installation" +If you are still seeing this bug on v1.6.1+, you may also try disabling "modern installation" (`poetry config installer.modern-installation false`) and re-installing requirements. See [this `debugpy` issue](https://github.com/microsoft/debugpy/issues/1246) for more details. diff --git a/docs/docs/expression_language/how_to/fallbacks.ipynb b/docs/docs/expression_language/how_to/fallbacks.ipynb index 23459f8be7376..de915b3240319 100644 --- a/docs/docs/expression_language/how_to/fallbacks.ipynb +++ b/docs/docs/expression_language/how_to/fallbacks.ipynb @@ -302,7 +302,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.4" + "version": "3.9.1" } }, "nbformat": 4, diff --git a/docs/docs/expression_language/how_to/inspect.ipynb b/docs/docs/expression_language/how_to/inspect.ipynb index e50d976d5f214..d680530e4e628 100644 --- a/docs/docs/expression_language/how_to/inspect.ipynb +++ b/docs/docs/expression_language/how_to/inspect.ipynb @@ -177,7 +177,7 @@ "source": [ "## Get the prompts\n", "\n", - "An important part of every chain is the prompts that are used. You can get the graphs present in the chain:" + "An important part of every chain is the prompts that are used. You can get the prompts present in the chain:" ] }, { diff --git a/docs/docs/expression_language/interface.ipynb b/docs/docs/expression_language/interface.ipynb index ffc9225ac41b5..a0e63966afaa2 100644 --- a/docs/docs/expression_language/interface.ipynb +++ b/docs/docs/expression_language/interface.ipynb @@ -30,6 +30,7 @@ "- [`ainvoke`](#async-invoke): call the chain on an input async\n", "- [`abatch`](#async-batch): call the chain on a list of inputs async\n", "- [`astream_log`](#async-stream-intermediate-steps): stream back intermediate steps as they happen, in addition to the final response\n", + "- [`astream_events`](#async-stream-events): **beta** stream events as they happen in the chain (introduced in `langchain-core` 0.1.14)\n", "\n", "The **input type** and **output type** varies by component:\n", "\n", @@ -87,7 +88,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 2, "id": "25e146d4-60da-40a2-9026-b5dfee106a3f", "metadata": {}, "outputs": [ @@ -99,7 +100,7 @@ " 'properties': {'topic': {'title': 'Topic', 'type': 'string'}}}" ] }, - "execution_count": 13, + "execution_count": 2, "metadata": {}, "output_type": "execute_result" } @@ -111,7 +112,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 3, "id": "ad130546-4c14-4f6c-95af-c56ea19b12ac", "metadata": {}, "outputs": [ @@ -123,7 +124,7 @@ " 'properties': {'topic': {'title': 'Topic', 'type': 'string'}}}" ] }, - "execution_count": 16, + "execution_count": 3, "metadata": {}, "output_type": "execute_result" } @@ -134,7 +135,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 4, "id": "49d34744-d6db-4fdf-a0d6-261522b7f251", "metadata": {}, "outputs": [ @@ -150,7 +151,8 @@ " {'$ref': '#/definitions/HumanMessage'},\n", " {'$ref': '#/definitions/ChatMessage'},\n", " {'$ref': '#/definitions/SystemMessage'},\n", - " {'$ref': '#/definitions/FunctionMessage'}]}}],\n", + " {'$ref': '#/definitions/FunctionMessage'},\n", + " {'$ref': '#/definitions/ToolMessage'}]}}],\n", " 'definitions': {'StringPromptValue': {'title': 'StringPromptValue',\n", " 'description': 'String prompt value.',\n", " 'type': 'object',\n", @@ -163,7 +165,10 @@ " 'AIMessage': {'title': 'AIMessage',\n", " 'description': 'A Message from an AI.',\n", " 'type': 'object',\n", - " 'properties': {'content': {'title': 'Content', 'type': 'string'},\n", + " 'properties': {'content': {'title': 'Content',\n", + " 'anyOf': [{'type': 'string'},\n", + " {'type': 'array',\n", + " 'items': {'anyOf': [{'type': 'string'}, {'type': 'object'}]}}]},\n", " 'additional_kwargs': {'title': 'Additional Kwargs', 'type': 'object'},\n", " 'type': {'title': 'Type',\n", " 'default': 'ai',\n", @@ -174,7 +179,10 @@ " 'HumanMessage': {'title': 'HumanMessage',\n", " 'description': 'A Message from a human.',\n", " 'type': 'object',\n", - " 'properties': {'content': {'title': 'Content', 'type': 'string'},\n", + " 'properties': {'content': {'title': 'Content',\n", + " 'anyOf': [{'type': 'string'},\n", + " {'type': 'array',\n", + " 'items': {'anyOf': [{'type': 'string'}, {'type': 'object'}]}}]},\n", " 'additional_kwargs': {'title': 'Additional Kwargs', 'type': 'object'},\n", " 'type': {'title': 'Type',\n", " 'default': 'human',\n", @@ -185,7 +193,10 @@ " 'ChatMessage': {'title': 'ChatMessage',\n", " 'description': 'A Message that can be assigned an arbitrary speaker (i.e. role).',\n", " 'type': 'object',\n", - " 'properties': {'content': {'title': 'Content', 'type': 'string'},\n", + " 'properties': {'content': {'title': 'Content',\n", + " 'anyOf': [{'type': 'string'},\n", + " {'type': 'array',\n", + " 'items': {'anyOf': [{'type': 'string'}, {'type': 'object'}]}}]},\n", " 'additional_kwargs': {'title': 'Additional Kwargs', 'type': 'object'},\n", " 'type': {'title': 'Type',\n", " 'default': 'chat',\n", @@ -196,7 +207,10 @@ " 'SystemMessage': {'title': 'SystemMessage',\n", " 'description': 'A Message for priming AI behavior, usually passed in as the first of a sequence\\nof input messages.',\n", " 'type': 'object',\n", - " 'properties': {'content': {'title': 'Content', 'type': 'string'},\n", + " 'properties': {'content': {'title': 'Content',\n", + " 'anyOf': [{'type': 'string'},\n", + " {'type': 'array',\n", + " 'items': {'anyOf': [{'type': 'string'}, {'type': 'object'}]}}]},\n", " 'additional_kwargs': {'title': 'Additional Kwargs', 'type': 'object'},\n", " 'type': {'title': 'Type',\n", " 'default': 'system',\n", @@ -206,7 +220,10 @@ " 'FunctionMessage': {'title': 'FunctionMessage',\n", " 'description': 'A Message for passing the result of executing a function back to a model.',\n", " 'type': 'object',\n", - " 'properties': {'content': {'title': 'Content', 'type': 'string'},\n", + " 'properties': {'content': {'title': 'Content',\n", + " 'anyOf': [{'type': 'string'},\n", + " {'type': 'array',\n", + " 'items': {'anyOf': [{'type': 'string'}, {'type': 'object'}]}}]},\n", " 'additional_kwargs': {'title': 'Additional Kwargs', 'type': 'object'},\n", " 'type': {'title': 'Type',\n", " 'default': 'function',\n", @@ -214,6 +231,20 @@ " 'type': 'string'},\n", " 'name': {'title': 'Name', 'type': 'string'}},\n", " 'required': ['content', 'name']},\n", + " 'ToolMessage': {'title': 'ToolMessage',\n", + " 'description': 'A Message for passing the result of executing a tool back to a model.',\n", + " 'type': 'object',\n", + " 'properties': {'content': {'title': 'Content',\n", + " 'anyOf': [{'type': 'string'},\n", + " {'type': 'array',\n", + " 'items': {'anyOf': [{'type': 'string'}, {'type': 'object'}]}}]},\n", + " 'additional_kwargs': {'title': 'Additional Kwargs', 'type': 'object'},\n", + " 'type': {'title': 'Type',\n", + " 'default': 'tool',\n", + " 'enum': ['tool'],\n", + " 'type': 'string'},\n", + " 'tool_call_id': {'title': 'Tool Call Id', 'type': 'string'}},\n", + " 'required': ['content', 'tool_call_id']},\n", " 'ChatPromptValueConcrete': {'title': 'ChatPromptValueConcrete',\n", " 'description': 'Chat prompt value which explicitly lists out the message types it accepts.\\nFor use in external schemas.',\n", " 'type': 'object',\n", @@ -223,7 +254,8 @@ " {'$ref': '#/definitions/HumanMessage'},\n", " {'$ref': '#/definitions/ChatMessage'},\n", " {'$ref': '#/definitions/SystemMessage'},\n", - " {'$ref': '#/definitions/FunctionMessage'}]}},\n", + " {'$ref': '#/definitions/FunctionMessage'},\n", + " {'$ref': '#/definitions/ToolMessage'}]}},\n", " 'type': {'title': 'Type',\n", " 'default': 'ChatPromptValueConcrete',\n", " 'enum': ['ChatPromptValueConcrete'],\n", @@ -231,7 +263,7 @@ " 'required': ['messages']}}}" ] }, - "execution_count": 15, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } @@ -254,7 +286,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 5, "id": "a0e41fd3-77d8-4911-af6a-d4d3aad5f77b", "metadata": {}, "outputs": [ @@ -262,37 +294,47 @@ "data": { "text/plain": [ "{'title': 'ChatOpenAIOutput',\n", - " 'anyOf': [{'$ref': '#/definitions/HumanMessage'},\n", - " {'$ref': '#/definitions/AIMessage'},\n", + " 'anyOf': [{'$ref': '#/definitions/AIMessage'},\n", + " {'$ref': '#/definitions/HumanMessage'},\n", " {'$ref': '#/definitions/ChatMessage'},\n", + " {'$ref': '#/definitions/SystemMessage'},\n", " {'$ref': '#/definitions/FunctionMessage'},\n", - " {'$ref': '#/definitions/SystemMessage'}],\n", - " 'definitions': {'HumanMessage': {'title': 'HumanMessage',\n", - " 'description': 'A Message from a human.',\n", + " {'$ref': '#/definitions/ToolMessage'}],\n", + " 'definitions': {'AIMessage': {'title': 'AIMessage',\n", + " 'description': 'A Message from an AI.',\n", " 'type': 'object',\n", - " 'properties': {'content': {'title': 'Content', 'type': 'string'},\n", + " 'properties': {'content': {'title': 'Content',\n", + " 'anyOf': [{'type': 'string'},\n", + " {'type': 'array',\n", + " 'items': {'anyOf': [{'type': 'string'}, {'type': 'object'}]}}]},\n", " 'additional_kwargs': {'title': 'Additional Kwargs', 'type': 'object'},\n", " 'type': {'title': 'Type',\n", - " 'default': 'human',\n", - " 'enum': ['human'],\n", + " 'default': 'ai',\n", + " 'enum': ['ai'],\n", " 'type': 'string'},\n", " 'example': {'title': 'Example', 'default': False, 'type': 'boolean'}},\n", " 'required': ['content']},\n", - " 'AIMessage': {'title': 'AIMessage',\n", - " 'description': 'A Message from an AI.',\n", + " 'HumanMessage': {'title': 'HumanMessage',\n", + " 'description': 'A Message from a human.',\n", " 'type': 'object',\n", - " 'properties': {'content': {'title': 'Content', 'type': 'string'},\n", + " 'properties': {'content': {'title': 'Content',\n", + " 'anyOf': [{'type': 'string'},\n", + " {'type': 'array',\n", + " 'items': {'anyOf': [{'type': 'string'}, {'type': 'object'}]}}]},\n", " 'additional_kwargs': {'title': 'Additional Kwargs', 'type': 'object'},\n", " 'type': {'title': 'Type',\n", - " 'default': 'ai',\n", - " 'enum': ['ai'],\n", + " 'default': 'human',\n", + " 'enum': ['human'],\n", " 'type': 'string'},\n", " 'example': {'title': 'Example', 'default': False, 'type': 'boolean'}},\n", " 'required': ['content']},\n", " 'ChatMessage': {'title': 'ChatMessage',\n", " 'description': 'A Message that can be assigned an arbitrary speaker (i.e. role).',\n", " 'type': 'object',\n", - " 'properties': {'content': {'title': 'Content', 'type': 'string'},\n", + " 'properties': {'content': {'title': 'Content',\n", + " 'anyOf': [{'type': 'string'},\n", + " {'type': 'array',\n", + " 'items': {'anyOf': [{'type': 'string'}, {'type': 'object'}]}}]},\n", " 'additional_kwargs': {'title': 'Additional Kwargs', 'type': 'object'},\n", " 'type': {'title': 'Type',\n", " 'default': 'chat',\n", @@ -300,10 +342,26 @@ " 'type': 'string'},\n", " 'role': {'title': 'Role', 'type': 'string'}},\n", " 'required': ['content', 'role']},\n", + " 'SystemMessage': {'title': 'SystemMessage',\n", + " 'description': 'A Message for priming AI behavior, usually passed in as the first of a sequence\\nof input messages.',\n", + " 'type': 'object',\n", + " 'properties': {'content': {'title': 'Content',\n", + " 'anyOf': [{'type': 'string'},\n", + " {'type': 'array',\n", + " 'items': {'anyOf': [{'type': 'string'}, {'type': 'object'}]}}]},\n", + " 'additional_kwargs': {'title': 'Additional Kwargs', 'type': 'object'},\n", + " 'type': {'title': 'Type',\n", + " 'default': 'system',\n", + " 'enum': ['system'],\n", + " 'type': 'string'}},\n", + " 'required': ['content']},\n", " 'FunctionMessage': {'title': 'FunctionMessage',\n", " 'description': 'A Message for passing the result of executing a function back to a model.',\n", " 'type': 'object',\n", - " 'properties': {'content': {'title': 'Content', 'type': 'string'},\n", + " 'properties': {'content': {'title': 'Content',\n", + " 'anyOf': [{'type': 'string'},\n", + " {'type': 'array',\n", + " 'items': {'anyOf': [{'type': 'string'}, {'type': 'object'}]}}]},\n", " 'additional_kwargs': {'title': 'Additional Kwargs', 'type': 'object'},\n", " 'type': {'title': 'Type',\n", " 'default': 'function',\n", @@ -311,19 +369,23 @@ " 'type': 'string'},\n", " 'name': {'title': 'Name', 'type': 'string'}},\n", " 'required': ['content', 'name']},\n", - " 'SystemMessage': {'title': 'SystemMessage',\n", - " 'description': 'A Message for priming AI behavior, usually passed in as the first of a sequence\\nof input messages.',\n", + " 'ToolMessage': {'title': 'ToolMessage',\n", + " 'description': 'A Message for passing the result of executing a tool back to a model.',\n", " 'type': 'object',\n", - " 'properties': {'content': {'title': 'Content', 'type': 'string'},\n", + " 'properties': {'content': {'title': 'Content',\n", + " 'anyOf': [{'type': 'string'},\n", + " {'type': 'array',\n", + " 'items': {'anyOf': [{'type': 'string'}, {'type': 'object'}]}}]},\n", " 'additional_kwargs': {'title': 'Additional Kwargs', 'type': 'object'},\n", " 'type': {'title': 'Type',\n", - " 'default': 'system',\n", - " 'enum': ['system'],\n", - " 'type': 'string'}},\n", - " 'required': ['content']}}}" + " 'default': 'tool',\n", + " 'enum': ['tool'],\n", + " 'type': 'string'},\n", + " 'tool_call_id': {'title': 'Tool Call Id', 'type': 'string'}},\n", + " 'required': ['content', 'tool_call_id']}}}" ] }, - "execution_count": 17, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } @@ -343,7 +405,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 6, "id": "bea9639d", "metadata": {}, "outputs": [ @@ -351,6 +413,8 @@ "name": "stdout", "output_type": "stream", "text": [ + "Sure, here's a bear-themed joke for you:\n", + "\n", "Why don't bears wear shoes?\n", "\n", "Because they already have bear feet!" @@ -372,17 +436,17 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 7, "id": "470e483f", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "AIMessage(content=\"Why don't bears wear shoes?\\n\\nBecause they already have bear feet!\")" + "AIMessage(content=\"Why don't bears wear shoes? \\n\\nBecause they have bear feet!\")" ] }, - "execution_count": 21, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } @@ -401,18 +465,18 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 8, "id": "9685de67", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[AIMessage(content=\"Why don't bears wear shoes?\\n\\nBecause they have bear feet!\"),\n", + "[AIMessage(content=\"Sure, here's a bear joke for you:\\n\\nWhy don't bears wear shoes?\\n\\nBecause they already have bear feet!\"),\n", " AIMessage(content=\"Why don't cats play poker in the wild?\\n\\nToo many cheetahs!\")]" ] }, - "execution_count": 22, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } @@ -431,18 +495,18 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 9, "id": "a08522f6", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[AIMessage(content=\"Why don't bears wear shoes? \\n\\nBecause they have bear feet!\"),\n", - " AIMessage(content=\"Why don't cats play poker in the wild?\\n\\nToo many cheetahs!\")]" + "[AIMessage(content=\"Why don't bears wear shoes?\\n\\nBecause they have bear feet!\"),\n", + " AIMessage(content=\"Why don't cats play poker in the wild? Too many cheetahs!\")]" ] }, - "execution_count": 23, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } @@ -461,7 +525,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 10, "id": "ea35eee4", "metadata": {}, "outputs": [ @@ -469,11 +533,9 @@ "name": "stdout", "output_type": "stream", "text": [ - "Sure, here's a bear-themed joke for you:\n", - "\n", "Why don't bears wear shoes?\n", "\n", - "Because they already have bear feet!" + "Because they have bear feet!" ] } ], @@ -492,17 +554,17 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 11, "id": "ef8c9b20", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "AIMessage(content=\"Why don't bears wear shoes? \\n\\nBecause they have bear feet!\")" + "AIMessage(content=\"Why don't bears ever wear shoes?\\n\\nBecause they already have bear feet!\")" ] }, - "execution_count": 25, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } @@ -521,7 +583,7 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 12, "id": "eba2a103", "metadata": {}, "outputs": [ @@ -531,7 +593,7 @@ "[AIMessage(content=\"Why don't bears wear shoes?\\n\\nBecause they have bear feet!\")]" ] }, - "execution_count": 26, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } @@ -540,6 +602,192 @@ "await chain.abatch([{\"topic\": \"bears\"}])" ] }, + { + "cell_type": "markdown", + "id": "c2d58e3f-2b2e-4dac-820b-5e9c263b1868", + "metadata": {}, + "source": [ + "## Async Stream Events (beta)" + ] + }, + { + "cell_type": "markdown", + "id": "53d365e5-dc14-4bb7-aa6a-7762c3af16a4", + "metadata": {}, + "source": [ + "Event Streaming is a **beta** API, and may change a bit based on feedback.\n", + "\n", + "Note: Introduced in langchain-core 0.2.0\n", + "\n", + "For now, when using the astream_events API, for everything to work properly please:\n", + "\n", + "* Use `async` throughout the code (including async tools etc)\n", + "* Propagate callbacks if defining custom functions / runnables. \n", + "* Whenever using runnables without LCEL, make sure to call `.astream()` on LLMs rather than `.ainvoke` to force the LLM to stream tokens.\n", + "\n", + "### Event Reference\n", + "\n", + "\n", + "Here is a reference table that shows some events that might be emitted by the various Runnable objects.\n", + "Definitions for some of the Runnable are included after the table.\n", + "\n", + "⚠️ When streaming the inputs for the runnable will not be available until the input stream has been entirely consumed This means that the inputs will be available at for the corresponding `end` hook rather than `start` event.\n", + "\n", + "\n", + "| event | name | chunk | input | output |\n", + "|----------------------|------------------|---------------------------------|-----------------------------------------------|-------------------------------------------------|\n", + "| on_chat_model_start | [model name] | | {\"messages\": [[SystemMessage, HumanMessage]]} | |\n", + "| on_chat_model_stream | [model name] | AIMessageChunk(content=\"hello\") | | |\n", + "| on_chat_model_end | [model name] | | {\"messages\": [[SystemMessage, HumanMessage]]} | {\"generations\": [...], \"llm_output\": None, ...} |\n", + "| on_llm_start | [model name] | | {'input': 'hello'} | |\n", + "| on_llm_stream | [model name] | 'Hello' | | |\n", + "| on_llm_end | [model name] | | 'Hello human!' |\n", + "| on_chain_start | format_docs | | | |\n", + "| on_chain_stream | format_docs | \"hello world!, goodbye world!\" | | |\n", + "| on_chain_end | format_docs | | [Document(...)] | \"hello world!, goodbye world!\" |\n", + "| on_tool_start | some_tool | | {\"x\": 1, \"y\": \"2\"} | |\n", + "| on_tool_stream | some_tool | {\"x\": 1, \"y\": \"2\"} | | |\n", + "| on_tool_end | some_tool | | | {\"x\": 1, \"y\": \"2\"} |\n", + "| on_retriever_start | [retriever name] | | {\"query\": \"hello\"} | |\n", + "| on_retriever_chunk | [retriever name] | {documents: [...]} | | |\n", + "| on_retriever_end | [retriever name] | | {\"query\": \"hello\"} | {documents: [...]} |\n", + "| on_prompt_start | [template_name] | | {\"question\": \"hello\"} | |\n", + "| on_prompt_end | [template_name] | | {\"question\": \"hello\"} | ChatPromptValue(messages: [SystemMessage, ...]) |\n", + "\n", + "\n", + "Here are declarations associated with the events shown above:\n", + "\n", + "`format_docs`:\n", + "\n", + "```python\n", + "def format_docs(docs: List[Document]) -> str:\n", + " '''Format the docs.'''\n", + " return \", \".join([doc.page_content for doc in docs])\n", + "\n", + "format_docs = RunnableLambda(format_docs)\n", + "```\n", + "\n", + "`some_tool`:\n", + "\n", + "```python\n", + "@tool\n", + "def some_tool(x: int, y: str) -> dict:\n", + " '''Some_tool.'''\n", + " return {\"x\": x, \"y\": y}\n", + "```\n", + "\n", + "`prompt`:\n", + "\n", + "```python\n", + "template = ChatPromptTemplate.from_messages(\n", + " [(\"system\", \"You are Cat Agent 007\"), (\"human\", \"{question}\")]\n", + ").with_config({\"run_name\": \"my_template\", \"tags\": [\"my_template\"]})\n", + "```\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "108cf792-a372-4626-bbef-9d7be23dde33", + "metadata": {}, + "source": [ + "Let's define a new chain to make it more interesting to show off the `astream_events` interface (and later the `astream_log` interface)." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "92eeb4da-0aae-457b-bd8f-8c35a024d4d1", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_community.vectorstores import FAISS\n", + "from langchain_core.output_parsers import StrOutputParser\n", + "from langchain_core.runnables import RunnablePassthrough\n", + "from langchain_openai import OpenAIEmbeddings\n", + "\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", + "\n", + "vectorstore = FAISS.from_texts(\n", + " [\"harrison worked at kensho\"], embedding=OpenAIEmbeddings()\n", + ")\n", + "retriever = vectorstore.as_retriever()\n", + "\n", + "retrieval_chain = (\n", + " {\n", + " \"context\": retriever.with_config(run_name=\"Docs\"),\n", + " \"question\": RunnablePassthrough(),\n", + " }\n", + " | prompt\n", + " | model.with_config(run_name=\"my_llm\")\n", + " | StrOutputParser()\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "1167e8f2-cab7-45b4-8922-7518b58a7d8d", + "metadata": {}, + "source": [ + "Now let's use `astream_events` to get events from the retriever and the LLM." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "0742d723-5b00-4a44-961e-dd4a3ec6d557", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/eugene/src/langchain/libs/core/langchain_core/_api/beta_decorator.py:86: LangChainBetaWarning: This API is in beta and may change in the future.\n", + " warn_beta(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "--\n", + "Retrieved the following documents:\n", + "[Document(page_content='harrison worked at kensho')]\n", + "\n", + "Streaming LLM:\n", + "|H|arrison| worked| at| Kens|ho|.||\n", + "Done streaming LLM.\n" + ] + } + ], + "source": [ + "async for event in retrieval_chain.astream_events(\n", + " \"where did harrison work?\", version=\"v1\", include_names=[\"Docs\", \"my_llm\"]\n", + "):\n", + " kind = event[\"event\"]\n", + " if kind == \"on_chat_model_stream\":\n", + " print(event[\"data\"][\"chunk\"].content, end=\"|\")\n", + " elif kind in {\"on_chat_model_start\"}:\n", + " print()\n", + " print(\"Streaming LLM:\")\n", + " elif kind in {\"on_chat_model_end\"}:\n", + " print()\n", + " print(\"Done streaming LLM.\")\n", + " elif kind == \"on_retriever_end\":\n", + " print(\"--\")\n", + " print(\"Retrieved the following documents:\")\n", + " print(event[\"data\"][\"output\"][\"documents\"])\n", + " elif kind == \"on_tool_end\":\n", + " print(f\"Ended tool: {event['name']}\")\n", + " else:\n", + " pass" + ] + }, { "cell_type": "markdown", "id": "f9cef104", @@ -607,7 +855,7 @@ }, { "cell_type": "code", - "execution_count": 29, + "execution_count": 15, "id": "21c9019e", "metadata": {}, "outputs": [ @@ -619,20 +867,23 @@ "RunLogPatch({'op': 'replace',\n", " 'path': '',\n", " 'value': {'final_output': None,\n", - " 'id': 'e2f2cc72-eb63-4d20-8326-237367482efb',\n", + " 'id': '82e9b4b1-3dd6-4732-8db9-90e79c4da48c',\n", " 'logs': {},\n", - " 'streamed_output': []}})\n", + " 'name': 'RunnableSequence',\n", + " 'streamed_output': [],\n", + " 'type': 'chain'}})\n", "----------------------------------------\n", "RunLogPatch({'op': 'add',\n", " 'path': '/logs/Docs',\n", " 'value': {'end_time': None,\n", " 'final_output': None,\n", - " 'id': '8da492cc-4492-4e74-b8b0-9e60e8693390',\n", + " 'id': '9206e94a-57bd-48ee-8c5e-fdd1c52a6da2',\n", " 'metadata': {},\n", " 'name': 'Docs',\n", - " 'start_time': '2023-10-19T17:50:13.526',\n", + " 'start_time': '2024-01-19T22:33:55.902+00:00',\n", + " 'streamed_output': [],\n", " 'streamed_output_str': [],\n", - " 'tags': ['map:key:context', 'FAISS'],\n", + " 'tags': ['map:key:context', 'FAISS', 'OpenAIEmbeddings'],\n", " 'type': 'retriever'}})\n", "----------------------------------------\n", "RunLogPatch({'op': 'add',\n", @@ -640,60 +891,41 @@ " 'value': {'documents': [Document(page_content='harrison worked at kensho')]}},\n", " {'op': 'add',\n", " 'path': '/logs/Docs/end_time',\n", - " 'value': '2023-10-19T17:50:13.713'})\n", - "----------------------------------------\n", - "RunLogPatch({'op': 'add', 'path': '/streamed_output/-', 'value': ''})\n", + " 'value': '2024-01-19T22:33:56.064+00:00'})\n", "----------------------------------------\n", - "RunLogPatch({'op': 'add', 'path': '/streamed_output/-', 'value': 'H'})\n", + "RunLogPatch({'op': 'add', 'path': '/streamed_output/-', 'value': ''},\n", + " {'op': 'replace', 'path': '/final_output', 'value': ''})\n", "----------------------------------------\n", - "RunLogPatch({'op': 'add', 'path': '/streamed_output/-', 'value': 'arrison'})\n", + "RunLogPatch({'op': 'add', 'path': '/streamed_output/-', 'value': 'H'},\n", + " {'op': 'replace', 'path': '/final_output', 'value': 'H'})\n", "----------------------------------------\n", - "RunLogPatch({'op': 'add', 'path': '/streamed_output/-', 'value': ' worked'})\n", + "RunLogPatch({'op': 'add', 'path': '/streamed_output/-', 'value': 'arrison'},\n", + " {'op': 'replace', 'path': '/final_output', 'value': 'Harrison'})\n", "----------------------------------------\n", - "RunLogPatch({'op': 'add', 'path': '/streamed_output/-', 'value': ' at'})\n", + "RunLogPatch({'op': 'add', 'path': '/streamed_output/-', 'value': ' worked'},\n", + " {'op': 'replace', 'path': '/final_output', 'value': 'Harrison worked'})\n", "----------------------------------------\n", - "RunLogPatch({'op': 'add', 'path': '/streamed_output/-', 'value': ' Kens'})\n", + "RunLogPatch({'op': 'add', 'path': '/streamed_output/-', 'value': ' at'},\n", + " {'op': 'replace', 'path': '/final_output', 'value': 'Harrison worked at'})\n", "----------------------------------------\n", - "RunLogPatch({'op': 'add', 'path': '/streamed_output/-', 'value': 'ho'})\n", + "RunLogPatch({'op': 'add', 'path': '/streamed_output/-', 'value': ' Kens'},\n", + " {'op': 'replace', 'path': '/final_output', 'value': 'Harrison worked at Kens'})\n", "----------------------------------------\n", - "RunLogPatch({'op': 'add', 'path': '/streamed_output/-', 'value': '.'})\n", - "----------------------------------------\n", - "RunLogPatch({'op': 'add', 'path': '/streamed_output/-', 'value': ''})\n", + "RunLogPatch({'op': 'add', 'path': '/streamed_output/-', 'value': 'ho'},\n", + " {'op': 'replace',\n", + " 'path': '/final_output',\n", + " 'value': 'Harrison worked at Kensho'})\n", "----------------------------------------\n", - "RunLogPatch({'op': 'replace',\n", + "RunLogPatch({'op': 'add', 'path': '/streamed_output/-', 'value': '.'},\n", + " {'op': 'replace',\n", " 'path': '/final_output',\n", - " 'value': {'output': 'Harrison worked at Kensho.'}})\n" + " 'value': 'Harrison worked at Kensho.'})\n", + "----------------------------------------\n", + "RunLogPatch({'op': 'add', 'path': '/streamed_output/-', 'value': ''})\n" ] } ], "source": [ - "from langchain_community.vectorstores import FAISS\n", - "from langchain_core.output_parsers import StrOutputParser\n", - "from langchain_core.runnables import RunnablePassthrough\n", - "from langchain_openai import OpenAIEmbeddings\n", - "\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", - "\n", - "vectorstore = FAISS.from_texts(\n", - " [\"harrison worked at kensho\"], embedding=OpenAIEmbeddings()\n", - ")\n", - "retriever = vectorstore.as_retriever()\n", - "\n", - "retrieval_chain = (\n", - " {\n", - " \"context\": retriever.with_config(run_name=\"Docs\"),\n", - " \"question\": RunnablePassthrough(),\n", - " }\n", - " | prompt\n", - " | model\n", - " | StrOutputParser()\n", - ")\n", - "\n", "async for chunk in retrieval_chain.astream_log(\n", " \"where did harrison work?\", include_names=[\"Docs\"]\n", "):\n", @@ -714,7 +946,7 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": 16, "id": "5c26b731-b4eb-4967-a42a-dec813249ecb", "metadata": {}, "outputs": [ @@ -724,172 +956,185 @@ "text": [ "----------------------------------------------------------------------\n", "RunLog({'final_output': None,\n", - " 'id': 'afe66178-d75f-4c2d-b348-b1d144239cd6',\n", + " 'id': '431d1c55-7c50-48ac-b3a2-2f5ba5f35172',\n", " 'logs': {},\n", - " 'streamed_output': []})\n", + " 'name': 'RunnableSequence',\n", + " 'streamed_output': [],\n", + " 'type': 'chain'})\n", "----------------------------------------------------------------------\n", "RunLog({'final_output': None,\n", - " 'id': 'afe66178-d75f-4c2d-b348-b1d144239cd6',\n", + " 'id': '431d1c55-7c50-48ac-b3a2-2f5ba5f35172',\n", " 'logs': {'Docs': {'end_time': None,\n", " 'final_output': None,\n", - " 'id': '88d51118-5756-4891-89c5-2f6a5e90cc26',\n", - " 'metadata': {},\n", - " 'name': 'Docs',\n", - " 'start_time': '2023-10-19T17:52:15.438',\n", - " 'streamed_output_str': [],\n", - " 'tags': ['map:key:context', 'FAISS'],\n", - " 'type': 'retriever'}},\n", - " 'streamed_output': []})\n", - "----------------------------------------------------------------------\n", - "RunLog({'final_output': None,\n", - " 'id': 'afe66178-d75f-4c2d-b348-b1d144239cd6',\n", - " 'logs': {'Docs': {'end_time': '2023-10-19T17:52:15.738',\n", - " 'final_output': {'documents': [Document(page_content='harrison worked at kensho')]},\n", - " 'id': '88d51118-5756-4891-89c5-2f6a5e90cc26',\n", + " 'id': '8de10b49-d6af-4cb7-a4e7-fbadf6efa01e',\n", " 'metadata': {},\n", " 'name': 'Docs',\n", - " 'start_time': '2023-10-19T17:52:15.438',\n", + " 'start_time': '2024-01-19T22:33:56.939+00:00',\n", + " 'streamed_output': [],\n", " 'streamed_output_str': [],\n", - " 'tags': ['map:key:context', 'FAISS'],\n", + " 'tags': ['map:key:context', 'FAISS', 'OpenAIEmbeddings'],\n", " 'type': 'retriever'}},\n", - " 'streamed_output': []})\n", + " 'name': 'RunnableSequence',\n", + " 'streamed_output': [],\n", + " 'type': 'chain'})\n", "----------------------------------------------------------------------\n", "RunLog({'final_output': None,\n", - " 'id': 'afe66178-d75f-4c2d-b348-b1d144239cd6',\n", - " 'logs': {'Docs': {'end_time': '2023-10-19T17:52:15.738',\n", + " 'id': '431d1c55-7c50-48ac-b3a2-2f5ba5f35172',\n", + " 'logs': {'Docs': {'end_time': '2024-01-19T22:33:57.120+00:00',\n", " 'final_output': {'documents': [Document(page_content='harrison worked at kensho')]},\n", - " 'id': '88d51118-5756-4891-89c5-2f6a5e90cc26',\n", + " 'id': '8de10b49-d6af-4cb7-a4e7-fbadf6efa01e',\n", " 'metadata': {},\n", " 'name': 'Docs',\n", - " 'start_time': '2023-10-19T17:52:15.438',\n", + " 'start_time': '2024-01-19T22:33:56.939+00:00',\n", + " 'streamed_output': [],\n", " 'streamed_output_str': [],\n", - " 'tags': ['map:key:context', 'FAISS'],\n", + " 'tags': ['map:key:context', 'FAISS', 'OpenAIEmbeddings'],\n", " 'type': 'retriever'}},\n", - " 'streamed_output': ['']})\n", + " 'name': 'RunnableSequence',\n", + " 'streamed_output': [],\n", + " 'type': 'chain'})\n", "----------------------------------------------------------------------\n", - "RunLog({'final_output': None,\n", - " 'id': 'afe66178-d75f-4c2d-b348-b1d144239cd6',\n", - " 'logs': {'Docs': {'end_time': '2023-10-19T17:52:15.738',\n", + "RunLog({'final_output': '',\n", + " 'id': '431d1c55-7c50-48ac-b3a2-2f5ba5f35172',\n", + " 'logs': {'Docs': {'end_time': '2024-01-19T22:33:57.120+00:00',\n", " 'final_output': {'documents': [Document(page_content='harrison worked at kensho')]},\n", - " 'id': '88d51118-5756-4891-89c5-2f6a5e90cc26',\n", + " 'id': '8de10b49-d6af-4cb7-a4e7-fbadf6efa01e',\n", " 'metadata': {},\n", " 'name': 'Docs',\n", - " 'start_time': '2023-10-19T17:52:15.438',\n", + " 'start_time': '2024-01-19T22:33:56.939+00:00',\n", + " 'streamed_output': [],\n", " 'streamed_output_str': [],\n", - " 'tags': ['map:key:context', 'FAISS'],\n", + " 'tags': ['map:key:context', 'FAISS', 'OpenAIEmbeddings'],\n", " 'type': 'retriever'}},\n", - " 'streamed_output': ['', 'H']})\n", + " 'name': 'RunnableSequence',\n", + " 'streamed_output': [''],\n", + " 'type': 'chain'})\n", "----------------------------------------------------------------------\n", - "RunLog({'final_output': None,\n", - " 'id': 'afe66178-d75f-4c2d-b348-b1d144239cd6',\n", - " 'logs': {'Docs': {'end_time': '2023-10-19T17:52:15.738',\n", + "RunLog({'final_output': 'H',\n", + " 'id': '431d1c55-7c50-48ac-b3a2-2f5ba5f35172',\n", + " 'logs': {'Docs': {'end_time': '2024-01-19T22:33:57.120+00:00',\n", " 'final_output': {'documents': [Document(page_content='harrison worked at kensho')]},\n", - " 'id': '88d51118-5756-4891-89c5-2f6a5e90cc26',\n", + " 'id': '8de10b49-d6af-4cb7-a4e7-fbadf6efa01e',\n", " 'metadata': {},\n", " 'name': 'Docs',\n", - " 'start_time': '2023-10-19T17:52:15.438',\n", + " 'start_time': '2024-01-19T22:33:56.939+00:00',\n", + " 'streamed_output': [],\n", " 'streamed_output_str': [],\n", - " 'tags': ['map:key:context', 'FAISS'],\n", + " 'tags': ['map:key:context', 'FAISS', 'OpenAIEmbeddings'],\n", " 'type': 'retriever'}},\n", - " 'streamed_output': ['', 'H', 'arrison']})\n", + " 'name': 'RunnableSequence',\n", + " 'streamed_output': ['', 'H'],\n", + " 'type': 'chain'})\n", "----------------------------------------------------------------------\n", - "RunLog({'final_output': None,\n", - " 'id': 'afe66178-d75f-4c2d-b348-b1d144239cd6',\n", - " 'logs': {'Docs': {'end_time': '2023-10-19T17:52:15.738',\n", + "RunLog({'final_output': 'Harrison',\n", + " 'id': '431d1c55-7c50-48ac-b3a2-2f5ba5f35172',\n", + " 'logs': {'Docs': {'end_time': '2024-01-19T22:33:57.120+00:00',\n", " 'final_output': {'documents': [Document(page_content='harrison worked at kensho')]},\n", - " 'id': '88d51118-5756-4891-89c5-2f6a5e90cc26',\n", + " 'id': '8de10b49-d6af-4cb7-a4e7-fbadf6efa01e',\n", " 'metadata': {},\n", " 'name': 'Docs',\n", - " 'start_time': '2023-10-19T17:52:15.438',\n", + " 'start_time': '2024-01-19T22:33:56.939+00:00',\n", + " 'streamed_output': [],\n", " 'streamed_output_str': [],\n", - " 'tags': ['map:key:context', 'FAISS'],\n", + " 'tags': ['map:key:context', 'FAISS', 'OpenAIEmbeddings'],\n", " 'type': 'retriever'}},\n", - " 'streamed_output': ['', 'H', 'arrison', ' worked']})\n", + " 'name': 'RunnableSequence',\n", + " 'streamed_output': ['', 'H', 'arrison'],\n", + " 'type': 'chain'})\n", "----------------------------------------------------------------------\n", - "RunLog({'final_output': None,\n", - " 'id': 'afe66178-d75f-4c2d-b348-b1d144239cd6',\n", - " 'logs': {'Docs': {'end_time': '2023-10-19T17:52:15.738',\n", + "RunLog({'final_output': 'Harrison worked',\n", + " 'id': '431d1c55-7c50-48ac-b3a2-2f5ba5f35172',\n", + " 'logs': {'Docs': {'end_time': '2024-01-19T22:33:57.120+00:00',\n", " 'final_output': {'documents': [Document(page_content='harrison worked at kensho')]},\n", - " 'id': '88d51118-5756-4891-89c5-2f6a5e90cc26',\n", + " 'id': '8de10b49-d6af-4cb7-a4e7-fbadf6efa01e',\n", " 'metadata': {},\n", " 'name': 'Docs',\n", - " 'start_time': '2023-10-19T17:52:15.438',\n", + " 'start_time': '2024-01-19T22:33:56.939+00:00',\n", + " 'streamed_output': [],\n", " 'streamed_output_str': [],\n", - " 'tags': ['map:key:context', 'FAISS'],\n", + " 'tags': ['map:key:context', 'FAISS', 'OpenAIEmbeddings'],\n", " 'type': 'retriever'}},\n", - " 'streamed_output': ['', 'H', 'arrison', ' worked', ' at']})\n", + " 'name': 'RunnableSequence',\n", + " 'streamed_output': ['', 'H', 'arrison', ' worked'],\n", + " 'type': 'chain'})\n", "----------------------------------------------------------------------\n", - "RunLog({'final_output': None,\n", - " 'id': 'afe66178-d75f-4c2d-b348-b1d144239cd6',\n", - " 'logs': {'Docs': {'end_time': '2023-10-19T17:52:15.738',\n", + "RunLog({'final_output': 'Harrison worked at',\n", + " 'id': '431d1c55-7c50-48ac-b3a2-2f5ba5f35172',\n", + " 'logs': {'Docs': {'end_time': '2024-01-19T22:33:57.120+00:00',\n", " 'final_output': {'documents': [Document(page_content='harrison worked at kensho')]},\n", - " 'id': '88d51118-5756-4891-89c5-2f6a5e90cc26',\n", + " 'id': '8de10b49-d6af-4cb7-a4e7-fbadf6efa01e',\n", " 'metadata': {},\n", " 'name': 'Docs',\n", - " 'start_time': '2023-10-19T17:52:15.438',\n", + " 'start_time': '2024-01-19T22:33:56.939+00:00',\n", + " 'streamed_output': [],\n", " 'streamed_output_str': [],\n", - " 'tags': ['map:key:context', 'FAISS'],\n", + " 'tags': ['map:key:context', 'FAISS', 'OpenAIEmbeddings'],\n", " 'type': 'retriever'}},\n", - " 'streamed_output': ['', 'H', 'arrison', ' worked', ' at', ' Kens']})\n", + " 'name': 'RunnableSequence',\n", + " 'streamed_output': ['', 'H', 'arrison', ' worked', ' at'],\n", + " 'type': 'chain'})\n", "----------------------------------------------------------------------\n", - "RunLog({'final_output': None,\n", - " 'id': 'afe66178-d75f-4c2d-b348-b1d144239cd6',\n", - " 'logs': {'Docs': {'end_time': '2023-10-19T17:52:15.738',\n", + "RunLog({'final_output': 'Harrison worked at Kens',\n", + " 'id': '431d1c55-7c50-48ac-b3a2-2f5ba5f35172',\n", + " 'logs': {'Docs': {'end_time': '2024-01-19T22:33:57.120+00:00',\n", " 'final_output': {'documents': [Document(page_content='harrison worked at kensho')]},\n", - " 'id': '88d51118-5756-4891-89c5-2f6a5e90cc26',\n", + " 'id': '8de10b49-d6af-4cb7-a4e7-fbadf6efa01e',\n", " 'metadata': {},\n", " 'name': 'Docs',\n", - " 'start_time': '2023-10-19T17:52:15.438',\n", + " 'start_time': '2024-01-19T22:33:56.939+00:00',\n", + " 'streamed_output': [],\n", " 'streamed_output_str': [],\n", - " 'tags': ['map:key:context', 'FAISS'],\n", + " 'tags': ['map:key:context', 'FAISS', 'OpenAIEmbeddings'],\n", " 'type': 'retriever'}},\n", - " 'streamed_output': ['', 'H', 'arrison', ' worked', ' at', ' Kens', 'ho']})\n", + " 'name': 'RunnableSequence',\n", + " 'streamed_output': ['', 'H', 'arrison', ' worked', ' at', ' Kens'],\n", + " 'type': 'chain'})\n", "----------------------------------------------------------------------\n", - "RunLog({'final_output': None,\n", - " 'id': 'afe66178-d75f-4c2d-b348-b1d144239cd6',\n", - " 'logs': {'Docs': {'end_time': '2023-10-19T17:52:15.738',\n", + "RunLog({'final_output': 'Harrison worked at Kensho',\n", + " 'id': '431d1c55-7c50-48ac-b3a2-2f5ba5f35172',\n", + " 'logs': {'Docs': {'end_time': '2024-01-19T22:33:57.120+00:00',\n", " 'final_output': {'documents': [Document(page_content='harrison worked at kensho')]},\n", - " 'id': '88d51118-5756-4891-89c5-2f6a5e90cc26',\n", + " 'id': '8de10b49-d6af-4cb7-a4e7-fbadf6efa01e',\n", " 'metadata': {},\n", " 'name': 'Docs',\n", - " 'start_time': '2023-10-19T17:52:15.438',\n", + " 'start_time': '2024-01-19T22:33:56.939+00:00',\n", + " 'streamed_output': [],\n", " 'streamed_output_str': [],\n", - " 'tags': ['map:key:context', 'FAISS'],\n", + " 'tags': ['map:key:context', 'FAISS', 'OpenAIEmbeddings'],\n", " 'type': 'retriever'}},\n", - " 'streamed_output': ['', 'H', 'arrison', ' worked', ' at', ' Kens', 'ho', '.']})\n", + " 'name': 'RunnableSequence',\n", + " 'streamed_output': ['', 'H', 'arrison', ' worked', ' at', ' Kens', 'ho'],\n", + " 'type': 'chain'})\n", "----------------------------------------------------------------------\n", - "RunLog({'final_output': None,\n", - " 'id': 'afe66178-d75f-4c2d-b348-b1d144239cd6',\n", - " 'logs': {'Docs': {'end_time': '2023-10-19T17:52:15.738',\n", + "RunLog({'final_output': 'Harrison worked at Kensho.',\n", + " 'id': '431d1c55-7c50-48ac-b3a2-2f5ba5f35172',\n", + " 'logs': {'Docs': {'end_time': '2024-01-19T22:33:57.120+00:00',\n", " 'final_output': {'documents': [Document(page_content='harrison worked at kensho')]},\n", - " 'id': '88d51118-5756-4891-89c5-2f6a5e90cc26',\n", + " 'id': '8de10b49-d6af-4cb7-a4e7-fbadf6efa01e',\n", " 'metadata': {},\n", " 'name': 'Docs',\n", - " 'start_time': '2023-10-19T17:52:15.438',\n", + " 'start_time': '2024-01-19T22:33:56.939+00:00',\n", + " 'streamed_output': [],\n", " 'streamed_output_str': [],\n", - " 'tags': ['map:key:context', 'FAISS'],\n", + " 'tags': ['map:key:context', 'FAISS', 'OpenAIEmbeddings'],\n", " 'type': 'retriever'}},\n", - " 'streamed_output': ['',\n", - " 'H',\n", - " 'arrison',\n", - " ' worked',\n", - " ' at',\n", - " ' Kens',\n", - " 'ho',\n", - " '.',\n", - " '']})\n", + " 'name': 'RunnableSequence',\n", + " 'streamed_output': ['', 'H', 'arrison', ' worked', ' at', ' Kens', 'ho', '.'],\n", + " 'type': 'chain'})\n", "----------------------------------------------------------------------\n", - "RunLog({'final_output': {'output': 'Harrison worked at Kensho.'},\n", - " 'id': 'afe66178-d75f-4c2d-b348-b1d144239cd6',\n", - " 'logs': {'Docs': {'end_time': '2023-10-19T17:52:15.738',\n", + "RunLog({'final_output': 'Harrison worked at Kensho.',\n", + " 'id': '431d1c55-7c50-48ac-b3a2-2f5ba5f35172',\n", + " 'logs': {'Docs': {'end_time': '2024-01-19T22:33:57.120+00:00',\n", " 'final_output': {'documents': [Document(page_content='harrison worked at kensho')]},\n", - " 'id': '88d51118-5756-4891-89c5-2f6a5e90cc26',\n", + " 'id': '8de10b49-d6af-4cb7-a4e7-fbadf6efa01e',\n", " 'metadata': {},\n", " 'name': 'Docs',\n", - " 'start_time': '2023-10-19T17:52:15.438',\n", + " 'start_time': '2024-01-19T22:33:56.939+00:00',\n", + " 'streamed_output': [],\n", " 'streamed_output_str': [],\n", - " 'tags': ['map:key:context', 'FAISS'],\n", + " 'tags': ['map:key:context', 'FAISS', 'OpenAIEmbeddings'],\n", " 'type': 'retriever'}},\n", + " 'name': 'RunnableSequence',\n", " 'streamed_output': ['',\n", " 'H',\n", " 'arrison',\n", @@ -898,7 +1143,8 @@ " ' Kens',\n", " 'ho',\n", " '.',\n", - " '']})\n" + " ''],\n", + " 'type': 'chain'})\n" ] } ], @@ -923,7 +1169,7 @@ }, { "cell_type": "code", - "execution_count": 32, + "execution_count": 17, "id": "0a1c409d", "metadata": {}, "outputs": [], @@ -940,7 +1186,7 @@ }, { "cell_type": "code", - "execution_count": 43, + "execution_count": 18, "id": "08044c0a", "metadata": {}, "outputs": [ @@ -948,8 +1194,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "CPU times: user 54.3 ms, sys: 0 ns, total: 54.3 ms\n", - "Wall time: 2.29 s\n" + "CPU times: user 18 ms, sys: 1.27 ms, total: 19.3 ms\n", + "Wall time: 692 ms\n" ] }, { @@ -958,7 +1204,7 @@ "AIMessage(content=\"Why don't bears wear shoes?\\n\\nBecause they already have bear feet!\")" ] }, - "execution_count": 43, + "execution_count": 18, "metadata": {}, "output_type": "execute_result" } @@ -970,7 +1216,7 @@ }, { "cell_type": "code", - "execution_count": 44, + "execution_count": 19, "id": "22c56804", "metadata": {}, "outputs": [ @@ -978,17 +1224,17 @@ "name": "stdout", "output_type": "stream", "text": [ - "CPU times: user 7.8 ms, sys: 0 ns, total: 7.8 ms\n", - "Wall time: 1.43 s\n" + "CPU times: user 10.5 ms, sys: 166 µs, total: 10.7 ms\n", + "Wall time: 579 ms\n" ] }, { "data": { "text/plain": [ - "AIMessage(content=\"In wild embrace,\\nNature's strength roams with grace.\")" + "AIMessage(content=\"In forest's embrace,\\nMajestic bears pace.\")" ] }, - "execution_count": 44, + "execution_count": 19, "metadata": {}, "output_type": "execute_result" } @@ -1000,7 +1246,7 @@ }, { "cell_type": "code", - "execution_count": 45, + "execution_count": 20, "id": "4fff4cbb", "metadata": {}, "outputs": [ @@ -1008,18 +1254,18 @@ "name": "stdout", "output_type": "stream", "text": [ - "CPU times: user 167 ms, sys: 921 µs, total: 168 ms\n", - "Wall time: 1.56 s\n" + "CPU times: user 32 ms, sys: 2.59 ms, total: 34.6 ms\n", + "Wall time: 816 ms\n" ] }, { "data": { "text/plain": [ - "{'joke': AIMessage(content=\"Why don't bears wear shoes?\\n\\nBecause they already have bear feet!\"),\n", - " 'poem': AIMessage(content=\"Fierce and wild, nature's might,\\nBears roam the woods, shadows of the night.\")}" + "{'joke': AIMessage(content=\"Sure, here's a bear-related joke for you:\\n\\nWhy did the bear bring a ladder to the bar?\\n\\nBecause he heard the drinks were on the house!\"),\n", + " 'poem': AIMessage(content=\"In wilderness they roam,\\nMajestic strength, nature's throne.\")}" ] }, - "execution_count": 45, + "execution_count": 20, "metadata": {}, "output_type": "execute_result" } @@ -1042,7 +1288,7 @@ }, { "cell_type": "code", - "execution_count": 40, + "execution_count": 21, "id": "f67d2268-c766-441b-8d64-57b8219ccb34", "metadata": {}, "outputs": [ @@ -1050,18 +1296,18 @@ "name": "stdout", "output_type": "stream", "text": [ - "CPU times: user 159 ms, sys: 3.66 ms, total: 163 ms\n", - "Wall time: 1.34 s\n" + "CPU times: user 17.3 ms, sys: 4.84 ms, total: 22.2 ms\n", + "Wall time: 628 ms\n" ] }, { "data": { "text/plain": [ - "[AIMessage(content=\"Why don't bears wear shoes?\\n\\nBecause they already have bear feet!\"),\n", - " AIMessage(content=\"Sure, here's a cat joke for you:\\n\\nWhy don't cats play poker in the wild?\\n\\nBecause there are too many cheetahs!\")]" + "[AIMessage(content=\"Why don't bears wear shoes?\\n\\nBecause they have bear feet!\"),\n", + " AIMessage(content=\"Why don't cats play poker in the wild?\\n\\nToo many cheetahs!\")]" ] }, - "execution_count": 40, + "execution_count": 21, "metadata": {}, "output_type": "execute_result" } @@ -1073,7 +1319,7 @@ }, { "cell_type": "code", - "execution_count": 41, + "execution_count": 22, "id": "83c8d511-9563-403e-9c06-cae986cf5dee", "metadata": {}, "outputs": [ @@ -1081,18 +1327,18 @@ "name": "stdout", "output_type": "stream", "text": [ - "CPU times: user 165 ms, sys: 0 ns, total: 165 ms\n", - "Wall time: 1.73 s\n" + "CPU times: user 15.8 ms, sys: 3.83 ms, total: 19.7 ms\n", + "Wall time: 718 ms\n" ] }, { "data": { "text/plain": [ - "[AIMessage(content=\"Silent giants roam,\\nNature's strength, love's emblem shown.\"),\n", - " AIMessage(content='Whiskers aglow, paws tiptoe,\\nGraceful hunters, hearts aglow.')]" + "[AIMessage(content='In the wild, bears roam,\\nMajestic guardians of ancient home.'),\n", + " AIMessage(content='Whiskers grace, eyes gleam,\\nCats dance through the moonbeam.')]" ] }, - "execution_count": 41, + "execution_count": 22, "metadata": {}, "output_type": "execute_result" } @@ -1104,7 +1350,7 @@ }, { "cell_type": "code", - "execution_count": 42, + "execution_count": 23, "id": "07a81230-8db8-4b96-bdcb-99ae1d171f2f", "metadata": {}, "outputs": [ @@ -1112,20 +1358,20 @@ "name": "stdout", "output_type": "stream", "text": [ - "CPU times: user 507 ms, sys: 125 ms, total: 632 ms\n", - "Wall time: 1.49 s\n" + "CPU times: user 44.8 ms, sys: 3.17 ms, total: 48 ms\n", + "Wall time: 721 ms\n" ] }, { "data": { "text/plain": [ - "[{'joke': AIMessage(content=\"Why don't bears wear shoes?\\n\\nBecause they already have bear feet!\"),\n", - " 'poem': AIMessage(content=\"Majestic bears roam,\\nNature's wild guardians of home.\")},\n", - " {'joke': AIMessage(content=\"Sure, here's a cat joke for you:\\n\\nWhy did the cat sit on the computer?\\n\\nBecause it wanted to keep an eye on the mouse!\"),\n", - " 'poem': AIMessage(content='Whiskers twitch, eyes gleam,\\nGraceful creatures, feline dream.')}]" + "[{'joke': AIMessage(content=\"Sure, here's a bear joke for you:\\n\\nWhy don't bears wear shoes?\\n\\nBecause they have bear feet!\"),\n", + " 'poem': AIMessage(content=\"Majestic bears roam,\\nNature's strength, beauty shown.\")},\n", + " {'joke': AIMessage(content=\"Why don't cats play poker in the wild?\\n\\nToo many cheetahs!\"),\n", + " 'poem': AIMessage(content=\"Whiskers dance, eyes aglow,\\nCats embrace the night's gentle flow.\")}]" ] }, - "execution_count": 42, + "execution_count": 23, "metadata": {}, "output_type": "execute_result" } @@ -1134,14 +1380,6 @@ "%%time\n", "combined.batch([{\"topic\": \"bears\"}, {\"topic\": \"cats\"}])" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5218cafd-370a-4e3a-85a0-452e570092fe", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { @@ -1160,7 +1398,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.1" + "version": "3.11.4" } }, "nbformat": 4, diff --git a/docs/docs/get_started/introduction.mdx b/docs/docs/get_started/introduction.mdx index d6219868e5415..4bf8b1fa9b590 100644 --- a/docs/docs/get_started/introduction.mdx +++ b/docs/docs/get_started/introduction.mdx @@ -78,7 +78,7 @@ Let models choose which tools to use given high-level directives Walkthroughs and techniques for common end-to-end use cases, like: - [Document question answering](/docs/use_cases/question_answering/) - [Chatbots](/docs/use_cases/chatbots/) -- [Analyzing structured data](/docs/use_cases/qa_structured/sql/) +- [Analyzing structured data](/docs/use_cases/sql/) - and much more... ### [Integrations](/docs/integrations/providers/) diff --git a/docs/docs/get_started/quickstart.mdx b/docs/docs/get_started/quickstart.mdx index 4c5462ead82d2..d07d3f2939ca3 100644 --- a/docs/docs/get_started/quickstart.mdx +++ b/docs/docs/get_started/quickstart.mdx @@ -59,7 +59,7 @@ In this quickstart, we will walk through a few different ways of doing that. We will start with a simple LLM chain, which just relies on information in the prompt template to respond. Next, we will build a retrieval chain, which fetches data from a separate database and passes that into the prompt template. We will then add in chat history, to create a conversation retrieval chain. This allows you interact in a chat manner with this LLM, so it remembers previous questions. -Finally, we will build an agent - which utilizes and LLM to determine whether or not it needs to fetch data to answer questions. +Finally, we will build an agent - which utilizes an LLM to determine whether or not it needs to fetch data to answer questions. We will cover these at a high level, but there are lot of details to all of these! We will link to relevant docs. @@ -597,6 +597,6 @@ To continue on your journey, we recommend you read the following (in order): - [Model IO](/docs/modules/model_io) covers more details of prompts, LLMs, and output parsers. - [Retrieval](/docs/modules/data_connection) covers more details of everything related to retrieval - [Agents](/docs/modules/agents) covers details of everything related to agents -- Explore common [end-to-end use cases](/docs/use_cases/qa_structured/sql) and [template applications](/docs/templates) +- Explore common [end-to-end use cases](/docs/use_cases/) and [template applications](/docs/templates) - [Read up on LangSmith](/docs/langsmith/), the platform for debugging, testing, monitoring and more - Learn more about serving your applications with [LangServe](/docs/langserve) diff --git a/docs/docs/guides/model_laboratory.ipynb b/docs/docs/guides/model_laboratory.ipynb index b6533de4c0d07..540bc4023fb6b 100644 --- a/docs/docs/guides/model_laboratory.ipynb +++ b/docs/docs/guides/model_laboratory.ipynb @@ -35,6 +35,22 @@ "from langchain_openai import OpenAI" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "3dd69cb4", + "metadata": {}, + "outputs": [], + "source": [ + "import getpass\n", + "import os\n", + "\n", + "# get a new token: https://dashboard.cohere.ai/\n", + "os.environ[\"COHERE_API_KEY\"] = getpass.getpass(\"Cohere API Key:\")\n", + "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"Open API Key:\")\n", + "os.environ[\"HUGGINGFACEHUB_API_TOKEN\"] = getpass.getpass(\"Hugging Face API Key:\")" + ] + }, { "cell_type": "code", "execution_count": 2, @@ -44,7 +60,7 @@ "source": [ "llms = [\n", " OpenAI(temperature=0),\n", - " Cohere(model=\"command-xlarge-20221108\", max_tokens=20, temperature=0),\n", + " Cohere(temperature=0),\n", " HuggingFaceHub(repo_id=\"google/flan-t5-xl\", model_kwargs={\"temperature\": 1}),\n", "]" ] @@ -160,7 +176,7 @@ " llm=open_ai_llm, search_chain=search, verbose=True\n", ")\n", "\n", - "cohere_llm = Cohere(temperature=0, model=\"command-xlarge-20221108\")\n", + "cohere_llm = Cohere(temperature=0)\n", "search = SerpAPIWrapper()\n", "self_ask_with_search_cohere = SelfAskWithSearchChain(\n", " llm=cohere_llm, search_chain=search, verbose=True\n", @@ -241,14 +257,6 @@ "source": [ "model_lab.compare(\"What is the hometown of the reigning men's U.S. Open champion?\")" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "94159131", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/docs/docs/integrations/callbacks/comet_tracing.ipynb b/docs/docs/integrations/callbacks/comet_tracing.ipynb new file mode 100644 index 0000000000000..aff2be5610275 --- /dev/null +++ b/docs/docs/integrations/callbacks/comet_tracing.ipynb @@ -0,0 +1,138 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "5371a9bb", + "metadata": {}, + "source": [ + "# Comet Tracing\n", + "\n", + "There are two ways to trace your LangChains executions with Comet:\n", + "\n", + "1. Setting the `LANGCHAIN_COMET_TRACING` environment variable to \"true\". This is the recommended way.\n", + "2. Import the `CometTracer` manually and pass it explicitely." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17c04cc6-c93d-4b6c-a033-e897577f4ed1", + "metadata": { + "ExecuteTime": { + "end_time": "2023-05-18T12:47:46.580776Z", + "start_time": "2023-05-18T12:47:46.577833Z" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "import comet_llm\n", + "\n", + "os.environ[\"LANGCHAIN_COMET_TRACING\"] = \"true\"\n", + "\n", + "# Connect to Comet if no API Key is set\n", + "comet_llm.init()\n", + "\n", + "# comet documentation to configure comet using env variables\n", + "# https://www.comet.com/docs/v2/api-and-sdk/llm-sdk/configuration/\n", + "# here we are configuring the comet project\n", + "os.environ[\"COMET_PROJECT_NAME\"] = \"comet-example-langchain-tracing\"\n", + "\n", + "from langchain.agents import AgentType, initialize_agent, load_tools\n", + "from langchain.llms import OpenAI" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1b62cd48", + "metadata": { + "ExecuteTime": { + "end_time": "2023-05-18T12:47:47.445229Z", + "start_time": "2023-05-18T12:47:47.436424Z" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "# Agent run with tracing. Ensure that OPENAI_API_KEY is set appropriately to run this example.\n", + "\n", + "llm = OpenAI(temperature=0)\n", + "tools = load_tools([\"llm-math\"], llm=llm)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bfa16b79-aa4b-4d41-a067-70d1f593f667", + "metadata": { + "ExecuteTime": { + "end_time": "2023-05-18T12:48:01.816137Z", + "start_time": "2023-05-18T12:47:49.109574Z" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "agent = initialize_agent(\n", + " tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, verbose=True\n", + ")\n", + "\n", + "agent.run(\"What is 2 raised to .123243 power?\") # this should be traced\n", + "# An url for the chain like the following should print in your console:\n", + "# https://www.comet.com//\n", + "# The url can be used to view the LLM chain in Comet." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5e212e7d", + "metadata": {}, + "outputs": [], + "source": [ + "# Now, we unset the environment variable and use a context manager.\n", + "if \"LANGCHAIN_COMET_TRACING\" in os.environ:\n", + " del os.environ[\"LANGCHAIN_COMET_TRACING\"]\n", + "\n", + "from langchain.callbacks.tracers.comet import CometTracer\n", + "\n", + "tracer = CometTracer()\n", + "\n", + "# Recreate the LLM, tools and agent and passing the callback to each of them\n", + "llm = OpenAI(temperature=0)\n", + "tools = load_tools([\"llm-math\"], llm=llm)\n", + "agent = initialize_agent(\n", + " tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, verbose=True\n", + ")\n", + "\n", + "agent.run(\n", + " \"What is 2 raised to .123243 power?\", callbacks=[tracer]\n", + ") # this should be traced" + ] + } + ], + "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.9.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/docs/integrations/callbacks/streamlit.md b/docs/docs/integrations/callbacks/streamlit.md index 776f0f6d9c26a..47b39cfd0b6a6 100644 --- a/docs/docs/integrations/callbacks/streamlit.md +++ b/docs/docs/integrations/callbacks/streamlit.md @@ -46,7 +46,7 @@ thoughts and actions live in your app. ```python from langchain_openai import OpenAI from langchain.agents import AgentType, initialize_agent, load_tools -from langchain.callbacks import StreamlitCallbackHandler +from langchain_community.callbacks import StreamlitCallbackHandler import streamlit as st llm = OpenAI(temperature=0, streaming=True) diff --git a/docs/docs/integrations/chat/anthropic.ipynb b/docs/docs/integrations/chat/anthropic.ipynb index b7c5fbce30484..f8aa35142de11 100644 --- a/docs/docs/integrations/chat/anthropic.ipynb +++ b/docs/docs/integrations/chat/anthropic.ipynb @@ -22,44 +22,84 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "d4a7c55d-b235-4ca4-a579-c90cc9570da9", "metadata": { + "ExecuteTime": { + "end_time": "2024-01-19T11:25:00.590587Z", + "start_time": "2024-01-19T11:25:00.127293Z" + }, "tags": [] }, "outputs": [], "source": [ - "from langchain.schema import HumanMessage\n", - "from langchain_community.chat_models import ChatAnthropic" + "from langchain_community.chat_models import ChatAnthropic\n", + "from langchain_core.prompts import ChatPromptTemplate" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "70cf04e8-423a-4ff6-8b09-f11fb711c817", "metadata": { + "ExecuteTime": { + "end_time": "2024-01-19T11:25:04.349676Z", + "start_time": "2024-01-19T11:25:03.964930Z" + }, "tags": [] }, "outputs": [], "source": [ - "chat = ChatAnthropic()" + "chat = ChatAnthropic(temperature=0, model_name=\"claude-2\")" + ] + }, + { + "cell_type": "markdown", + "id": "d1f9df276476f0bc", + "metadata": { + "collapsed": false + }, + "source": [ + "The code provided assumes that your ANTHROPIC_API_KEY is set in your environment variables. If you would like to manually specify your API key and also choose a different model, you can use the following code:\n", + "```python\n", + "chat = ChatAnthropic(temperature=0, anthropic_api_key=\"YOUR_API_KEY\", model_name=\"claude-instant-1.2\")\n", + "\n", + "```\n", + "Please note that the default model is \"claude-2,\" and you can check the available models at [here](https://docs.anthropic.com/claude/reference/selecting-a-model)." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "8199ef8f-eb8b-4253-9ea0-6c24a013ca4c", "metadata": { + "ExecuteTime": { + "end_time": "2024-01-19T11:25:07.274418Z", + "start_time": "2024-01-19T11:25:05.898031Z" + }, "tags": [] }, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": "AIMessage(content=' 저는 파이썬을 좋아합니다.')" + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "messages = [\n", - " HumanMessage(\n", - " content=\"Translate this sentence from English to French. I love programming.\"\n", - " )\n", - "]\n", - "chat.invoke(messages)" + "system = \"You are a helpful assistant that translates {input_language} to {output_language}.\"\n", + "human = \"{text}\"\n", + "prompt = ChatPromptTemplate.from_messages([(\"system\", system), (\"human\", human)])\n", + "\n", + "chain = prompt | chat\n", + "chain.invoke({\n", + " \"input_language\": \"English\",\n", + " \"output_language\": \"Korean\",\n", + " \"text\": \"I love Python\",\n", + "})" ] }, { @@ -72,44 +112,78 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "93a21c5c-6ef9-4688-be60-b2e1f94842fb", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "from langchain.callbacks.manager import CallbackManager\n", - "from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler" - ] - }, - { - "cell_type": "code", - "execution_count": null, + "execution_count": 4, "id": "c5fac0e9-05a4-4fc1-a3b3-e5bbb24b971b", "metadata": { + "ExecuteTime": { + "end_time": "2024-01-19T11:25:10.448733Z", + "start_time": "2024-01-19T11:25:08.866277Z" + }, "tags": [] }, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": "AIMessage(content=\" Why don't bears like fast food? Because they can't catch it!\")" + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "await chat.ainvoke([messages])" + "chat = ChatAnthropic(temperature=0, model_name=\"claude-2\")\n", + "prompt = ChatPromptTemplate.from_messages([(\"human\", \"Tell me a joke about {topic}\")])\n", + "chain = prompt | chat\n", + "await chain.ainvoke({\"topic\": \"bear\"})" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "id": "025be980-e50d-4a68-93dc-c9c7b500ce34", "metadata": { + "ExecuteTime": { + "end_time": "2024-01-19T11:25:24.438696Z", + "start_time": "2024-01-19T11:25:14.687480Z" + }, "tags": [] }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " Here are some of the most famous tourist attractions in Japan:\n", + "\n", + "- Tokyo - Tokyo Tower, Tokyo Skytree, Imperial Palace, Sensoji Temple, Meiji Shrine, Shibuya Crossing\n", + "\n", + "- Kyoto - Kinkakuji (Golden Pavilion), Fushimi Inari Shrine, Kiyomizu-dera Temple, Arashiyama Bamboo Grove, Gion Geisha District\n", + "\n", + "- Osaka - Osaka Castle, Dotonbori, Universal Studios Japan, Osaka Aquarium Kaiyukan \n", + "\n", + "- Hiroshima - Hiroshima Peace Memorial Park and Museum, Itsukushima Shrine (Miyajima Island)\n", + "\n", + "- Mount Fuji - Iconic and famous mountain, popular for hiking and viewing from places like Hakone and Kawaguchiko Lake\n", + "\n", + "- Himeji - Himeji Castle, one of Japan's most impressive feudal castles\n", + "\n", + "- Nara - Todaiji Temple, Nara Park with its bowing deer, Horyuji Temple with some of world's oldest wooden structures \n", + "\n", + "- Nikko - Elaborate shrines and temples nestled around Nikko National Park\n", + "\n", + "- Sapporo - Snow" + ] + } + ], "source": [ - "chat = ChatAnthropic(\n", - " streaming=True,\n", - " verbose=True,\n", - " callback_manager=CallbackManager([StreamingStdOutCallbackHandler()]),\n", + "chat = ChatAnthropic(temperature=0.3, model_name=\"claude-2\")\n", + "prompt = ChatPromptTemplate.from_messages(\n", + " [(\"human\", \"Give me a list of famous tourist attractions in Japan\")]\n", ")\n", - "chat.stream(messages)" + "chain = prompt | chat\n", + "for chunk in chain.stream({}):\n", + " print(chunk.content, end=\"\", flush=True)" ] }, { @@ -134,15 +208,130 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "id": "07c47c2a", - "metadata": {}, - "outputs": [], + "metadata": { + "ExecuteTime": { + "end_time": "2024-01-19T11:25:25.288133Z", + "start_time": "2024-01-19T11:25:24.438968Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": "AIMessage(content='파이썬을 사랑합니다.')" + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "from langchain_anthropic import ChatAnthropicMessages\n", "\n", "chat = ChatAnthropicMessages(model_name=\"claude-instant-1.2\")\n", - "chat.invoke(messages)" + "system = (\n", + " \"You are a helpful assistant that translates {input_language} to {output_language}.\"\n", + ")\n", + "human = \"{text}\"\n", + "prompt = ChatPromptTemplate.from_messages([(\"system\", system), (\"human\", human)])\n", + "\n", + "chain = prompt | chat\n", + "chain.invoke(\n", + " {\n", + " \"input_language\": \"English\",\n", + " \"output_language\": \"Korean\",\n", + " \"text\": \"I love Python\",\n", + " }\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "19e53d75935143fd", + "metadata": { + "collapsed": false + }, + "source": [ + "ChatAnthropicMessages also requires the anthropic_api_key argument, or the ANTHROPIC_API_KEY environment variable must be set. \n", + "\n", + "ChatAnthropicMessages also supports async and streaming functionality:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "e20a139d30e3d333", + "metadata": { + "ExecuteTime": { + "end_time": "2024-01-19T11:25:26.012325Z", + "start_time": "2024-01-19T11:25:25.288358Z" + }, + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/plain": "AIMessage(content='파이썬을 사랑합니다.')" + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "await chain.ainvoke(\n", + " {\n", + " \"input_language\": \"English\",\n", + " \"output_language\": \"Korean\",\n", + " \"text\": \"I love Python\",\n", + " }\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "6f34f1073d7e7120", + "metadata": { + "ExecuteTime": { + "end_time": "2024-01-19T11:25:28.323455Z", + "start_time": "2024-01-19T11:25:26.012040Z" + }, + "collapsed": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Here are some of the most famous tourist attractions in Japan:\n", + "\n", + "- Tokyo Tower - A communication and observation tower in Tokyo modeled after the Eiffel Tower. It offers stunning views of the city.\n", + "\n", + "- Mount Fuji - Japan's highest and most famous mountain. It's a iconic symbol of Japan and a UNESCO World Heritage Site. \n", + "\n", + "- Itsukushima Shrine (Miyajima) - A shrine located on an island in Hiroshima prefecture, known for its \"floating\" torii gate that seems to float on water during high tide.\n", + "\n", + "- Himeji Castle - A UNESCO World Heritage Site famous for having withstood numerous battles without destruction to its intricate white walls and sloping, triangular roofs. \n", + "\n", + "- Kawaguchiko Station - Near Mount Fuji, this area is known for its scenic Fuji Five Lakes region. \n", + "\n", + "- Hiroshima Peace Memorial Park and Museum - Commemorates the world's first atomic bombing in Hiroshima on August 6, 1945. \n", + "\n", + "- Arashiyama Bamboo Grove - A renowned bamboo forest located in Kyoto that draws many visitors.\n", + "\n", + "- Kegon Falls - One of Japan's largest waterfalls" + ] + } + ], + "source": [ + "prompt = ChatPromptTemplate.from_messages(\n", + " [(\"human\", \"Give me a list of famous tourist attractions in Japan\")]\n", + ")\n", + "chain = prompt | chat\n", + "for chunk in chain.stream({}):\n", + " print(chunk.content, end=\"\", flush=True)" ] } ], diff --git a/docs/docs/integrations/chat/azureml_chat_endpoint.ipynb b/docs/docs/integrations/chat/azureml_chat_endpoint.ipynb index 61d3cf14cda5b..6fe4c869f4f94 100644 --- a/docs/docs/integrations/chat/azureml_chat_endpoint.ipynb +++ b/docs/docs/integrations/chat/azureml_chat_endpoint.ipynb @@ -15,9 +15,9 @@ "source": [ "# AzureMLChatOnlineEndpoint\n", "\n", - ">[Azure Machine Learning](https://azure.microsoft.com/en-us/products/machine-learning/) is a platform used to build, train, and deploy machine learning models. Users can explore the types of models to deploy in the Model Catalog, which provides Azure Foundation Models and OpenAI Models. `Azure Foundation Models` include various open-source models and popular Hugging Face models. Users can also import models of their liking into AzureML.\n", + ">[Azure Machine Learning](https://azure.microsoft.com/en-us/products/machine-learning/) is a platform used to build, train, and deploy machine learning models. Users can explore the types of models to deploy in the Model Catalog, which provides foundational and general purpose models from different providers.\n", ">\n", - ">[Azure Machine Learning Online Endpoints](https://learn.microsoft.com/en-us/azure/machine-learning/concept-endpoints). After you train machine learning models or pipelines, you need to deploy them to production so that others can use them for inference. Inference is the process of applying new input data to the machine learning model or pipeline to generate outputs. While these outputs are typically referred to as \"predictions,\" inferencing can be used to generate outputs for other machine learning tasks, such as classification and clustering. In `Azure Machine Learning`, you perform inferencing by using endpoints and deployments. `Endpoints` and `Deployments` allow you to decouple the interface of your production workload from the implementation that serves it.\n", + ">In general, you need to deploy models in order to consume its predictions (inference). In `Azure Machine Learning`, [Online Endpoints](https://learn.microsoft.com/en-us/azure/machine-learning/concept-endpoints) are used to deploy these models with a real-time serving. They are based on the ideas of `Endpoints` and `Deployments` which allow you to decouple the interface of your production workload from the implementation that serves it.\n", "\n", "This notebook goes over how to use a chat model hosted on an `Azure Machine Learning Endpoint`." ] @@ -37,10 +37,11 @@ "source": [ "## Set up\n", "\n", - "To use the wrapper, you must [deploy a model on AzureML](https://learn.microsoft.com/en-us/azure/machine-learning/how-to-use-foundation-models?view=azureml-api-2#deploying-foundation-models-to-endpoints-for-inferencing) and obtain the following parameters:\n", + "You must [deploy a model on Azure ML](https://learn.microsoft.com/en-us/azure/machine-learning/how-to-use-foundation-models?view=azureml-api-2#deploying-foundation-models-to-endpoints-for-inferencing) or [to Azure AI studio](https://learn.microsoft.com/en-us/azure/ai-studio/how-to/deploy-models-open) and obtain the following parameters:\n", "\n", - "* `endpoint_api_key`: The API key provided by the endpoint\n", - "* `endpoint_url`: The REST endpoint url provided by the endpoint" + "* `endpoint_url`: The REST endpoint url provided by the endpoint.\n", + "* `endpoint_api_type`: Use `endpoint_type='realtime'` when deploying models to **Realtime endpoints** (hosted managed infrastructure). Use `endpoint_type='serverless'` when deploying models using the **Pay-as-you-go** offering (model as a service).\n", + "* `endpoint_api_key`: The API key provided by the endpoint" ] }, { @@ -51,7 +52,40 @@ "\n", "The `content_formatter` parameter is a handler class for transforming the request and response of an AzureML endpoint to match with required schema. Since there are a wide range of models in the model catalog, each of which may process data differently from one another, a `ContentFormatterBase` class is provided to allow users to transform data to their liking. The following content formatters are provided:\n", "\n", - "* `LLamaContentFormatter`: Formats request and response data for LLaMa2-chat" + "* `LLamaChatContentFormatter`: Formats request and response data for LLaMa2-chat\n", + "\n", + "*Note: `langchain.chat_models.azureml_endpoint.LLamaContentFormatter` is being deprecated and replaced with `langchain.chat_models.azureml_endpoint.LLamaChatContentFormatter`.*\n", + "\n", + "You can implement custom content formatters specific for your model deriving from the class `langchain_community.llms.azureml_endpoint.ContentFormatterBase`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Examples\n", + "\n", + "The following section cotain examples about how to use this class:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from langchain.schema import HumanMessage\n", + "from langchain_community.chat_models.azureml_endpoint import (\n", + " AzureMLEndpointApiType,\n", + " LlamaChatContentFormatter,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example: Chat completions with real-time endpoints" ] }, { @@ -76,11 +110,79 @@ "\n", "chat = AzureMLChatOnlineEndpoint(\n", " endpoint_url=\"https://..inference.ml.azure.com/score\",\n", + " endpoint_api_type=AzureMLEndpointApiType.realtime,\n", " endpoint_api_key=\"my-api-key\",\n", - " content_formatter=LlamaContentFormatter,\n", + " content_formatter=LlamaChatContentFormatter(),\n", ")\n", - "response = chat(\n", - " messages=[HumanMessage(content=\"Will the Collatz conjecture ever be solved?\")]\n", + "response = chat.invoke(\n", + " [HumanMessage(content=\"Will the Collatz conjecture ever be solved?\")]\n", + ")\n", + "response" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example: Chat completions with pay-as-you-go deployments (model as a service)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "chat = AzureMLChatOnlineEndpoint(\n", + " endpoint_url=\"https://..inference.ml.azure.com/v1/chat/completions\",\n", + " endpoint_api_type=AzureMLEndpointApiType.serverless,\n", + " endpoint_api_key=\"my-api-key\",\n", + " content_formatter=LlamaChatContentFormatter,\n", + ")\n", + "response = chat.invoke(\n", + " [HumanMessage(content=\"Will the Collatz conjecture ever be solved?\")]\n", + ")\n", + "response" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If you need to pass additional parameters to the model, use `model_kwards` argument:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "chat = AzureMLChatOnlineEndpoint(\n", + " endpoint_url=\"https://..inference.ml.azure.com/v1/chat/completions\",\n", + " endpoint_api_type=AzureMLEndpointApiType.serverless,\n", + " endpoint_api_key=\"my-api-key\",\n", + " content_formatter=LlamaChatContentFormatter,\n", + " model_kwargs={\"temperature\": 0.8},\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Parameters can also be passed during invocation:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "response = chat.invoke(\n", + " [HumanMessage(content=\"Will the Collatz conjecture ever be solved?\")],\n", + " max_tokens=512,\n", ")\n", "response" ] diff --git a/docs/docs/integrations/chat/baichuan.ipynb b/docs/docs/integrations/chat/baichuan.ipynb index 2724727ad75b1..14f2d0d4c987d 100644 --- a/docs/docs/integrations/chat/baichuan.ipynb +++ b/docs/docs/integrations/chat/baichuan.ipynb @@ -13,7 +13,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# ChatBaichuan\n", + "# Chat with Baichuan-192K\n", "\n", "Baichuan chat models API by Baichuan Intelligent Technology. For more information, see [https://platform.baichuan-ai.com/docs/api](https://platform.baichuan-ai.com/docs/api)" ] @@ -44,19 +44,16 @@ }, "outputs": [], "source": [ - "chat = ChatBaichuan(\n", - " baichuan_api_key=\"YOUR_API_KEY\", baichuan_secret_key=\"YOUR_SECRET_KEY\"\n", - ")" + "chat = ChatBaichuan(baichuan_api_key=\"YOUR_API_KEY\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "or you can set `api_key` and `secret_key` in your environment variables\n", + "or you can set `api_key` in your environment variables\n", "```bash\n", "export BAICHUAN_API_KEY=YOUR_API_KEY\n", - "export BAICHUAN_SECRET_KEY=YOUR_SECRET_KEY\n", "```" ] }, @@ -91,7 +88,7 @@ "collapsed": false }, "source": [ - "## For ChatBaichuan with Streaming" + "## Chat with Baichuan-192K with Streaming" ] }, { @@ -108,7 +105,6 @@ "source": [ "chat = ChatBaichuan(\n", " baichuan_api_key=\"YOUR_API_KEY\",\n", - " baichuan_secret_key=\"YOUR_SECRET_KEY\",\n", " streaming=True,\n", ")" ] diff --git a/docs/docs/integrations/chat/baidu_qianfan_endpoint.ipynb b/docs/docs/integrations/chat/baidu_qianfan_endpoint.ipynb index 43a1336c1b99a..34b524d8dee79 100644 --- a/docs/docs/integrations/chat/baidu_qianfan_endpoint.ipynb +++ b/docs/docs/integrations/chat/baidu_qianfan_endpoint.ipynb @@ -53,9 +53,16 @@ "- AquilaChat-7B" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Set up" + ] + }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -65,83 +72,105 @@ "from langchain_community.chat_models import QianfanChatEndpoint\n", "from langchain_core.language_models.chat_models import HumanMessage\n", "\n", - "os.environ[\"QIANFAN_AK\"] = \"your_ak\"\n", - "os.environ[\"QIANFAN_SK\"] = \"your_sk\"\n", - "\n", - "chat = QianfanChatEndpoint(\n", - " streaming=True,\n", - ")\n", - "res = chat([HumanMessage(content=\"write a funny joke\")])" + "os.environ[\"QIANFAN_AK\"] = \"Your_api_key\"\n", + "os.environ[\"QIANFAN_SK\"] = \"You_secret_Key\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Usage" ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 2, "metadata": {}, "outputs": [ { - "name": "stderr", - "output_type": "stream", - "text": [ - "[INFO] [09-15 20:00:36] logging.py:55 [t:139698882193216]: requesting llm api endpoint: /chat/eb-instant\n", - "[INFO] [09-15 20:00:37] logging.py:55 [t:139698882193216]: async requesting llm api endpoint: /chat/eb-instant\n" - ] - }, + "data": { + "text/plain": [ + "AIMessage(content='您好!请问您需要什么帮助?我将尽力回答您的问题。')" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "chat = QianfanChatEndpoint(streaming=True)\n", + "messages = [HumanMessage(content=\"Hello\")]\n", + "chat.invoke(messages)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "chat resp: content='您好,您似乎输入' additional_kwargs={} example=False\n", - "chat resp: content='了一个话题标签,请问需要我帮您找到什么资料或者帮助您解答什么问题吗?' additional_kwargs={} example=False\n", - "chat resp: content='' additional_kwargs={} example=False\n" - ] - }, + "data": { + "text/plain": [ + "AIMessage(content='您好!有什么我可以帮助您的吗?')" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "await chat.ainvoke(messages)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ { - "name": "stderr", - "output_type": "stream", - "text": [ - "[INFO] [09-15 20:00:39] logging.py:55 [t:139698882193216]: async requesting llm api endpoint: /chat/eb-instant\n" - ] - }, + "data": { + "text/plain": [ + "[AIMessage(content='您好!有什么我可以帮助您的吗?')]" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "chat.batch([messages])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Streaming" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "generations=[[ChatGeneration(text=\"The sea is a vast expanse of water that covers much of the Earth's surface. It is a source of travel, trade, and entertainment, and is also a place of scientific exploration and marine conservation. The sea is an important part of our world, and we should cherish and protect it.\", generation_info={'finish_reason': 'finished'}, message=AIMessage(content=\"The sea is a vast expanse of water that covers much of the Earth's surface. It is a source of travel, trade, and entertainment, and is also a place of scientific exploration and marine conservation. The sea is an important part of our world, and we should cherish and protect it.\", additional_kwargs={}, example=False))]] llm_output={} run=[RunInfo(run_id=UUID('d48160a6-5960-4c1d-8a0e-90e6b51a209b'))]\n", - "astream content='The sea is a vast' additional_kwargs={} example=False\n", - "astream content=' expanse of water, a place of mystery and adventure. It is the source of many cultures and civilizations, and a center of trade and exploration. The sea is also a source of life and beauty, with its unique marine life and diverse' additional_kwargs={} example=False\n", - "astream content=' coral reefs. Whether you are swimming, diving, or just watching the sea, it is a place that captivates the imagination and transforms the spirit.' additional_kwargs={} example=False\n" + "您好!有什么我可以帮助您的吗?\n" ] } ], "source": [ - "from langchain.schema import HumanMessage\n", - "from langchain_community.chat_models import QianfanChatEndpoint\n", - "\n", - "chatLLM = QianfanChatEndpoint()\n", - "res = chatLLM.stream([HumanMessage(content=\"hi\")], streaming=True)\n", - "for r in res:\n", - " print(\"chat resp:\", r)\n", - "\n", - "\n", - "async def run_aio_generate():\n", - " resp = await chatLLM.agenerate(\n", - " messages=[[HumanMessage(content=\"write a 20 words sentence about sea.\")]]\n", - " )\n", - " print(resp)\n", - "\n", - "\n", - "await run_aio_generate()\n", - "\n", - "\n", - "async def run_aio_stream():\n", - " async for res in chatLLM.astream(\n", - " [HumanMessage(content=\"write a 20 words sentence about sea.\")]\n", - " ):\n", - " print(\"astream\", res)\n", - "\n", - "\n", - "await run_aio_stream()" + "try:\n", + " for chunk in chat.stream(messages):\n", + " print(chunk.content, end=\"\", flush=True)\n", + "except TypeError as e:\n", + " print(\"\")" ] }, { @@ -151,39 +180,36 @@ "source": [ "## Use different models in Qianfan\n", "\n", - "In the case you want to deploy your own model based on Ernie Bot or third-party open-source model, you could follow these steps:\n", + "The default model is ERNIE-Bot-turbo, in the case you want to deploy your own model based on Ernie Bot or third-party open-source model, you could follow these steps:\n", "\n", - "- 1. (Optional, if the model are included in the default models, skip it)Deploy your model in Qianfan Console, get your own customized deploy endpoint.\n", - "- 2. Set up the field called `endpoint` in the initialization:" + "1. (Optional, if the model are included in the default models, skip it) Deploy your model in Qianfan Console, get your own customized deploy endpoint.\n", + "2. Set up the field called `endpoint` in the initialization:" ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 6, "metadata": {}, "outputs": [ { - "name": "stderr", - "output_type": "stream", - "text": [ - "[INFO] [09-15 20:00:50] logging.py:55 [t:139698882193216]: requesting llm api endpoint: /chat/bloomz_7b1\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "content='你好!很高兴见到你。' additional_kwargs={} example=False\n" - ] + "data": { + "text/plain": [ + "AIMessage(content='Hello,可以回答问题了,我会竭尽全力为您解答,请问有什么问题吗?')" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "chatBloom = QianfanChatEndpoint(\n", + "chatBot = QianfanChatEndpoint(\n", " streaming=True,\n", - " model=\"BLOOMZ-7B\",\n", + " model=\"ERNIE-Bot\",\n", ")\n", - "res = chatBloom([HumanMessage(content=\"hi\")])\n", - "print(res)" + "\n", + "messages = [HumanMessage(content=\"Hello\")]\n", + "chatBot.invoke(messages)" ] }, { @@ -202,35 +228,25 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 7, "metadata": {}, "outputs": [ { - "name": "stderr", - "output_type": "stream", - "text": [ - "[INFO] [09-15 20:00:57] logging.py:55 [t:139698882193216]: requesting llm api endpoint: /chat/eb-instant\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "content='您好,您似乎输入' additional_kwargs={} example=False\n", - "content='了一个文本字符串,但并没有给出具体的问题或场景。' additional_kwargs={} example=False\n", - "content='如果您能提供更多信息,我可以更好地回答您的问题。' additional_kwargs={} example=False\n", - "content='' additional_kwargs={} example=False\n" - ] + "data": { + "text/plain": [ + "AIMessage(content='您好!有什么我可以帮助您的吗?')" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "res = chat.stream(\n", - " [HumanMessage(content=\"hi\")],\n", + "chat.invoke(\n", + " [HumanMessage(content=\"Hello\")],\n", " **{\"top_p\": 0.4, \"temperature\": 0.1, \"penalty_score\": 1},\n", - ")\n", - "\n", - "for r in res:\n", - " print(r)" + ")" ] } ], @@ -250,7 +266,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.5" + "version": "3.9.18" }, "vscode": { "interpreter": { diff --git a/docs/docs/integrations/chat/deepinfra.ipynb b/docs/docs/integrations/chat/deepinfra.ipynb new file mode 100644 index 0000000000000..0f88097a20ac7 --- /dev/null +++ b/docs/docs/integrations/chat/deepinfra.ipynb @@ -0,0 +1,224 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "id": "bf733a38-db84-4363-89e2-de6735c37230", + "metadata": {}, + "source": [ + "# DeepInfra\n", + "\n", + "[DeepInfra](https://deepinfra.com/?utm_source=langchain) is a serverless inference as a service that provides access to a [variety of LLMs](https://deepinfra.com/models?utm_source=langchain) and [embeddings models](https://deepinfra.com/models?type=embeddings&utm_source=langchain). This notebook goes over how to use LangChain with DeepInfra for chat models." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Set the Environment API Key\n", + "Make sure to get your API key from DeepInfra. You have to [Login](https://deepinfra.com/login?from=%2Fdash) and get a new token.\n", + "\n", + "You are given a 1 hour free of serverless GPU compute to test different models. (see [here](https://github.com/deepinfra/deepctl#deepctl))\n", + "You can print your token with `deepctl auth token`" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " ········\n" + ] + } + ], + "source": [ + "# get a new token: https://deepinfra.com/login?from=%2Fdash\n", + "\n", + "from getpass import getpass\n", + "\n", + "DEEPINFRA_API_TOKEN = getpass()" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "# or pass deepinfra_api_token parameter to the ChatDeepInfra constructor\n", + "os.environ[\"DEEPINFRA_API_TOKEN\"] = DEEPINFRA_API_TOKEN" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "d4a7c55d-b235-4ca4-a579-c90cc9570da9", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "from langchain.chat_models import ChatDeepInfra\n", + "from langchain.schema import HumanMessage" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "70cf04e8-423a-4ff6-8b09-f11fb711c817", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "chat = ChatDeepInfra(model=\"meta-llama/Llama-2-7b-chat-hf\")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "8199ef8f-eb8b-4253-9ea0-6c24a013ca4c", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "AIMessage(content=\" J'aime la programmation.\", additional_kwargs={}, example=False)" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "messages = [\n", + " HumanMessage(\n", + " content=\"Translate this sentence from English to French. I love programming.\"\n", + " )\n", + "]\n", + "chat(messages)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "c361ab1e-8c0c-4206-9e3c-9d1424a12b9c", + "metadata": {}, + "source": [ + "## `ChatDeepInfra` also supports async and streaming functionality:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "93a21c5c-6ef9-4688-be60-b2e1f94842fb", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "c5fac0e9-05a4-4fc1-a3b3-e5bbb24b971b", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "LLMResult(generations=[[ChatGeneration(text=\" J'aime programmer.\", generation_info=None, message=AIMessage(content=\" J'aime programmer.\", additional_kwargs={}, example=False))]], llm_output={}, run=[RunInfo(run_id=UUID('8cc8fb68-1c35-439c-96a0-695036a93652'))])" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "await chat.agenerate([messages])" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "025be980-e50d-4a68-93dc-c9c7b500ce34", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " J'aime la programmation." + ] + }, + { + "data": { + "text/plain": [ + "AIMessage(content=\" J'aime la programmation.\", additional_kwargs={}, example=False)" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "chat = ChatDeepInfra(\n", + " streaming=True,\n", + " verbose=True,\n", + " callbacks=[StreamingStdOutCallbackHandler()],\n", + ")\n", + "chat(messages)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c253883f", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "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.9.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/docs/integrations/chat/ernie.ipynb b/docs/docs/integrations/chat/ernie.ipynb index 17c5fb650cc9e..fd467c22276e4 100644 --- a/docs/docs/integrations/chat/ernie.ipynb +++ b/docs/docs/integrations/chat/ernie.ipynb @@ -16,29 +16,58 @@ "# ErnieBotChat\n", "\n", "[ERNIE-Bot](https://cloud.baidu.com/doc/WENXINWORKSHOP/s/jlil56u11) is a large language model developed by Baidu, covering a huge amount of Chinese data.\n", - "This notebook covers how to get started with ErnieBot chat models.\n", + "This notebook covers how to get started with ErnieBot chat models." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Deprecated Warning**\n", + "\n", + "We recommend users using `langchain_community.chat_models.ErnieBotChat` \n", + "to use `langchain_community.chat_models.QianfanChatEndpoint` instead.\n", + "\n", + "documentation for `QianfanChatEndpoint` is [here](./baidu_qianfan_endpoint).\n", + "\n", + "they are 4 why we recommend users to use `QianfanChatEndpoint`:\n", "\n", - "**Note:** We recommend users using this class to switch to [Baidu Qianfan](./baidu_qianfan_endpoint). they are 3 why we recommend users to use `QianfanChatEndpoint`:\n", "1. `QianfanChatEndpoint` support more LLM in the Qianfan platform.\n", "2. `QianfanChatEndpoint` support streaming mode.\n", "3. `QianfanChatEndpoint` support function calling usgage.\n", - "\n", + "4. `ErnieBotChat` is lack of maintenance and deprecated." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ "Some tips for migration:\n", + "\n", "- change `ernie_client_id` to `qianfan_ak`, also change `ernie_client_secret` to `qianfan_sk`.\n", - "- install `qianfan` package. \n", - " ```\n", - " pip install qianfan\n", - " ```" + "- install `qianfan` package. like `pip install qianfan`\n", + "- change `ErnieBotChat` to `QianfanChatEndpoint`." ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "from langchain.schema import HumanMessage\n", - "from langchain_community.chat_models import ErnieBotChat" + "from langchain_community.chat_models.baidu_qianfan_endpoint import QianfanChatEndpoint\n", + "\n", + "chat = QianfanChatEndpoint(\n", + " qianfan_ak=\"your qianfan ak\",\n", + " qianfan_sk=\"your qianfan sk\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Usage" ] }, { @@ -47,6 +76,9 @@ "metadata": {}, "outputs": [], "source": [ + "from langchain.schema import HumanMessage\n", + "from langchain_community.chat_models import ErnieBotChat\n", + "\n", "chat = ErnieBotChat(\n", " ernie_client_id=\"YOUR_CLIENT_ID\", ernie_client_secret=\"YOUR_CLIENT_SECRET\"\n", ")" diff --git a/docs/docs/integrations/chat/google_generative_ai.ipynb b/docs/docs/integrations/chat/google_generative_ai.ipynb index fe718d7a7a728..c8544163a4f4c 100644 --- a/docs/docs/integrations/chat/google_generative_ai.ipynb +++ b/docs/docs/integrations/chat/google_generative_ai.ipynb @@ -320,11 +320,26 @@ "4. Message may be blocked if they violate the safety checks of the LLM. In this case, the model will return an empty response." ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "75fdfad6", + "metadata": {}, + "outputs": [], + "source": [] + }, { "cell_type": "markdown", "id": "92b5aca5", "metadata": {}, - "source": [] + "source": [ + "## Additional Configuraation\n", + "\n", + "You can pass the following parameters to ChatGoogleGenerativeAI in order to customize the SDK's behavior:\n", + "\n", + "- `client_options`: [Client Options](https://googleapis.dev/python/google-api-core/latest/client_options.html#module-google.api_core.client_options) to pass to the Google API Client, such as a custom `client_options[\"api_endpoint\"]`\n", + "- `transport`: The transport method to use, such as `rest`, `grpc`, or `grpc_asyncio`." + ] } ], "metadata": { diff --git a/docs/docs/integrations/chat/google_vertex_ai_palm.ipynb b/docs/docs/integrations/chat/google_vertex_ai_palm.ipynb index 551e1c8df08bf..008746f747946 100644 --- a/docs/docs/integrations/chat/google_vertex_ai_palm.ipynb +++ b/docs/docs/integrations/chat/google_vertex_ai_palm.ipynb @@ -11,7 +11,6 @@ ] }, { - "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ @@ -35,29 +34,18 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": { "tags": [] }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m A new release of pip is available: \u001b[0m\u001b[31;49m23.2\u001b[0m\u001b[39;49m -> \u001b[0m\u001b[32;49m23.3.2\u001b[0m\n", - "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m To update, run: \u001b[0m\u001b[32;49mpip install --upgrade pip\u001b[0m\n", - "Note: you may need to restart the kernel to use updated packages.\n" - ] - } - ], + "outputs": [], "source": [ "%pip install --upgrade --quiet langchain-google-vertexai" ] }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -67,7 +55,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -76,7 +64,7 @@ "AIMessage(content=\" J'aime la programmation.\")" ] }, - "execution_count": 2, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } @@ -92,6 +80,40 @@ "chain.invoke({})" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Gemini doesn't support SystemMessage at the moment, but it can be added to the first human message in the row. If you want such behavior, just set the `convert_system_message_to_human` to `True`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "AIMessage(content=\"J'aime la programmation.\")" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "system = \"You are a helpful assistant who translate English to French\"\n", + "human = \"Translate this sentence from English to French. I love programming.\"\n", + "prompt = ChatPromptTemplate.from_messages([(\"system\", system), (\"human\", human)])\n", + "\n", + "chat = ChatVertexAI(model_name=\"gemini-pro\", convert_system_message_to_human=True)\n", + "\n", + "chain = prompt | chat\n", + "chain.invoke({})" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -101,7 +123,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 4, "metadata": {}, "outputs": [ { @@ -110,7 +132,7 @@ "AIMessage(content=' プログラミングが大好きです')" ] }, - "execution_count": 3, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } @@ -122,6 +144,8 @@ "human = \"{text}\"\n", "prompt = ChatPromptTemplate.from_messages([(\"system\", system), (\"human\", human)])\n", "\n", + "chat = ChatVertexAI()\n", + "\n", "chain = prompt | chat\n", "\n", "chain.invoke(\n", @@ -134,7 +158,6 @@ ] }, { - "attachments": {}, "cell_type": "markdown", "metadata": { "execution": { @@ -154,7 +177,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": { "tags": [] }, @@ -165,27 +188,51 @@ "text": [ " ```python\n", "def is_prime(n):\n", - " if n <= 1:\n", - " return False\n", - " for i in range(2, n):\n", - " if n % i == 0:\n", - " return False\n", - " return True\n", + " \"\"\"\n", + " Check if a number is prime.\n", + "\n", + " Args:\n", + " n: The number to check.\n", + "\n", + " Returns:\n", + " True if n is prime, False otherwise.\n", + " \"\"\"\n", + "\n", + " # If n is 1, it is not prime.\n", + " if n == 1:\n", + " return False\n", + "\n", + " # Iterate over all numbers from 2 to the square root of n.\n", + " for i in range(2, int(n ** 0.5) + 1):\n", + " # If n is divisible by any number from 2 to its square root, it is not prime.\n", + " if n % i == 0:\n", + " return False\n", + "\n", + " # If n is divisible by no number from 2 to its square root, it is prime.\n", + " return True\n", + "\n", "\n", "def find_prime_numbers(n):\n", - " prime_numbers = []\n", - " for i in range(2, n + 1):\n", - " if is_prime(i):\n", - " prime_numbers.append(i)\n", - " return prime_numbers\n", + " \"\"\"\n", + " Find all prime numbers up to a given number.\n", + "\n", + " Args:\n", + " n: The upper bound for the prime numbers to find.\n", + "\n", + " Returns:\n", + " A list of all prime numbers up to n.\n", + " \"\"\"\n", "\n", - "print(find_prime_numbers(100))\n", - "```\n", + " # Create a list of all numbers from 2 to n.\n", + " numbers = list(range(2, n + 1))\n", "\n", - "Output:\n", + " # Iterate over the list of numbers and remove any that are not prime.\n", + " for number in numbers:\n", + " if not is_prime(number):\n", + " numbers.remove(number)\n", "\n", - "```\n", - "[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97]\n", + " # Return the list of prime numbers.\n", + " return numbers\n", "```\n" ] } @@ -199,6 +246,143 @@ "print(message.content)" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Full generation info\n", + "\n", + "We can use the `generate` method to get back extra metadata like [safety attributes](https://cloud.google.com/vertex-ai/docs/generative-ai/learn/responsible-ai#safety_attribute_confidence_scoring) and not just chat completions\n", + "\n", + "Note that the `generation_info` will be different depending if you're using a gemini model or not.\n", + "\n", + "### Gemini model\n", + "\n", + "`generation_info` will include:\n", + "\n", + "- `is_blocked`: whether generation was blocked or not\n", + "- `safety_ratings`: safety ratings' categories and probability labels" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'is_blocked': False,\n", + " 'safety_ratings': [{'category': 'HARM_CATEGORY_HARASSMENT',\n", + " 'probability_label': 'NEGLIGIBLE'},\n", + " {'category': 'HARM_CATEGORY_HATE_SPEECH',\n", + " 'probability_label': 'NEGLIGIBLE'},\n", + " {'category': 'HARM_CATEGORY_SEXUALLY_EXPLICIT',\n", + " 'probability_label': 'NEGLIGIBLE'},\n", + " {'category': 'HARM_CATEGORY_DANGEROUS_CONTENT',\n", + " 'probability_label': 'NEGLIGIBLE'}]}\n" + ] + } + ], + "source": [ + "from pprint import pprint\n", + "\n", + "from langchain_core.messages import HumanMessage\n", + "from langchain_google_vertexai import ChatVertexAI, HarmBlockThreshold, HarmCategory\n", + "\n", + "human = \"Translate this sentence from English to French. I love programming.\"\n", + "messages = [HumanMessage(content=human)]\n", + "\n", + "\n", + "chat = ChatVertexAI(\n", + " model_name=\"gemini-pro\",\n", + " safety_settings={\n", + " HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE\n", + " },\n", + ")\n", + "\n", + "result = chat.generate([messages])\n", + "pprint(result.generations[0][0].generation_info)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Non-gemini model\n", + "\n", + "`generation_info` will include:\n", + "\n", + "- `is_blocked`: whether generation was blocked or not\n", + "- `safety_attributes`: a dictionary mapping safety attributes to their scores" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'is_blocked': False,\n", + " 'safety_attributes': {'Derogatory': 0.1,\n", + " 'Finance': 0.3,\n", + " 'Insult': 0.1,\n", + " 'Sexual': 0.1}}\n" + ] + } + ], + "source": [ + "chat = ChatVertexAI() # default is `chat-bison`\n", + "\n", + "result = chat.generate([messages])\n", + "pprint(result.generations[0][0].generation_info)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Function Calling with Gemini\n", + "\n", + "We can call Gemini models with tools." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "MyModel(name='Erick', age=27)" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from langchain.pydantic_v1 import BaseModel\n", + "from langchain_google_vertexai import create_structured_runnable\n", + "\n", + "llm = ChatVertexAI(model_name=\"gemini-pro\")\n", + "\n", + "\n", + "class MyModel(BaseModel):\n", + " name: str\n", + " age: int\n", + "\n", + "\n", + "chain = create_structured_runnable(MyModel, llm)\n", + "chain.invoke(\"My name is Erick and I'm 27 years old\")" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -210,7 +394,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -224,7 +408,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -268,7 +452,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -309,8 +493,14 @@ } ], "metadata": { + "environment": { + "kernel": "python3", + "name": "common-cpu.m108", + "type": "gcloud", + "uri": "gcr.io/deeplearning-platform-release/base-cpu:m108" + }, "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "Python 3", "language": "python", "name": "python3" }, @@ -324,7 +514,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.4" + "version": "3.10.10" }, "vscode": { "interpreter": { diff --git a/docs/docs/integrations/chat/konko.ipynb b/docs/docs/integrations/chat/konko.ipynb index 95c826a093752..173fd34691a3e 100644 --- a/docs/docs/integrations/chat/konko.ipynb +++ b/docs/docs/integrations/chat/konko.ipynb @@ -21,17 +21,31 @@ "\n", "1. Select the right LLM(s) for their application\n", "2. Prototype with various open-source and proprietary LLMs\n", - "3. Move to production in-line with their security, privacy, throughput, latency SLAs without infrastructure set-up or administration using Konko AI's SOC 2 compliant infrastructure\n", + "3. Access Fine Tuning for open-source LLMs to get industry-leading performance at a fraction of the cost\n", + "4. Setup low-cost production APIs according to security, privacy, throughput, latency SLAs without infrastructure set-up or administration using Konko AI's SOC 2 compliant, multi-cloud infrastructure\n", "\n", + "### Steps to Access Models\n", + "1. **Explore Available Models:** Start by browsing through the [available models](https://docs.konko.ai/docs/list-of-models) on Konko. Each model caters to different use cases and capabilities.\n", "\n", - "This example goes over how to use LangChain to interact with `Konko` [models](https://docs.konko.ai/docs/overview)" + "2. **Identify Suitable Endpoints:** Determine which [endpoint](https://docs.konko.ai/docs/list-of-models#list-of-available-models) (ChatCompletion or Completion) supports your selected model.\n", + "\n", + "3. **Selecting a Model:** [Choose a model](https://docs.konko.ai/docs/list-of-models#list-of-available-models) based on its metadata and how well it fits your use case.\n", + "\n", + "4. **Prompting Guidelines:** Once a model is selected, refer to the [prompting guidelines](https://docs.konko.ai/docs/prompting) to effectively communicate with it.\n", + "\n", + "5. **Using the API:** Finally, use the appropriate Konko [API endpoint](https://docs.konko.ai/docs/quickstart-for-completion-and-chat-completion-endpoint) to call the model and receive responses.\n", + "\n", + "To run this notebook, you'll need Konko API key. You can create one by signing up on [Konko](https://www.konko.ai/).\n", + "\n", + "This example goes over how to use LangChain to interact with `Konko` ChatCompletion [models](https://docs.konko.ai/docs/list-of-models#konko-hosted-models-for-chatcompletion)\n", + "\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "To run this notebook, you'll need Konko API key. You can request it by messaging support@konko.ai." + "To run this notebook, you'll need Konko API key. You can create one by signing up on [Konko](https://www.konko.ai/)." ] }, { @@ -84,36 +98,34 @@ "source": [ "## Calling a model\n", "\n", - "Find a model on the [Konko overview page](https://docs.konko.ai/docs/overview)\n", - "\n", - "For example, for this [LLama 2 model](https://docs.konko.ai/docs/meta-llama-2-13b-chat). The model id would be: `\"meta-llama/Llama-2-13b-chat-hf\"`\n", + "Find a model on the [Konko overview page](https://docs.konko.ai/v0.5.0/docs/list-of-models)\n", "\n", - "Another way to find the list of models running on the Konko instance is through this [endpoint](https://docs.konko.ai/reference/listmodels).\n", + "Another way to find the list of models running on the Konko instance is through this [endpoint](https://docs.konko.ai/reference/get-models).\n", "\n", "From here, we can initialize our model:\n" ] }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ - "chat = ChatKonko(max_tokens=400, model=\"meta-llama/Llama-2-13b-chat-hf\")" + "chat = ChatKonko(max_tokens=400, model=\"meta-llama/llama-2-13b-chat\")" ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "AIMessage(content=\" Sure, I'd be happy to explain the Big Bang Theory briefly!\\n\\nThe Big Bang Theory is the leading explanation for the origin and evolution of the universe, based on a vast amount of observational evidence from many fields of science. In essence, the theory posits that the universe began as an infinitely hot and dense point, known as a singularity, around 13.8 billion years ago. This singularity expanded rapidly, and as it did, it cooled and formed subatomic particles, which eventually coalesced into the first atoms, and later into the stars and galaxies we see today.\\n\\nThe theory gets its name from the idea that the universe began in a state of incredibly high energy and temperature, and has been expanding and cooling ever since. This expansion is thought to have been driven by a mysterious force known as dark energy, which is thought to be responsible for the accelerating expansion of the universe.\\n\\nOne of the key predictions of the Big Bang Theory is that the universe should be homogeneous and isotropic on large scales, meaning that it should look the same in all directions and have the same properties everywhere. This prediction has been confirmed by a wealth of observational evidence, including the cosmic microwave background radiation, which is thought to be a remnant of the early universe.\\n\\nOverall, the Big Bang Theory is a well-established and widely accepted explanation for the origins of the universe, and it has been supported by a vast amount of observational evidence from many fields of science.\", additional_kwargs={}, example=False)" + "AIMessage(content=\" Sure thing! The Big Bang Theory is a scientific theory that explains the origins of the universe. In short, it suggests that the universe began as an infinitely hot and dense point around 13.8 billion years ago and expanded rapidly. This expansion continues to this day, and it's what makes the universe look the way it does.\\n\\nHere's a brief overview of the key points:\\n\\n1. The universe started as a singularity, a point of infinite density and temperature.\\n2. The singularity expanded rapidly, causing the universe to cool and expand.\\n3. As the universe expanded, particles began to form, including protons, neutrons, and electrons.\\n4. These particles eventually came together to form atoms, and later, stars and galaxies.\\n5. The universe is still expanding today, and the rate of this expansion is accelerating.\\n\\nThat's the Big Bang Theory in a nutshell! It's a pretty mind-blowing idea when you think about it, and it's supported by a lot of scientific evidence. Do you have any other questions about it?\")" ] }, - "execution_count": 7, + "execution_count": 3, "metadata": {}, "output_type": "execute_result" } @@ -125,13 +137,6 @@ "]\n", "chat(messages)" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/docs/docs/integrations/chat/ollama.ipynb b/docs/docs/integrations/chat/ollama.ipynb index 054666d807671..d0df5b4b99d8a 100644 --- a/docs/docs/integrations/chat/ollama.ipynb +++ b/docs/docs/integrations/chat/ollama.ipynb @@ -15,105 +15,231 @@ "source": [ "# ChatOllama\n", "\n", - "[Ollama](https://ollama.ai/) allows you to run open-source large language models, such as LLaMA2, locally.\n", + "[Ollama](https://ollama.ai/) allows you to run open-source large language models, such as Llama 2, locally.\n", "\n", "Ollama bundles model weights, configuration, and data into a single package, defined by a Modelfile. \n", "\n", "It optimizes setup and configuration details, including GPU usage.\n", "\n", - "For a complete list of supported models and model variants, see the [Ollama model library](https://ollama.ai/library).\n", + "For a complete list of supported models and model variants, see the [Ollama model library](https://github.com/jmorganca/ollama#model-library).\n", "\n", "## Setup\n", "\n", "First, follow [these instructions](https://github.com/jmorganca/ollama) to set up and run a local Ollama instance:\n", "\n", - "* [Download](https://ollama.ai/download)\n", - "* Fetch a model via `ollama pull `\n", - "* e.g., for `Llama-7b`: `ollama pull llama2`\n", - "* This will download the most basic version of the model (e.g., minimum # parameters and 4-bit quantization)\n", - "* On Mac, it will download to:\n", + "* [Download](https://ollama.ai/download) and install Ollama onto the available supported platforms (including Windows Subsystem for Linux)\n", + "* Fetch available LLM model via `ollama pull `\n", + " * View a list of available models via the [model library](https://ollama.ai/library)\n", + " * e.g., for `Llama-7b`: `ollama pull llama2`\n", + "* This will download the default tagged version of the model. Typically, the default points to the latest, smallest sized-parameter model.\n", "\n", - "`~/.ollama/models/manifests/registry.ollama.ai/library//latest`\n", + "> On Mac, the models will be download to `~/.ollama/models`\n", + "> \n", + "> On Linux (or WSL), the models will be stored at `/usr/share/ollama/.ollama/models`\n", "\n", - "* And we can specify a particular version, e.g., for `ollama pull vicuna:13b-v1.5-16k-q4_0`\n", - "* The file is here with the model version in place of `latest`\n", + "* Specify the exact version of the model of interest as such `ollama pull vicuna:13b-v1.5-16k-q4_0` (View the [various tags for the `Vicuna`](https://ollama.ai/library/vicuna/tags) model in this instance)\n", + "* To view all pulled models, use `ollama list`\n", + "* To chat directly with a model from the command line, use `ollama run `\n", + "* View the [Ollama documentation](https://github.com/jmorganca/ollama) for more commands. Run `ollama help` in the terminal to see available commands too.\n", "\n", - "`~/.ollama/models/manifests/registry.ollama.ai/library/vicuna/13b-v1.5-16k-q4_0`\n", + "## Usage\n", "\n", - "You can easily access models in a few ways:\n", + "You can see a full list of supported parameters on the [API reference page](https://api.python.langchain.com/en/latest/llms/langchain.llms.ollama.Ollama.html).\n", "\n", - "1/ if the app is running:\n", + "If you are using a LLaMA `chat` model (e.g., `ollama pull llama2:7b-chat`) then you can use the `ChatOllama` interface.\n", "\n", - "* All of your local models are automatically served on `localhost:11434`\n", - "* Select your model when setting `llm = Ollama(..., model=\":\")`\n", - "* If you set `llm = Ollama(..., model=\"` to start interacting via the command line directly\n", "\n", + "### via an API\n", "\n", - "## Usage\n", + "Send an `application/json` request to the API endpoint of Ollama to interact.\n", "\n", - "You can see a full list of supported parameters on the [API reference page](https://api.python.langchain.com/en/latest/llms/langchain.llms.ollama.Ollama.html).\n", + "```bash\n", + "curl http://localhost:11434/api/generate -d '{\n", + " \"model\": \"llama2\",\n", + " \"prompt\":\"Why is the sky blue?\"\n", + "}'\n", + "```\n", "\n", - "If you are using a LLaMA `chat` model (e.g., `ollama pull llama2:7b-chat`) then you can use the `ChatOllama` interface.\n", + "See the Ollama [API documentation](https://github.com/jmorganca/ollama/blob/main/docs/api.md) for all endpoints.\n", "\n", - "This includes [special tokens](https://huggingface.co/blog/llama2#how-to-prompt-llama-2) for system message and user input." + "#### via LangChain\n", + "\n", + "See a typical basic example of using Ollama via the `ChatOllama` chat model in your LangChain application." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " Sure, here's a fun space-themed joke for you:\n", + "\n", + "Why don't astronauts like broccoli? \n", + "Because it has too many \"crisps\" in it!\n", + "\n" + ] + } + ], "source": [ - "from langchain.callbacks.manager import CallbackManager\n", - "from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler\n", + "# LangChain supports many other chat models. Here, we're using Ollama\n", "from langchain_community.chat_models import ChatOllama\n", - "\n", - "chat_model = ChatOllama(\n", - " model=\"llama2:7b-chat\",\n", - ")" + "from langchain_core.output_parsers import StrOutputParser\n", + "from langchain_core.prompts import ChatPromptTemplate\n", + "\n", + "# supports many more optional parameters. Hover on your `ChatOllama(...)`\n", + "# class to view the latest available supported parameters\n", + "llm = ChatOllama(model=\"llama2\")\n", + "prompt = ChatPromptTemplate.from_template(\"Tell me a short joke about {topic}\")\n", + "\n", + "# using LangChain Expressive Language chain syntax\n", + "# learn more about the LCEL on\n", + "# https://python.langchain.com/docs/expression_language/why\n", + "chain = prompt | llm | StrOutputParser()\n", + "\n", + "# for brevity, response is printed in terminal\n", + "# You can use LangServe to deploy your application for\n", + "# production\n", + "print(chain.invoke({\"topic\": \"Space travel\"}))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Optionally, pass `StreamingStdOutCallbackHandler` to stream tokens:\n", + "LCEL chains, out of the box, provide extra functionalities, such as streaming of responses, and async support" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " Sure\n", + ",\n", + " here\n", + "'s\n", + " a\n", + " joke\n", + ":\n", + " Why\n", + " did\n", + " the\n", + " astronaut\n", + " break\n", + " up\n", + " with\n", + " his\n", + " girlfriend\n", + "?\n", + " Because\n", + " he\n", + " needed\n", + " more\n", + " space\n", + " to\n", + " explore\n", + ".\n", + "\n", + "\n", + "\n" + ] + } + ], + "source": [ + "topic = {\"topic\": \"Space travel\"}\n", "\n", - "```\n", - "chat_model = ChatOllama(\n", - " model=\"llama2:7b-chat\",\n", - " callback_manager=CallbackManager([StreamingStdOutCallbackHandler()]),\n", - ")\n", - "```" + "for chunks in chain.stream(topic):\n", + " print(chunks)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For streaming async support, here's an example - all possible via the single chain created above." ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 13, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "AIMessage(content='\\nArtificial intelligence (AI) has a rich and diverse history that spans several decades. Here is a brief overview of the major milestones and events in the development of AI:\\n\\n1. 1950s: The Dartmouth Conference: The field of AI was officially launched at a conference held at Dartmouth College in 1956. Attendees included computer scientists, mathematicians, and cognitive scientists who were interested in exploring the possibilities of creating machines that could simulate human intelligence.\\n2. 1951: The Turing Test: British mathematician Alan Turing proposed a test to measure a machine\\'s ability to exhibit intelligent behavior equivalent to, or indistinguishable from, that of a human. The Turing Test has since become a benchmark for measuring the success of AI systems.\\n3. 1956: The First AI Program: Computer scientist John McCarthy created the first AI program, called the Logical Theorist, which was designed to reason and solve problems using logical deduction.\\n4. 1960s: Rule-Based Expert Systems: The development of rule-based expert systems, which used a set of rules to reason and make decisions, marked a significant milestone in the history of AI. These systems were widely used in industries such as banking, healthcare, and transportation.\\n5. 1970s: Machine Learning: Machine learning, which enables machines to learn from data without being explicitly programmed, emerged as a major area of research in AI. This led to the development of algorithms such as decision trees and neural networks.\\n6. 1980s: Expert Systems: The development of expert systems, which were designed to mimic the decision-making abilities of human experts, reached its peak in the 1980s. These systems were widely used in industries such as banking and healthcare.\\n7. 1990s: AI Winter: Despite the progress that had been made in AI research, the field experienced a decline in funding and interest in the 1990s, which became known as the \"AI winter.\"\\n8. 2000s: Machine Learning Resurgence: The resurgence of machine learning, driven by advances in computational power and data storage, led to a new wave of AI research and applications.\\n9. 2010s: Deep Learning: The development of deep learning algorithms, which are capable of learning complex patterns in large datasets, marked a significant breakthrough in AI research. These algorithms have been used in applications such as image and speech recognition, natural language processing, and autonomous vehicles.\\n10. Present Day: AI is now being applied to a wide range of industries and domains, including healthcare, finance, transportation, and education. The field is continuing to evolve, with new technologies and applications emerging all the time.\\n\\nOverall, the history of AI reflects a long-standing interest in creating machines that can simulate human intelligence. While the field has experienced periods of progress and setbacks, it continues to evolve and expand into new areas of research and application.')" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + " Sure\n", + ",\n", + " here\n", + "'s\n", + " a\n", + " little\n", + " one\n", + ":\n", + " Why\n", + " did\n", + " the\n", + " rocket\n", + " scientist\n", + " break\n", + " up\n", + " with\n", + " her\n", + " partner\n", + "?\n", + " Because\n", + " he\n", + " couldn\n", + "'t\n", + " handle\n", + " all\n", + " her\n", + " \"\n", + "space\n", + "y\n", + "\"\n", + " jokes\n", + ".\n", + "\n", + "\n", + "\n" + ] } ], "source": [ - "from langchain.schema import HumanMessage\n", + "topic = {\"topic\": \"Space travel\"}\n", "\n", - "messages = [HumanMessage(content=\"Tell me about the history of AI\")]\n", - "chat_model(messages)" + "async for chunks in chain.astream(topic):\n", + " print(chunks)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Take a look at the [LangChain Expressive Language (LCEL) Interface](https://python.langchain.com/docs/expression_language/interface) for the other available interfaces for use when a chain is created.\n", + "\n", + "## Building from source\n", + "\n", + "For up to date instructions on building from source, check the Ollama documentation on [Building from Source](https://github.com/jmorganca/ollama?tab=readme-ov-file#building)" ] }, { @@ -122,42 +248,32 @@ "source": [ "## Extraction\n", " \n", - "Update your version of Ollama and supply the [`format`](https://github.com/jmorganca/ollama/blob/main/docs/api.md#json-mode) flag.\n", - "\n", - "We can enforce the model to produce JSON.\n", + "Use the latest version of Ollama and supply the [`format`](https://github.com/jmorganca/ollama/blob/main/docs/api.md#json-mode) flag. The `format` flag will force the model to produce the response in JSON.\n", "\n", - "**Note:** You can also try out the experimental [OllamaFunctions](https://python.langchain.com/docs/integrations/chat/ollama_functions) wrapper for convenience." + "> **Note:** You can also try out the experimental [OllamaFunctions](https://python.langchain.com/docs/integrations/chat/ollama_functions) wrapper for convenience." ] }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 6, "metadata": {}, "outputs": [], "source": [ - "from langchain.callbacks.manager import CallbackManager\n", - "from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler\n", "from langchain_community.chat_models import ChatOllama\n", "\n", - "chat_model = ChatOllama(\n", - " model=\"llama2\",\n", - " format=\"json\",\n", - " callback_manager=CallbackManager([StreamingStdOutCallbackHandler()]),\n", - ")" + "llm = ChatOllama(model=\"llama2\", format=\"json\", temperature=0)" ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "{\"morning\": {\"sky\": \"pink\", \"sun\": \"rise\"}, \"daytime\": {\"sky\": \"blue\", \"sun\": \"high\"}, \"afternoon\": {\"sky\": \"gray\", \"sun\": \"peak\"}, \"evening\": {\"sky\": \"orange\", \"sun\": \"set\"}}\n", - " \t\n", - "\n" + "content='{\\n\"morning\": {\\n\"color\": \"light blue\"\\n},\\n\"noon\": {\\n\"color\": \"blue\"\\n},\\n\"afternoon\": {\\n\"color\": \"grayish-blue\"\\n},\\n\"evening\": {\\n\"color\": \"pinkish-orange\"\\n}\\n}'\n" ] } ], @@ -170,37 +286,27 @@ " )\n", "]\n", "\n", - "chat_model_response = chat_model(messages)" + "chat_model_response = llm.invoke(messages)\n", + "print(chat_model_response)" ] }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 53, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "{\n", - " \"name\": \"John\",\n", - " \"age\": 35,\n", - " \"fav_food\": \"pizza\"\n", - "}\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n" + "{\n", + "\"name\": \"John\",\n", + "\"age\": 35,\n", + "\"interests\": [\n", + "\"pizza\"\n", + "]\n", + "}\n" ] } ], @@ -208,6 +314,9 @@ "import json\n", "\n", "from langchain.schema import HumanMessage\n", + "from langchain_community.chat_models import ChatOllama\n", + "from langchain_core.output_parsers import StrOutputParser\n", + "from langchain_core.prompts import ChatPromptTemplate\n", "\n", "json_schema = {\n", " \"title\": \"Person\",\n", @@ -225,17 +334,24 @@ " \"required\": [\"name\", \"age\"],\n", "}\n", "\n", + "llm = ChatOllama(model=\"llama2\")\n", + "\n", "messages = [\n", " HumanMessage(\n", " content=\"Please tell me about a person using the following JSON schema:\"\n", " ),\n", - " HumanMessage(content=json.dumps(json_schema, indent=2)),\n", + " HumanMessage(content=\"{dumps}\"),\n", " HumanMessage(\n", " content=\"Now, considering the schema, tell me about a person named John who is 35 years old and loves pizza.\"\n", " ),\n", "]\n", "\n", - "chat_model_response = chat_model(messages)" + "prompt = ChatPromptTemplate.from_messages(messages)\n", + "dumps = json.dumps(json_schema, indent=2)\n", + "\n", + "chain = prompt | llm | StrOutputParser()\n", + "\n", + "print(chain.invoke({\"dumps\": dumps}))" ] }, { @@ -246,25 +362,32 @@ "\n", "Ollama has support for multi-modal LLMs, such as [bakllava](https://ollama.ai/library/bakllava) and [llava](https://ollama.ai/library/llava).\n", "\n", - "Browse the full set of versions for models with `tags`, such as [here](https://ollama.ai/library/llava/tags).\n", + "Browse the full set of versions for models with `tags`, such as [Llava](https://ollama.ai/library/llava/tags).\n", "\n", - "Download the desired LLM:\n", - "```\n", - "ollama pull bakllava\n", - "```\n", + "Download the desired LLM via `ollama pull bakllava`\n", + "\n", + "Be sure to update Ollama so that you have the most recent version to support multi-modal.\n", "\n", - "Be sure to update Ollama so that you have the most recent version to support multi-modal." + "Check out the typical example of how to use ChatOllama multi-modal support below:" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 18, "metadata": { "scrolled": true }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], "source": [ - "%pip install --upgrade --quiet pillow" + "pip install --upgrade --quiet pillow" ] }, { @@ -275,7 +398,7 @@ { "data": { "text/html": [ - "" + "" ], "text/plain": [ "" @@ -319,7 +442,7 @@ " display(HTML(image_html))\n", "\n", "\n", - "file_path = \"/Users/rlm/Desktop/Eval_Sets/multi_modal_presentations/DDOG/img_23.jpg\"\n", + "file_path = \"../../../static/img/ollama_example_img.jpg\"\n", "pil_image = Image.open(file_path)\n", "\n", "image_b64 = convert_to_base64(pil_image)\n", @@ -328,40 +451,52 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 5, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "AIMessage(content='90%')" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "90%\n" + ] } ], "source": [ + "from langchain.schema import HumanMessage\n", "from langchain_community.chat_models import ChatOllama\n", - "from langchain_core.messages import HumanMessage\n", "\n", - "chat_model = ChatOllama(\n", - " model=\"bakllava\",\n", - ")\n", + "llm = ChatOllama(model=\"bakllava\", temperature=0)\n", "\n", - "# Call the chat model with both messages and images\n", - "content_parts = []\n", - "image_part = {\n", - " \"type\": \"image_url\",\n", - " \"image_url\": f\"data:image/jpeg;base64,{image_b64}\",\n", - "}\n", - "text_part = {\"type\": \"text\", \"text\": \"What is the Daollar-based gross retention rate?\"}\n", "\n", - "content_parts.append(image_part)\n", - "content_parts.append(text_part)\n", - "prompt = [HumanMessage(content=content_parts)]\n", - "chat_model(prompt)" + "def prompt_func(data):\n", + " text = data[\"text\"]\n", + " image = data[\"image\"]\n", + "\n", + " image_part = {\n", + " \"type\": \"image_url\",\n", + " \"image_url\": f\"data:image/jpeg;base64,{image}\",\n", + " }\n", + "\n", + " content_parts = []\n", + "\n", + " text_part = {\"type\": \"text\", \"text\": text}\n", + "\n", + " content_parts.append(image_part)\n", + " content_parts.append(text_part)\n", + "\n", + " return [HumanMessage(content=content_parts)]\n", + "\n", + "\n", + "from langchain_core.output_parsers import StrOutputParser\n", + "\n", + "chain = prompt_func | llm | StrOutputParser()\n", + "\n", + "query_chain = chain.invoke(\n", + " {\"text\": \"What is the Dollar-based gross retention rate?\", \"image\": image_b64}\n", + ")\n", + "\n", + "print(query_chain)" ] } ], @@ -381,7 +516,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.16" + "version": "3.9.1" } }, "nbformat": 4, diff --git a/docs/docs/integrations/chat/sparkllm.ipynb b/docs/docs/integrations/chat/sparkllm.ipynb new file mode 100644 index 0000000000000..4fe68f9a2a709 --- /dev/null +++ b/docs/docs/integrations/chat/sparkllm.ipynb @@ -0,0 +1,99 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "3ddface67cd10a87", + "metadata": { + "collapsed": false + }, + "source": [ + "# SparkLLM Chat\n", + "\n", + "SparkLLM chat models API by iFlyTek. For more information, see [iFlyTek Open Platform](https://www.xfyun.cn/)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Basic use" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "43daa39972d4c533", + "metadata": { + "collapsed": false, + "is_executing": true + }, + "outputs": [], + "source": [ + "\"\"\"For basic init and call\"\"\"\n", + "from langchain.chat_models import ChatSparkLLM\n", + "from langchain.schema import HumanMessage\n", + "\n", + "chat = ChatSparkLLM(\n", + " spark_app_id=\"\", spark_api_key=\"\", spark_api_secret=\"\"\n", + ")\n", + "message = HumanMessage(content=\"Hello\")\n", + "chat([message])" + ] + }, + { + "cell_type": "markdown", + "id": "df755f4c5689510", + "metadata": { + "collapsed": false + }, + "source": [ + "- Get SparkLLM's app_id, api_key and api_secret from [iFlyTek SparkLLM API Console](https://console.xfyun.cn/services/bm3) (for more info, see [iFlyTek SparkLLM Intro](https://xinghuo.xfyun.cn/sparkapi) ), then set environment variables `IFLYTEK_SPARK_APP_ID`, `IFLYTEK_SPARK_API_KEY` and `IFLYTEK_SPARK_API_SECRET` or pass parameters when creating `ChatSparkLLM` as the demo above." + ] + }, + { + "cell_type": "markdown", + "id": "984e32ee47bc6772", + "metadata": { + "collapsed": false + }, + "source": [ + "## For ChatSparkLLM with Streaming" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7dc162bd65fec08f", + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "chat = ChatSparkLLM(streaming=True)\n", + "for chunk in chat.stream(\"Hello!\"):\n", + " print(chunk.content, end=\"\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/docs/integrations/document_loaders/cassandra.ipynb b/docs/docs/integrations/document_loaders/cassandra.ipynb new file mode 100644 index 0000000000000..49f261a18a84b --- /dev/null +++ b/docs/docs/integrations/document_loaders/cassandra.ipynb @@ -0,0 +1,241 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "id": "vm8vn9t8DvC_" + }, + "source": [ + "# Cassandra" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "[Cassandra](https://cassandra.apache.org/) is a NoSQL, row-oriented, highly scalable and highly available database.Starting with version 5.0, the database ships with [vector search capabilities](https://cassandra.apache.org/doc/trunk/cassandra/vector-search/overview.html)." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "id": "5WjXERXzFEhg" + }, + "source": [ + "## Overview" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "id": "juAmbgoWD17u" + }, + "source": [ + "The Cassandra Document Loader returns a list of Langchain Documents from a Cassandra database.\n", + "\n", + "You must either provide a CQL query or a table name to retrieve the documents.\n", + "The Loader takes the following parameters:\n", + "\n", + "* table: (Optional) The table to load the data from.\n", + "* session: (Optional) The cassandra driver session. If not provided, the cassio resolved session will be used.\n", + "* keyspace: (Optional) The keyspace of the table. If not provided, the cassio resolved keyspace will be used.\n", + "* query: (Optional) The query used to load the data.\n", + "* page_content_mapper: (Optional) a function to convert a row to string page content. The default converts the row to JSON.\n", + "* metadata_mapper: (Optional) a function to convert a row to metadata dict.\n", + "* query_parameters: (Optional) The query parameters used when calling session.execute .\n", + "* query_timeout: (Optional) The query timeout used when calling session.execute .\n", + "* query_custom_payload: (Optional) The query custom_payload used when calling `session.execute`.\n", + "* query_execution_profile: (Optional) The query execution_profile used when calling `session.execute`.\n", + "* query_host: (Optional) The query host used when calling `session.execute`.\n", + "* query_execute_as: (Optional) The query execute_as used when calling `session.execute`." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Load documents with the Document Loader" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_community.document_loaders import CassandraLoader" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Init from a cassandra driver Session\n", + "\n", + "You need to create a `cassandra.cluster.Session` object, as described in the [Cassandra driver documentation](https://docs.datastax.com/en/developer/python-driver/latest/api/cassandra/cluster/#module-cassandra.cluster). The details vary (e.g. with network settings and authentication), but this might be something like:" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "outputs": [], + "source": [ + "from cassandra.cluster import Cluster\n", + "\n", + "cluster = Cluster()\n", + "session = cluster.connect()" + ], + "metadata": { + "collapsed": false + }, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "You need to provide the name of an existing keyspace of the Cassandra instance:" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "outputs": [], + "source": [ + "CASSANDRA_KEYSPACE = input(\"CASSANDRA_KEYSPACE = \")" + ], + "metadata": { + "collapsed": false + }, + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "Creating the document loader:" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "ExecuteTime": { + "end_time": "2024-01-19T15:47:25.893037Z", + "start_time": "2024-01-19T15:47:25.889398Z" + } + }, + "outputs": [], + "source": [ + "loader = CassandraLoader(\n", + " table=\"movie_reviews\",\n", + " session=session,\n", + " keyspace=CASSANDRA_KEYSPACE,\n", + ")" + ] + }, + { + "cell_type": "code", + "outputs": [], + "source": [ + "docs = loader.load()" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-01-19T15:47:26.399472Z", + "start_time": "2024-01-19T15:47:26.389145Z" + } + }, + "execution_count": 17 + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "ExecuteTime": { + "end_time": "2024-01-19T15:47:33.287783Z", + "start_time": "2024-01-19T15:47:33.277862Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": "Document(page_content='Row(_id=\\'659bdffa16cbc4586b11a423\\', title=\\'Dangerous Men\\', reviewtext=\\'\"Dangerous Men,\" the picture\\\\\\'s production notes inform, took 26 years to reach the big screen. After having seen it, I wonder: What was the rush?\\')', metadata={'table': 'movie_reviews', 'keyspace': 'default_keyspace'})" + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "docs[0]" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Init from cassio\n", + "\n", + "It's also possible to use cassio to configure the session and keyspace." + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "outputs": [], + "source": [ + "import cassio\n", + "\n", + "cassio.init(contact_points=\"127.0.0.1\", keyspace=CASSANDRA_KEYSPACE)\n", + "\n", + "loader = CassandraLoader(\n", + " table=\"movie_reviews\",\n", + ")\n", + "\n", + "docs = loader.load()" + ], + "metadata": { + "collapsed": false + }, + "execution_count": null + } + ], + "metadata": { + "colab": { + "collapsed_sections": [ + "5WjXERXzFEhg" + ], + "provenance": [] + }, + "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.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/docs/integrations/document_loaders/example_data/fake.vsdx b/docs/docs/integrations/document_loaders/example_data/fake.vsdx new file mode 100644 index 0000000000000..4e6502942eaba Binary files /dev/null and b/docs/docs/integrations/document_loaders/example_data/fake.vsdx differ diff --git a/docs/docs/integrations/document_loaders/psychic.ipynb b/docs/docs/integrations/document_loaders/psychic.ipynb index 89e8d6487c23b..edd94d57c048f 100644 --- a/docs/docs/integrations/document_loaders/psychic.ipynb +++ b/docs/docs/integrations/document_loaders/psychic.ipynb @@ -8,7 +8,7 @@ "This notebook covers how to load documents from `Psychic`. See [here](/docs/integrations/providers/psychic) for more details.\n", "\n", "## Prerequisites\n", - "1. Follow the Quick Start section in [this document](/docs/ecosystem/integrations/psychic)\n", + "1. Follow the Quick Start section in [this document](/docs/integrations/providers/psychic)\n", "2. Log into the [Psychic dashboard](https://dashboard.psychic.dev/) and get your secret key\n", "3. Install the frontend react library into your web app and have a user authenticate a connection. The connection will be created using the connection id that you specify." ] diff --git a/docs/docs/integrations/document_loaders/surrealdb.ipynb b/docs/docs/integrations/document_loaders/surrealdb.ipynb new file mode 100644 index 0000000000000..e8e68effb978b --- /dev/null +++ b/docs/docs/integrations/document_loaders/surrealdb.ipynb @@ -0,0 +1,236 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "5812b612-3e77-4be2-aefb-fbb16141ab79", + "metadata": {}, + "source": [ + "# SurrealDB\n", + "\n", + ">[SurrealDB](https://surrealdb.com/) is an end-to-end cloud-native database designed for modern applications, including web, mobile, serverless, Jamstack, backend, and traditional applications. With SurrealDB, you can simplify your database and API infrastructure, reduce development time, and build secure, performant apps quickly and cost-effectively.\n", + ">\n", + ">**Key features of SurrealDB include:**\n", + ">\n", + ">* **Reduces development time:** SurrealDB simplifies your database and API stack by removing the need for most server-side components, allowing you to build secure, performant apps faster and cheaper.\n", + ">* **Real-time collaborative API backend service:** SurrealDB functions as both a database and an API backend service, enabling real-time collaboration.\n", + ">* **Support for multiple querying languages:** SurrealDB supports SQL querying from client devices, GraphQL, ACID transactions, WebSocket connections, structured and unstructured data, graph querying, full-text indexing, and geospatial querying.\n", + ">* **Granular access control:** SurrealDB provides row-level permissions-based access control, giving you the ability to manage data access with precision.\n", + ">\n", + ">View the [features](https://surrealdb.com/features), the latest [releases](https://surrealdb.com/releases), and [documentation](https://surrealdb.com/docs).\n", + "\n", + "This notebook shows how to use functionality related to the `SurrealDBLoader`." + ] + }, + { + "cell_type": "markdown", + "id": "f56ccec5-24b3-4762-91a6-91385e041fee", + "metadata": {}, + "source": [ + "## Overview\n", + "\n", + "The SurrealDB Document Loader returns a list of Langchain Documents from a SurrealDB database.\n", + "\n", + "The Document Loader takes the following optional parameters:\n", + "\n", + "* `dburl`: connection string to the websocket endpoint. default: `ws://localhost:8000/rpc`\n", + "* `ns`: name of the namespace. default: `langchain`\n", + "* `db`: name of the database. default: `database`\n", + "* `table`: name of the table. default: `documents`\n", + "* `db_user`: SurrealDB credentials if needed: db username.\n", + "* `db_pass`: SurrealDB credentails if needed: db password.\n", + "* `filter_criteria`: dictionary to construct the `WHERE` clause for filtering results from table.\n", + "\n", + "The output `Document` takes the following shape:\n", + "```\n", + "Document(\n", + " page_content=,\n", + " metadata={\n", + " 'id': ,\n", + " 'ns': ,\n", + " 'db': ,\n", + " 'table': ,\n", + " ... \n", + " }\n", + ")\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "77b024e0-c804-4b19-9f5e-0099eb61ba79", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "Uncomment the below cells to install surrealdb and langchain." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "508bc4f3-3aa2-45d3-8e59-cd7d0ffec379", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# %pip install --upgrade --quiet surrealdb langchain langchain-community" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "3ee3d767-b9ba-4be4-9e80-8fa6376beaba", + "metadata": {}, + "outputs": [], + "source": [ + "# add this import for running in jupyter notebook\n", + "import nest_asyncio\n", + "\n", + "nest_asyncio.apply()" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "1ec629f4-b99a-44f1-a938-29de7439f121", + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "\n", + "from langchain_community.document_loaders.surrealdb import SurrealDBLoader" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "8deb90ac-7d4e-422c-a87a-8e6e41390a6d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "42" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "loader = SurrealDBLoader(\n", + " dburl=\"ws://localhost:8000/rpc\",\n", + " ns=\"langchain\",\n", + " db=\"database\",\n", + " table=\"documents\",\n", + " db_user=\"root\",\n", + " db_pass=\"root\",\n", + " filter_criteria={},\n", + ")\n", + "docs = loader.load()\n", + "len(docs)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "0aa9d3f7-56b3-464d-9d3d-1df7164122ba", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'id': 'documents:zzz434sa584xl3b4ohvk',\n", + " 'source': '../../modules/state_of_the_union.txt',\n", + " 'ns': 'langchain',\n", + " 'db': 'database',\n", + " 'table': 'documents'}" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "doc = docs[-1]\n", + "doc.metadata" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "0378dd34-c690-4b8e-8816-90a8acc2f227", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "18078" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(doc.page_content)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "f30f1141-329b-4674-acb4-36d9d5a9ef0a", + "metadata": {}, + "outputs": [], + "source": [ + "page_content = json.loads(doc.page_content)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "2a58496f-a831-40ec-be6b-92ce70f78133", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'When we use taxpayer dollars to rebuild America – we are going to Buy American: buy American products to support American jobs. \\n\\nThe federal government spends about $600 Billion a year to keep the country safe and secure. \\n\\nThere’s been a law on the books for almost a century \\nto make sure taxpayers’ dollars support American jobs and businesses. \\n\\nEvery Administration says they’ll do it, but we are actually doing it. \\n\\nWe will buy American to make sure everything from the deck of an aircraft carrier to the steel on highway guardrails are made in America. \\n\\nBut to compete for the best jobs of the future, we also need to level the playing field with China and other competitors. \\n\\nThat’s why it is so important to pass the Bipartisan Innovation Act sitting in Congress that will make record investments in emerging technologies and American manufacturing. \\n\\nLet me give you one example of why it’s so important to pass it.'" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "page_content[\"text\"]" + ] + } + ], + "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.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/docs/integrations/document_loaders/unstructured_file.ipynb b/docs/docs/integrations/document_loaders/unstructured_file.ipynb index 113e42bf2bb61..0d26030ab7ef9 100644 --- a/docs/docs/integrations/document_loaders/unstructured_file.ipynb +++ b/docs/docs/integrations/document_loaders/unstructured_file.ipynb @@ -12,7 +12,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "2886982e", "metadata": {}, "outputs": [], @@ -100,6 +100,54 @@ "docs[0].page_content[:400]" ] }, + { + "cell_type": "markdown", + "id": "b4ab0a79", + "metadata": {}, + "source": [ + "### Load list of files" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "092d9a0b", + "metadata": {}, + "outputs": [], + "source": [ + "files = [\"./example_data/whatsapp_chat.txt\", \"./example_data/layout-parser-paper.pdf\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f841c4f8", + "metadata": {}, + "outputs": [], + "source": [ + "loader = UnstructuredFileLoader(files)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "993c240b", + "metadata": {}, + "outputs": [], + "source": [ + "docs = loader.load()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5ce4ff07", + "metadata": {}, + "outputs": [], + "source": [ + "docs[0].page_content[:400]" + ] + }, { "cell_type": "markdown", "id": "7874d01d", @@ -495,7 +543,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.10" + "version": "3.9.0" } }, "nbformat": 4, diff --git a/docs/docs/integrations/document_loaders/vsdx.ipynb b/docs/docs/integrations/document_loaders/vsdx.ipynb new file mode 100644 index 0000000000000..1a162961879e3 --- /dev/null +++ b/docs/docs/integrations/document_loaders/vsdx.ipynb @@ -0,0 +1,486 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Vsdx" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "> A [visio file](https://fr.wikipedia.org/wiki/Microsoft_Visio) (with extension .vsdx) is associated with Microsoft Visio, a diagram creation software. It stores information about the structure, layout, and graphical elements of a diagram. This format facilitates the creation and sharing of visualizations in areas such as business, engineering, and computer science." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "A Visio file can contain multiple pages. Some of them may serve as the background for others, and this can occur across multiple layers. This **loader** extracts the textual content from each page and its associated pages, enabling the extraction of all visible text from each page, similar to what an OCR algorithm would do." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**WARNING** : Only Visio files with the **.vsdx** extension are compatible with this loader. Files with extensions such as .vsd, ... are not compatible because they cannot be converted to compressed XML." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_community.document_loaders import VsdxLoader" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "loader = VsdxLoader(file_path=\"./example_data/fake.vsdx\")\n", + "documents = loader.load()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Display loaded documents**" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "------ Page 0 ------\n", + "Title page : Summary\n", + "Source : ./example_data/fake.vsdx\n", + "\n", + "==> CONTENT <== \n", + "Created by\n", + "Created the\n", + "Modified by\n", + "Modified the\n", + "Version\n", + "Title\n", + "Florian MOREL\n", + "2024-01-14\n", + "FLORIAN Morel\n", + "Today\n", + "0.0.0.0.0.1\n", + "This is a title\n", + "Best Caption of the worl\n", + "This is an arrow\n", + "This is Earth\n", + "This is a bounded arrow\n", + "\n", + "------ Page 1 ------\n", + "Title page : Glossary\n", + "Source : ./example_data/fake.vsdx\n", + "\n", + "==> CONTENT <== \n", + "Created by\n", + "Created the\n", + "Modified by\n", + "Modified the\n", + "Version\n", + "Title\n", + "Florian MOREL\n", + "2024-01-14\n", + "FLORIAN Morel\n", + "Today\n", + "0.0.0.0.0.1\n", + "This is a title\n", + "\n", + "------ Page 2 ------\n", + "Title page : blanket page\n", + "Source : ./example_data/fake.vsdx\n", + "\n", + "==> CONTENT <== \n", + "Created by\n", + "Created the\n", + "Modified by\n", + "Modified the\n", + "Version\n", + "Title\n", + "Florian MOREL\n", + "2024-01-14\n", + "FLORIAN Morel\n", + "Today\n", + "0.0.0.0.0.1\n", + "This is a title\n", + "This file is a vsdx file\n", + "First text\n", + "Second text\n", + "Third text\n", + "\n", + "------ Page 3 ------\n", + "Title page : BLABLABLA\n", + "Source : ./example_data/fake.vsdx\n", + "\n", + "==> CONTENT <== \n", + "Created by\n", + "Created the\n", + "Modified by\n", + "Modified the\n", + "Version\n", + "Title\n", + "Florian MOREL\n", + "2024-01-14\n", + "FLORIAN Morel\n", + "Today\n", + "0.0.0.0.0.1\n", + "This is a title\n", + "Another RED arrow wow\n", + "Arrow with point but red\n", + "Green line\n", + "User\n", + "Captions\n", + "Red arrow magic !\n", + "Something white\n", + "Something Red\n", + "This a a completly useless diagramm, cool !!\n", + "\n", + "But this is for example !\n", + "This diagramm is a base of many pages in this file. But it is editable in file \\\"BG WITH CONTENT\\\"\n", + "This is a page with something...\n", + "\n", + "WAW I have learned something !\n", + "This is a page with something...\n", + "\n", + "WAW I have learned something !\n", + "\n", + "X2\n", + "\n", + "------ Page 4 ------\n", + "Title page : What a page !!\n", + "Source : ./example_data/fake.vsdx\n", + "\n", + "==> CONTENT <== \n", + "Created by\n", + "Created the\n", + "Modified by\n", + "Modified the\n", + "Version\n", + "Title\n", + "Florian MOREL\n", + "2024-01-14\n", + "FLORIAN Morel\n", + "Today\n", + "0.0.0.0.0.1\n", + "This is a title\n", + "Something white\n", + "Something Red\n", + "This a a completly useless diagramm, cool !!\n", + "\n", + "But this is for example !\n", + "This diagramm is a base of many pages in this file. But it is editable in file \\\"BG WITH CONTENT\\\"\n", + "Another RED arrow wow\n", + "Arrow with point but red\n", + "Green line\n", + "User\n", + "Captions\n", + "Red arrow magic !\n", + "\n", + "------ Page 5 ------\n", + "Title page : next page after previous one\n", + "Source : ./example_data/fake.vsdx\n", + "\n", + "==> CONTENT <== \n", + "Created by\n", + "Created the\n", + "Modified by\n", + "Modified the\n", + "Version\n", + "Title\n", + "Florian MOREL\n", + "2024-01-14\n", + "FLORIAN Morel\n", + "Today\n", + "0.0.0.0.0.1\n", + "This is a title\n", + "Another RED arrow wow\n", + "Arrow with point but red\n", + "Green line\n", + "User\n", + "Captions\n", + "Red arrow magic !\n", + "Something white\n", + "Something Red\n", + "This a a completly useless diagramm, cool !!\n", + "\n", + "But this is for example !\n", + "This diagramm is a base of many pages in this file. But it is editable in file \\\"BG WITH CONTENT\\\"\n", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor\n", + "\\u00a0\\u00a0\\u00a0\\u00a0\\u00a0\\u00a0\\u00a0\\u00a0\\u00a0\\u00a0\\u00a0\\u00a0\\u00a0\\u00a0\\u00a0\\u00a0\\u00a0\\u00a0\\u00a0\\u00a0-\\u00a0incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in\n", + "\n", + "\n", + "voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa\n", + "*\n", + "\n", + "\n", + "qui officia deserunt mollit anim id est laborum.\n", + "\n", + "------ Page 6 ------\n", + "Title page : Connector Page\n", + "Source : ./example_data/fake.vsdx\n", + "\n", + "==> CONTENT <== \n", + "Created by\n", + "Created the\n", + "Modified by\n", + "Modified the\n", + "Version\n", + "Title\n", + "Florian MOREL\n", + "2024-01-14\n", + "FLORIAN Morel\n", + "Today\n", + "0.0.0.0.0.1\n", + "This is a title\n", + "Something white\n", + "Something Red\n", + "This a a completly useless diagramm, cool !!\n", + "\n", + "But this is for example !\n", + "This diagramm is a base of many pages in this file. But it is editable in file \\\"BG WITH CONTENT\\\"\n", + "\n", + "------ Page 7 ------\n", + "Title page : Useful ↔ Useless page\n", + "Source : ./example_data/fake.vsdx\n", + "\n", + "==> CONTENT <== \n", + "Created by\n", + "Created the\n", + "Modified by\n", + "Modified the\n", + "Version\n", + "Title\n", + "Florian MOREL\n", + "2024-01-14\n", + "FLORIAN Morel\n", + "Today\n", + "0.0.0.0.0.1\n", + "This is a title\n", + "Something white\n", + "Something Red\n", + "This a a completly useless diagramm, cool !!\n", + "\n", + "But this is for example !\n", + "This diagramm is a base of many pages in this file. But it is editable in file \\\"BG WITH CONTENT\\\"\n", + "Title of this document : BLABLABLA\n", + "\n", + "------ Page 8 ------\n", + "Title page : Alone page\n", + "Source : ./example_data/fake.vsdx\n", + "\n", + "==> CONTENT <== \n", + "Black cloud\n", + "Unidirectional traffic primary path\n", + "Unidirectional traffic backup path\n", + "Encapsulation\n", + "User\n", + "Captions\n", + "Bidirectional traffic\n", + "Alone, sad\n", + "Test of another page\n", + "This is a \\\"bannier\\\"\n", + "Tests of some exotics characters :\\u00a0\\u00e3\\u00e4\\u00e5\\u0101\\u0103 \\u00fc\\u2554\\u00a0 \\u00a0\\u00bc \\u00c7 \\u25d8\\u25cb\\u2642\\u266b\\u2640\\u00ee\\u2665\n", + "This is ethernet\n", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n", + "This is an empty case\n", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor\n", + "\\u00a0 \\u00a0 \\u00a0 \\u00a0 \\u00a0 \\u00a0 \\u00a0 \\u00a0 \\u00a0 \\u00a0 \\u00a0 \\u00a0 \\u00a0 \\u00a0 \\u00a0 \\u00a0 \\u00a0 \\u00a0 \\u00a0 \\u00a0-\\u00a0 incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in\n", + "\n", + "\n", + " voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa \n", + "*\n", + "\n", + "\n", + "qui officia deserunt mollit anim id est laborum.\n", + "\n", + "------ Page 9 ------\n", + "Title page : BG\n", + "Source : ./example_data/fake.vsdx\n", + "\n", + "==> CONTENT <== \n", + "Best Caption of the worl\n", + "This is an arrow\n", + "This is Earth\n", + "This is a bounded arrow\n", + "Created by\n", + "Created the\n", + "Modified by\n", + "Modified the\n", + "Version\n", + "Title\n", + "Florian MOREL\n", + "2024-01-14\n", + "FLORIAN Morel\n", + "Today\n", + "0.0.0.0.0.1\n", + "This is a title\n", + "\n", + "------ Page 10 ------\n", + "Title page : BG + caption1\n", + "Source : ./example_data/fake.vsdx\n", + "\n", + "==> CONTENT <== \n", + "Created by\n", + "Created the\n", + "Modified by\n", + "Modified the\n", + "Version\n", + "Title\n", + "Florian MOREL\n", + "2024-01-14\n", + "FLORIAN Morel\n", + "Today\n", + "0.0.0.0.0.1\n", + "This is a title\n", + "Another RED arrow wow\n", + "Arrow with point but red\n", + "Green line\n", + "User\n", + "Captions\n", + "Red arrow magic !\n", + "Something white\n", + "Something Red\n", + "This a a completly useless diagramm, cool !!\n", + "\n", + "But this is for example !\n", + "This diagramm is a base of many pages in this file. But it is editable in file \\\"BG WITH CONTENT\\\"\n", + "Useful\\u2194 Useless page\\u00a0\n", + "\n", + "Tests of some exotics characters :\\u00a0\\u00e3\\u00e4\\u00e5\\u0101\\u0103 \\u00fc\\u2554\\u00a0\\u00a0\\u00bc \\u00c7 \\u25d8\\u25cb\\u2642\\u266b\\u2640\\u00ee\\u2665\n", + "\n", + "------ Page 11 ------\n", + "Title page : BG+\n", + "Source : ./example_data/fake.vsdx\n", + "\n", + "==> CONTENT <== \n", + "Created by\n", + "Created the\n", + "Modified by\n", + "Modified the\n", + "Version\n", + "Title\n", + "Florian MOREL\n", + "2024-01-14\n", + "FLORIAN Morel\n", + "Today\n", + "0.0.0.0.0.1\n", + "This is a title\n", + "\n", + "------ Page 12 ------\n", + "Title page : BG WITH CONTENT\n", + "Source : ./example_data/fake.vsdx\n", + "\n", + "==> CONTENT <== \n", + "Created by\n", + "Created the\n", + "Modified by\n", + "Modified the\n", + "Version\n", + "Title\n", + "Florian MOREL\n", + "2024-01-14\n", + "FLORIAN Morel\n", + "Today\n", + "0.0.0.0.0.1\n", + "This is a title\n", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n", + "\n", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n", + "\n", + "\n", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. - Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n", + "\n", + "\n", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n", + "This is a page with a lot of text\n", + "\n", + "------ Page 13 ------\n", + "Title page : 2nd caption with ____________________________________________________________________ content\n", + "Source : ./example_data/fake.vsdx\n", + "\n", + "==> CONTENT <== \n", + "Created by\n", + "Created the\n", + "Modified by\n", + "Modified the\n", + "Version\n", + "Title\n", + "Florian MOREL\n", + "2024-01-14\n", + "FLORIAN Morel\n", + "Today\n", + "0.0.0.0.0.1\n", + "This is a title\n", + "Another RED arrow wow\n", + "Arrow with point but red\n", + "Green line\n", + "User\n", + "Captions\n", + "Red arrow magic !\n", + "Something white\n", + "Something Red\n", + "This a a completly useless diagramm, cool !!\n", + "\n", + "But this is for example !\n", + "This diagramm is a base of many pages in this file. But it is editable in file \\\"BG WITH CONTENT\\\"\n", + "Only connectors on this page. This is the CoNNeCtor page\n" + ] + } + ], + "source": [ + "for i, doc in enumerate(documents):\n", + " print(f\"\\n------ Page {doc.metadata['page']} ------\")\n", + " print(f\"Title page : {doc.metadata['page_name']}\")\n", + " print(f\"Source : {doc.metadata['source']}\")\n", + " print(\"\\n==> CONTENT <== \")\n", + " print(doc.page_content)" + ] + } + ], + "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.8.2" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/docs/integrations/llms/ai21.ipynb b/docs/docs/integrations/llms/ai21.ipynb index 3adbd7098745a..2e22f85f11d59 100644 --- a/docs/docs/integrations/llms/ai21.ipynb +++ b/docs/docs/integrations/llms/ai21.ipynb @@ -14,12 +14,21 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "02be122d-04e8-4ec6-84d1-f1d8961d6828", "metadata": { "tags": [] }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[33mWARNING: There was an error checking the latest version of pip.\u001b[0m\u001b[33m\n", + "\u001b[0mNote: you may need to restart the kernel to use updated packages.\n" + ] + } + ], "source": [ "# install the package:\n", "%pip install --upgrade --quiet ai21" @@ -27,20 +36,12 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 1, "id": "4229227e-6ca2-41ad-a3c3-5f29e3559091", "metadata": { "tags": [] }, - "outputs": [ - { - "name": "stdin", - "output_type": "stream", - "text": [ - " ········\n" - ] - } - ], + "outputs": [], "source": [ "# get AI21_API_KEY. Use https://studio.ai21.com/account/account\n", "\n", @@ -51,21 +52,20 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 7, "id": "6fb585dd", "metadata": { "tags": [] }, "outputs": [], "source": [ - "from langchain.chains import LLMChain\n", - "from langchain.prompts import PromptTemplate\n", - "from langchain_community.llms import AI21" + "from langchain_community.llms import AI21\n", + "from langchain_core.prompts import PromptTemplate" ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 12, "id": "035dea0f", "metadata": { "tags": [] @@ -76,12 +76,12 @@ "\n", "Answer: Let's think step by step.\"\"\"\n", "\n", - "prompt = PromptTemplate(template=template, input_variables=[\"question\"])" + "prompt = PromptTemplate.from_template(template)" ] }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 9, "id": "3f3458d9", "metadata": { "tags": [] @@ -93,19 +93,19 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 10, "id": "a641dbd9", "metadata": { "tags": [] }, "outputs": [], "source": [ - "llm_chain = LLMChain(prompt=prompt, llm=llm)" + "llm_chain = prompt | llm" ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 13, "id": "9f0b1960", "metadata": { "tags": [] @@ -114,10 +114,10 @@ { "data": { "text/plain": [ - "'\\n1. What year was Justin Bieber born?\\nJustin Bieber was born in 1994.\\n2. What team won the Super Bowl in 1994?\\nThe Dallas Cowboys won the Super Bowl in 1994.'" + "'\\nThe Super Bowl in the year Justin Beiber was born was in the year 1991.\\nThe Super Bowl in 1991 was won by the Washington Redskins.\\nFinal answer: Washington Redskins'" ] }, - "execution_count": 12, + "execution_count": 13, "metadata": {}, "output_type": "execute_result" } @@ -125,7 +125,7 @@ "source": [ "question = \"What NFL team won the Super Bowl in the year Justin Beiber was born?\"\n", "\n", - "llm_chain.run(question)" + "llm_chain.invoke({\"question\": question})" ] }, { @@ -153,7 +153,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.3" + "version": "3.10.13" } }, "nbformat": 4, diff --git a/docs/docs/integrations/llms/azure_ml.ipynb b/docs/docs/integrations/llms/azure_ml.ipynb index 63aa8a2cb0633..9d066bddb3cc9 100644 --- a/docs/docs/integrations/llms/azure_ml.ipynb +++ b/docs/docs/integrations/llms/azure_ml.ipynb @@ -6,9 +6,9 @@ "source": [ "# Azure ML\n", "\n", - "[Azure ML](https://azure.microsoft.com/en-us/products/machine-learning/) is a platform used to build, train, and deploy machine learning models. Users can explore the types of models to deploy in the Model Catalog, which provides Azure Foundation Models and OpenAI Models. Azure Foundation Models include various open-source models and popular Hugging Face models. Users can also import models of their liking into AzureML.\n", + "[Azure ML](https://azure.microsoft.com/en-us/products/machine-learning/) is a platform used to build, train, and deploy machine learning models. Users can explore the types of models to deploy in the Model Catalog, which provides foundational and general purpose models from different providers.\n", "\n", - "This notebook goes over how to use an LLM hosted on an `AzureML online endpoint`" + "This notebook goes over how to use an LLM hosted on an `Azure ML Online Endpoint`." ] }, { @@ -26,11 +26,12 @@ "source": [ "## Set up\n", "\n", - "To use the wrapper, you must [deploy a model on AzureML](https://learn.microsoft.com/en-us/azure/machine-learning/how-to-use-foundation-models?view=azureml-api-2#deploying-foundation-models-to-endpoints-for-inferencing) and obtain the following parameters:\n", + "You must [deploy a model on Azure ML](https://learn.microsoft.com/en-us/azure/machine-learning/how-to-use-foundation-models?view=azureml-api-2#deploying-foundation-models-to-endpoints-for-inferencing) or [to Azure AI studio](https://learn.microsoft.com/en-us/azure/ai-studio/how-to/deploy-models-open) and obtain the following parameters:\n", "\n", - "* `endpoint_api_key`: Required - The API key provided by the endpoint\n", - "* `endpoint_url`: Required - The REST endpoint url provided by the endpoint\n", - "* `deployment_name`: Not required - The deployment name of the model using the endpoint" + "* `endpoint_url`: The REST endpoint url provided by the endpoint.\n", + "* `endpoint_api_type`: Use `endpoint_type='realtime'` when deploying models to **Realtime endpoints** (hosted managed infrastructure). Use `endpoint_type='serverless'` when deploying models using the **Pay-as-you-go** offering (model as a service).\n", + "* `endpoint_api_key`: The API key provided by the endpoint.\n", + "* `deployment_name`: (Optional) The deployment name of the model using the endpoint." ] }, { @@ -46,31 +47,107 @@ "* `HFContentFormatter`: Formats request and response data for text-generation Hugging Face models\n", "* `LLamaContentFormatter`: Formats request and response data for LLaMa2\n", "\n", - "*Note: `OSSContentFormatter` is being deprecated and replaced with `GPT2ContentFormatter`. The logic is the same but `GPT2ContentFormatter` is a more suitable name. You can still continue to use `OSSContentFormatter` as the changes are backwards compatible.*\n", + "*Note: `OSSContentFormatter` is being deprecated and replaced with `GPT2ContentFormatter`. The logic is the same but `GPT2ContentFormatter` is a more suitable name. You can still continue to use `OSSContentFormatter` as the changes are backwards compatible.*" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Examples" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example: LlaMa 2 completions with real-time endpoints" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from langchain.schema import HumanMessage\n", + "from langchain_community.llms.azureml_endpoint import (\n", + " AzureMLEndpointApiType,\n", + " LlamaContentFormatter,\n", + ")\n", "\n", - "Below is an example using a summarization model from Hugging Face." + "llm = AzureMLOnlineEndpoint(\n", + " endpoint_url=\"https://..inference.ml.azure.com/score\",\n", + " endpoint_api_type=AzureMLEndpointApiType.realtime,\n", + " endpoint_api_key=\"my-api-key\",\n", + " content_formatter=LlamaContentFormatter(),\n", + " model_kwargs={\"temperature\": 0.8, \"max_new_tokens\": 400},\n", + ")\n", + "response = llm.invoke(\"Write me a song about sparkling water:\")\n", + "response" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### Custom Content Formatter" + "Model parameters can also be indicated during invocation:" ] }, { "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "HaSeul won her first music show trophy with \"So What\" on Mnet's M Countdown. Loona released their second EP titled [#] (read as hash] on February 5, 2020. HaSeul did not take part in the promotion of the album because of mental health issues. On October 19, 2020, they released their third EP called [12:00]. It was their first album to enter the Billboard 200, debuting at number 112. On June 2, 2021, the group released their fourth EP called Yummy-Yummy. On August 27, it was announced that they are making their Japanese debut on September 15 under Universal Music Japan sublabel EMI Records.\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "response = llm.invoke(\"Write me a song about sparkling water:\", temperature=0.5)\n", + "response" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example: Chat completions with pay-as-you-go deployments (model as a service)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from langchain.schema import HumanMessage\n", + "from langchain_community.llms.azureml_endpoint import (\n", + " AzureMLEndpointApiType,\n", + " LlamaContentFormatter,\n", + ")\n", + "\n", + "llm = AzureMLOnlineEndpoint(\n", + " endpoint_url=\"https://..inference.ml.azure.com/v1/completions\",\n", + " endpoint_api_type=AzureMLEndpointApiType.serverless,\n", + " endpoint_api_key=\"my-api-key\",\n", + " content_formatter=LlamaContentFormatter(),\n", + " model_kwargs={\"temperature\": 0.8, \"max_new_tokens\": 400},\n", + ")\n", + "response = llm.invoke(\"Write me a song about sparkling water:\")\n", + "response" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example: Custom content formatter\n", + "\n", + "Below is an example using a summarization model from Hugging Face." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "import json\n", "import os\n", @@ -104,6 +181,7 @@ "content_formatter = CustomFormatter()\n", "\n", "llm = AzureMLOnlineEndpoint(\n", + " endpoint_api_type=\"realtime\",\n", " endpoint_api_key=os.getenv(\"BART_ENDPOINT_API_KEY\"),\n", " endpoint_url=os.getenv(\"BART_ENDPOINT_URL\"),\n", " model_kwargs={\"temperature\": 0.8, \"max_new_tokens\": 400},\n", @@ -132,7 +210,7 @@ "that Loona will release the double A-side single, \"Hula Hoop / Star Seed\" on September 15, with a physical CD release on October \n", "20.[53] In December, Chuu filed an injunction to suspend her exclusive contract with Blockberry Creative.[54][55]\n", "\"\"\"\n", - "summarized_text = llm(large_text)\n", + "summarized_text = llm.invoke(large_text)\n", "print(summarized_text)" ] }, @@ -140,22 +218,14 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Dolly with LLMChain" + "### Example: Dolly with LLMChain" ] }, { "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Many people are willing to talk about themselves; it's others who seem to be stuck up. Try to understand others where they're coming from. Like minded people can build a tribe together.\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "from langchain.chains import LLMChain\n", "from langchain.prompts import PromptTemplate\n", @@ -177,31 +247,22 @@ ")\n", "\n", "chain = LLMChain(llm=llm, prompt=prompt)\n", - "print(chain.run({\"word_count\": 100, \"topic\": \"how to make friends\"}))" + "print(chain.invoke({\"word_count\": 100, \"topic\": \"how to make friends\"}))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### Serializing an LLM\n", + "## Serializing an LLM\n", "You can also save and load LLM configurations" ] }, { "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[1mAzureMLOnlineEndpoint\u001b[0m\n", - "Params: {'deployment_name': 'databricks-dolly-v2-12b-4', 'model_kwargs': {'temperature': 0.2, 'max_tokens': 150, 'top_p': 0.8, 'frequency_penalty': 0.32, 'presence_penalty': 0.072}}\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "from langchain_community.llms.loading import load_llm\n", "\n", @@ -224,9 +285,9 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "langchain", "language": "python", - "name": "python3" + "name": "langchain" }, "language_info": { "codemirror_mode": { @@ -238,7 +299,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.12" + "version": "3.11.5" } }, "nbformat": 4, diff --git a/docs/docs/integrations/llms/google_vertex_ai_palm.ipynb b/docs/docs/integrations/llms/google_vertex_ai_palm.ipynb index 9a4edee338fdd..4f53a75c925b4 100644 --- a/docs/docs/integrations/llms/google_vertex_ai_palm.ipynb +++ b/docs/docs/integrations/llms/google_vertex_ai_palm.ipynb @@ -11,29 +11,30 @@ }, { "cell_type": "markdown", - "metadata": { - "id": "xazoWTniN8Xa" - }, + "metadata": {}, "source": [ "# Google Cloud Vertex AI\n", "\n", - "**Note:** This is separate from the `Google Generative AI` integration, it exposes [Vertex AI Generative API](https://cloud.google.com/vertex-ai/docs/generative-ai/learn/overview) on `Google Cloud`.\n" + "**Note:** This is separate from the `Google Generative AI` integration, it exposes [Vertex AI Generative API](https://cloud.google.com/vertex-ai/docs/generative-ai/learn/overview) on `Google Cloud`.\n", + "\n", + "VertexAI exposes all foundational models available in google cloud:\n", + "- Gemini (`gemini-pro` and `gemini-pro-vision`)\n", + "- Palm 2 for Text (`text-bison`)\n", + "- Codey for Code Generation (`code-bison`)\n", + "\n", + "For a full and updated list of available models visit [VertexAI documentation](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/overview)" ] }, { "cell_type": "markdown", - "metadata": { - "id": "Q_UoF2FKN8Xb" - }, + "metadata": {}, "source": [ - "## Setting up" + "## Setup" ] }, { "cell_type": "markdown", - "metadata": { - "id": "8uImJzc4N8Xb" - }, + "metadata": {}, "source": [ "By default, Google Cloud [does not use](https://cloud.google.com/vertex-ai/docs/generative-ai/data-governance#foundation_model_development) customer data to train its foundation models as part of Google Cloud's AI/ML Privacy Commitment. More details about how Google processes data can also be found in [Google's Customer Data Processing Addendum (CDPA)](https://cloud.google.com/terms/data-processing-addendum).\n", "\n", @@ -52,78 +53,29 @@ "cell_type": "code", "execution_count": null, "metadata": {}, - "outputs": [], - "source": [ - "%pip install --upgrade --quiet langchain-core langchain-google-vertexai" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - " **Pros of Python:**\n", "\n", - "* **Easy to learn and use:** Python is known for its simple syntax and readability, making it a great choice for beginners. It also has a large and supportive community, with many resources available online.\n", - "* **Versatile:** Python can be used for a wide variety of tasks, including web development, data science, machine learning, and artificial intelligence.\n", - "* **Powerful:** Python has a rich library of built-in functions and modules, making it easy to perform complex tasks without having to write a lot of code.\n", - "* **Cross-platform:** Python can be run on a variety of operating systems\n" + "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m A new release of pip is available: \u001b[0m\u001b[31;49m23.2.1\u001b[0m\u001b[39;49m -> \u001b[0m\u001b[32;49m23.3.2\u001b[0m\n", + "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m To update, run: \u001b[0m\u001b[32;49mpip install --upgrade pip\u001b[0m\n", + "Note: you may need to restart the kernel to use updated packages.\n" ] } ], "source": [ - "from langchain_google_vertexai import VertexAI\n", - "\n", - "llm = VertexAI()\n", - "print(llm(\"What are some of the pros and cons of Python as a programming language?\"))" + "%pip install --upgrade --quiet langchain-core langchain-google-vertexai" ] }, { "cell_type": "markdown", - "metadata": { - "id": "38S1FS3qN8Xc" - }, - "source": [ - "You can also use Gemini model (in preview) with VertexAI:" - ] - }, - { - "cell_type": "code", - "execution_count": 6, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "**Pros of Python:**\n", - "\n", - "* **Easy to learn and use:** Python is known for its simplicity and readability, making it a great choice for beginners and experienced programmers alike. Its syntax is straightforward and intuitive, allowing developers to quickly pick up the language and start writing code.\n", - "\n", - "\n", - "* **Versatile:** Python is a general-purpose language that can be used for a wide range of applications, including web development, data science, machine learning, and scripting. Its extensive standard library and vast ecosystem of third-party modules make it suitable for a variety of tasks.\n", - "\n", - "\n", - "* **Cross-platform:** Python is compatible with multiple operating systems, including\n" - ] - } - ], "source": [ - "llm = VertexAI(model_name=\"gemini-pro\")\n", - "print(llm(\"What are some of the pros and cons of Python as a programming language?\"))" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "_-9MhhN8N8Xc" - }, - "source": [ - "## Using in a chain" + "## Usage\n", + "\n", + "VertexAI supports all [LLM](/docs/modules/model_io/llms/) functionality." ] }, { @@ -132,12 +84,7 @@ "metadata": {}, "outputs": [], "source": [ - "from langchain_core.prompts import PromptTemplate\n", - "\n", - "template = \"\"\"Question: {question}\n", - "\n", - "Answer: Let's think step by step.\"\"\"\n", - "prompt = PromptTemplate.from_template(template)" + "from langchain_google_vertexai import VertexAI" ] }, { @@ -146,7 +93,7 @@ "metadata": {}, "outputs": [], "source": [ - "chain = prompt | llm" + "model = VertexAI(model_name=\"gemini-pro\")" ] }, { @@ -155,57 +102,39 @@ "metadata": {}, "outputs": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - " Justin Bieber was born on March 1, 1994. Bill Clinton was the president of the United States from January 20, 1993, to January 20, 2001.\n", - "The final answer is Bill Clinton\n" - ] + "data": { + "text/plain": [ + "'**Pros:**\\n\\n* **Easy to learn and use:** Python is known for its simple syntax and readability, making it a great choice for beginners and experienced programmers alike.\\n* **Versatile:** Python can be used for a wide variety of tasks, including web development, data science, machine learning, and scripting.\\n* **Large community:** Python has a large and active community of developers, which means there is a wealth of resources and support available.\\n* **Extensive library support:** Python has a vast collection of libraries and frameworks that can be used to extend its functionality.\\n* **Cross-platform:** Python is available for a'" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "question = \"Who was the president in the year Justin Beiber was born?\"\n", - "print(chain.invoke({\"question\": question}))" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "AV7oXXuHN8Xd" - }, - "source": [ - "## Code generation example" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "3ZzVtF6tN8Xd" - }, - "source": [ - "You can now leverage the `Codey API` for code generation within `Vertex AI`.\n", - "\n", - "The model names are:\n", - "- `code-bison`: for code suggestion\n", - "- `code-gecko`: for code completion" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "llm = VertexAI(model_name=\"code-bison\", max_output_tokens=1000, temperature=0.3)" + "message = \"What are some of the pros and cons of Python as a programming language?\"\n", + "model.invoke(message)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "'**Pros:**\\n\\n* **Easy to learn and use:** Python is known for its simple syntax and readability, making it a great choice for beginners and experienced programmers alike.\\n* **Versatile:** Python can be used for a wide variety of tasks, including web development, data science, machine learning, and scripting.\\n* **Large community:** Python has a large and active community of developers, which means there is a wealth of resources and support available.\\n* **Extensive library support:** Python has a vast collection of libraries and frameworks that can be used to extend its functionality.\\n* **Cross-platform:** Python is available for a'" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "question = \"Write a python function that checks if a string is a valid email address\"" + "await model.ainvoke(message)" ] }, { @@ -217,29 +146,19 @@ "name": "stdout", "output_type": "stream", "text": [ - "```python\n", - "import re\n", + "**Pros:**\n", "\n", - "def is_valid_email(email):\n", - " pattern = re.compile(r\"[^@]+@[^@]+\\.[^@]+\")\n", - " return pattern.match(email)\n", - "```\n" + "* **Easy to learn and use:** Python is known for its simple syntax and readability, making it a great choice for beginners and experienced programmers alike.\n", + "* **Versatile:** Python can be used for a wide variety of tasks, including web development, data science, machine learning, and scripting.\n", + "* **Large community:** Python has a large and active community of developers, which means there is a wealth of resources and support available.\n", + "* **Extensive library support:** Python has a vast collection of libraries and frameworks that can be used to extend its functionality.\n", + "* **Cross-platform:** Python is available for a" ] } ], "source": [ - "print(llm(question))" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "0WqyaSC2N8Xd" - }, - "source": [ - "## Full generation info\n", - "\n", - "We can use the `generate` method to get back extra metadata like [safety attributes](https://cloud.google.com/vertex-ai/docs/generative-ai/learn/responsible-ai#safety_attribute_confidence_scoring) and not just text completions" + "for chunk in model.stream(message):\n", + " print(chunk, end=\"\", flush=True)" ] }, { @@ -250,43 +169,44 @@ { "data": { "text/plain": [ - "[[GenerationChunk(text='```python\\nimport re\\n\\ndef is_valid_email(email):\\n pattern = re.compile(r\"[^@]+@[^@]+\\\\.[^@]+\")\\n return pattern.match(email)\\n```', generation_info={'is_blocked': False, 'safety_attributes': {'Health': 0.1}})]]" + "['**Pros:**\\n\\n* **Easy to learn and use:** Python is known for its simple syntax and readability, making it a great choice for beginners and experienced programmers alike.\\n* **Versatile:** Python can be used for a wide variety of tasks, including web development, data science, machine learning, and scripting.\\n* **Large community:** Python has a large and active community of developers, which means there is a wealth of resources and support available.\\n* **Extensive library support:** Python has a vast collection of libraries and frameworks that can be used to extend its functionality.\\n* **Cross-platform:** Python is available for a']" ] }, - "execution_count": 23, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "result = llm.generate([question])\n", - "result.generations" + "model.batch([message])" ] }, { "cell_type": "markdown", - "metadata": { - "id": "Wd5M4BBUN8Xd" - }, + "metadata": {}, "source": [ - "## Asynchronous calls\n", - "\n", - "With `agenerate` we can make asynchronous calls" + "We can use the `generate` method to get back extra metadata like [safety attributes](https://cloud.google.com/vertex-ai/docs/generative-ai/learn/responsible-ai#safety_attribute_confidence_scoring) and not just text completions." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "[[GenerationChunk(text='**Pros:**\\n\\n* **Easy to learn and use:** Python is known for its simple syntax and readability, making it a great choice for beginners and experienced programmers alike.\\n* **Versatile:** Python can be used for a wide variety of tasks, including web development, data science, machine learning, and scripting.\\n* **Large community:** Python has a large and active community of developers, which means there is a wealth of resources and support available.\\n* **Extensive library support:** Python has a vast collection of libraries and frameworks that can be used to extend its functionality.\\n* **Cross-platform:** Python is available for a')]]" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "# If running in a Jupyter notebook you'll need to install nest_asyncio\n", - "\n", - "%pip install --upgrade --quiet nest_asyncio\n", - "\n", - "import nest_asyncio\n", - "\n", - "nest_asyncio.apply()" + "result = model.generate([message])\n", + "result.generations" ] }, { @@ -297,38 +217,65 @@ { "data": { "text/plain": [ - "LLMResult(generations=[[GenerationChunk(text='```python\\nimport re\\n\\ndef is_valid_email(email):\\n pattern = re.compile(r\"[^@]+@[^@]+\\\\.[^@]+\")\\n return pattern.match(email)\\n```', generation_info={'is_blocked': False, 'safety_attributes': {'Health': 0.1}})]], llm_output=None, run=[RunInfo(run_id=UUID('caf74e91-aefb-48ac-8031-0c505fcbbcc6'))])" + "[[GenerationChunk(text='**Pros:**\\n\\n* **Easy to learn and use:** Python is known for its simple syntax and readability, making it a great choice for beginners and experienced programmers alike.\\n* **Versatile:** Python can be used for a wide variety of tasks, including web development, data science, machine learning, and scripting.\\n* **Large community:** Python has a large and active community of developers, which means there is a wealth of resources and support available.\\n* **Extensive library support:** Python has a vast collection of libraries and frameworks that can be used to extend its functionality.\\n* **Cross-platform:** Python is available for a')]]" ] }, - "execution_count": 25, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "import asyncio\n", - "\n", - "asyncio.run(llm.agenerate([question]))" + "result = await model.agenerate([message])\n", + "result.generations" ] }, { "cell_type": "markdown", - "metadata": { - "id": "VLsy_4bZN8Xd" - }, + "metadata": {}, "source": [ - "## Streaming calls\n", - "\n", - "With `stream` we can stream results from the model" + "You can also easily combine with a prompt template for easy structuring of user input. We can do this using [LCEL](/docs/expression_language)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1. You start with 5 apples.\n", + "2. You throw away 2 apples, so you have 5 - 2 = 3 apples left.\n", + "3. You eat 1 apple, so you have 3 - 1 = 2 apples left.\n", + "\n", + "Therefore, you have 2 apples left.\n" + ] + } + ], + "source": [ + "from langchain_core.prompts import PromptTemplate\n", + "\n", + "template = \"\"\"Question: {question}\n", + "\n", + "Answer: Let's think step by step.\"\"\"\n", + "prompt = PromptTemplate.from_template(template)\n", + "\n", + "chain = prompt | model\n", + "\n", + "question = \"\"\"\n", + "I have five apples. I throw two away. I eat one. How many apples do I have left?\n", + "\"\"\"\n", + "print(chain.invoke({\"question\": question}))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, "source": [ - "import sys" + "You can use different foundational models for specialized in different tasks. \n", + "For an updated list of available models visit [VertexAI documentation](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/overview)" ] }, { @@ -354,49 +301,38 @@ " True if the string is a valid email address, False otherwise.\n", " \"\"\"\n", "\n", - " # Check for a valid email address format.\n", - " if not re.match(r\"^[A-Za-z0-9\\.\\+_-]+@[A-Za-z0-9\\._-]+\\.[a-zA-Z]*$\", email):\n", - " return False\n", - "\n", - " # Check if the domain name exists.\n", - " try:\n", - " domain = email.split(\"@\")[1]\n", - " socket.gethostbyname(domain)\n", - " except socket.gaierror:\n", - " return False\n", + " # Compile the regular expression for an email address.\n", + " regex = re.compile(r\"[^@]+@[^@]+\\.[^@]+\")\n", "\n", - " return True\n", - "```" + " # Check if the string matches the regular expression.\n", + " return regex.match(email) is not None\n", + "```\n" ] } ], "source": [ - "for chunk in llm.stream(question):\n", - " sys.stdout.write(chunk)\n", - " sys.stdout.flush()" + "llm = VertexAI(model_name=\"code-bison\", max_output_tokens=1000, temperature=0.3)\n", + "question = \"Write a python function that checks if a string is a valid email address\"\n", + "print(model.invoke(question))" ] }, { "cell_type": "markdown", - "metadata": { - "id": "4VJ8GwhaN8Xd" - }, + "metadata": {}, "source": [ "## Multimodality" ] }, { "cell_type": "markdown", - "metadata": { - "id": "L7BovARaN8Xe" - }, + "metadata": {}, "source": [ "With Gemini, you can use LLM in a multimodal mode:" ] }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -429,16 +365,14 @@ }, { "cell_type": "markdown", - "metadata": { - "id": "3Vk3gQrrOaL9" - }, + "metadata": {}, "source": [ "Let's double-check it's a cat :)" ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -448,7 +382,7 @@ "" ] }, - "execution_count": 9, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -462,16 +396,14 @@ }, { "cell_type": "markdown", - "metadata": { - "id": "1uEACSSm8AL2" - }, + "metadata": {}, "source": [ "You can also pass images as bytes:" ] }, { "cell_type": "code", - "execution_count": 18, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -506,18 +438,14 @@ }, { "cell_type": "markdown", - "metadata": { - "id": "AuhF5WQuN8Xe" - }, + "metadata": {}, "source": [ "Please, note that you can also use the image stored in GCS (just point the `url` to the full GCS path, starting with `gs://` instead of a local one)." ] }, { "cell_type": "markdown", - "metadata": { - "id": "qaC2UmxS9WtB" - }, + "metadata": {}, "source": [ "And you can also pass a history of a previous chat to the LLM:" ] @@ -564,18 +492,14 @@ }, { "cell_type": "markdown", - "metadata": { - "id": "VEYAfdBpN8Xe" - }, + "metadata": {}, "source": [ "## Vertex Model Garden" ] }, { "cell_type": "markdown", - "metadata": { - "id": "N3ptjr_LN8Xe" - }, + "metadata": {}, "source": [ "Vertex Model Garden [exposes](https://cloud.google.com/vertex-ai/docs/start/explore-models) open-sourced models that can be deployed and served on Vertex AI. If you have successfully deployed a model from Vertex Model Garden, you can find a corresponding Vertex AI [endpoint](https://cloud.google.com/vertex-ai/docs/general/deployment#what_happens_when_you_deploy_a_model) in the console or via API." ] @@ -604,14 +528,12 @@ "metadata": {}, "outputs": [], "source": [ - "print(llm(\"What is the meaning of life?\"))" + "llm.invoke(\"What is the meaning of life?\")" ] }, { "cell_type": "markdown", - "metadata": { - "id": "TDXoFZ6YN8Xe" - }, + "metadata": {}, "source": [ "Like all LLMs, we can then compose it with other components:" ] @@ -643,8 +565,16 @@ "name": "python3" }, "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", "name": "python", - "version": "3.11.4" + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.6" } }, "nbformat": 4, diff --git a/docs/docs/integrations/llms/gradient.ipynb b/docs/docs/integrations/llms/gradient.ipynb index 9dff7f01e1c00..8d46fa089684c 100644 --- a/docs/docs/integrations/llms/gradient.ipynb +++ b/docs/docs/integrations/llms/gradient.ipynb @@ -59,7 +59,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Optional: Validate your Enviroment variables ```GRADIENT_ACCESS_TOKEN``` and ```GRADIENT_WORKSPACE_ID``` to get currently deployed models. Using the `gradientai` Python package." + "Optional: Validate your Environment variables ```GRADIENT_ACCESS_TOKEN``` and ```GRADIENT_WORKSPACE_ID``` to get currently deployed models. Using the `gradientai` Python package." ] }, { diff --git a/docs/docs/integrations/llms/konko.ipynb b/docs/docs/integrations/llms/konko.ipynb new file mode 100644 index 0000000000000..4ef5a7a3281c4 --- /dev/null +++ b/docs/docs/integrations/llms/konko.ipynb @@ -0,0 +1,100 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "136d9ba6-c42a-435b-9e19-77ebcc7a3145", + "metadata": {}, + "source": [ + "# ChatKonko\n", + "\n", + ">[Konko](https://www.konko.ai/) API is a fully managed Web API designed to help application developers:\n", + "\n", + "Konko API is a fully managed API designed to help application developers:\n", + "\n", + "1. Select the right LLM(s) for their application\n", + "2. Prototype with various open-source and proprietary LLMs\n", + "3. Access Fine Tuning for open-source LLMs to get industry-leading performance at a fraction of the cost\n", + "4. Setup low-cost production APIs according to security, privacy, throughput, latency SLAs without infrastructure set-up or administration using Konko AI's SOC 2 compliant, multi-cloud infrastructure\n" + ] + }, + { + "cell_type": "markdown", + "id": "0d896d07-82b4-4f38-8c37-f0bc8b0e4fe1", + "metadata": {}, + "source": [ + "### Steps to Access Models\n", + "1. **Explore Available Models:** Start by browsing through the [available models](https://docs.konko.ai/docs/list-of-models) on Konko. Each model caters to different use cases and capabilities.\n", + "\n", + "2. **Identify Suitable Endpoints:** Determine which [endpoint](https://docs.konko.ai/docs/list-of-models#list-of-available-models) (ChatCompletion or Completion) supports your selected model.\n", + "\n", + "3. **Selecting a Model:** [Choose a model](https://docs.konko.ai/docs/list-of-models#list-of-available-models) based on its metadata and how well it fits your use case.\n", + "\n", + "4. **Prompting Guidelines:** Once a model is selected, refer to the [prompting guidelines](https://docs.konko.ai/docs/prompting) to effectively communicate with it.\n", + "\n", + "5. **Using the API:** Finally, use the appropriate Konko [API endpoint](https://docs.konko.ai/docs/quickstart-for-completion-and-chat-completion-endpoint) to call the model and receive responses.\n", + "\n", + "This example goes over how to use LangChain to interact with `Konko` completion [models](https://docs.konko.ai/docs/list-of-models#konko-hosted-models-for-completion)\n", + "\n", + "To run this notebook, you'll need Konko API key. You can create one by signing up on [Konko](https://www.konko.ai/)." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "dd70bccb-7a65-42d0-a3f2-8116f3549da7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "Answer:\n", + "The Big Bang Theory is a theory that explains the origin of the universe. According to the theory, the universe began with a single point of infinite density and temperature. This point is called the singularity. The singularity exploded and expanded rapidly. The expansion of the universe is still continuing.\n", + "The Big Bang Theory is a theory that explains the origin of the universe. According to the theory, the universe began with a single point of infinite density and temperature. This point is called the singularity. The singularity exploded and expanded rapidly. The expansion of the universe is still continuing.\n", + "\n", + "Question\n" + ] + } + ], + "source": [ + "from langchain.llms import Konko\n", + "\n", + "llm = Konko(model=\"mistralai/mistral-7b-v0.1\", temperature=0.1, max_tokens=128)\n", + "\n", + "input_ = \"\"\"You are a helpful assistant. Explain Big Bang Theory briefly.\"\"\"\n", + "print(llm(input_))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "78148bf7-2211-40b4-93a7-e90139ab1169", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "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.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/docs/integrations/llms/llamacpp.ipynb b/docs/docs/integrations/llms/llamacpp.ipynb index c433baf1d027c..853787fc198a6 100644 --- a/docs/docs/integrations/llms/llamacpp.ipynb +++ b/docs/docs/integrations/llms/llamacpp.ipynb @@ -316,7 +316,7 @@ "prompt = \"\"\"\n", "Question: A rap battle between Stephen Colbert and John Oliver\n", "\"\"\"\n", - "llm(prompt)" + "llm.invoke(prompt)" ] }, { @@ -618,7 +618,7 @@ ], "source": [ "%%capture captured --no-stdout\n", - "result = llm(\"Describe a person in JSON format:\")" + "result = llm.invoke(\"Describe a person in JSON format:\")" ] }, { @@ -674,7 +674,7 @@ ], "source": [ "%%capture captured --no-stdout\n", - "result = llm(\"List of top-3 my favourite books:\")" + "result = llm.invoke(\"List of top-3 my favourite books:\")" ] } ], diff --git a/docs/docs/integrations/llms/llm_caching.ipynb b/docs/docs/integrations/llms/llm_caching.ipynb index 1bba907163b39..791ff870b0fda 100644 --- a/docs/docs/integrations/llms/llm_caching.ipynb +++ b/docs/docs/integrations/llms/llm_caching.ipynb @@ -318,7 +318,7 @@ "metadata": {}, "source": [ "### Standard Cache\n", - "Use [Redis](/docs/integrations/partners/redis) to cache prompts and responses." + "Use [Redis](/docs/integrations/providers/redis) to cache prompts and responses." ] }, { @@ -404,7 +404,7 @@ "metadata": {}, "source": [ "### Semantic Cache\n", - "Use [Redis](/docs/integrations/partners/redis) to cache prompts and responses and evaluate hits based on semantic similarity." + "Use [Redis](/docs/integrations/providers/redis) to cache prompts and responses and evaluate hits based on semantic similarity." ] }, { @@ -728,7 +728,7 @@ }, "source": [ "## `Momento` Cache\n", - "Use [Momento](/docs/integrations/partners/momento) to cache prompts and responses.\n", + "Use [Momento](/docs/integrations/providers/momento) to cache prompts and responses.\n", "\n", "Requires momento to use, uncomment below to install:" ] diff --git a/docs/docs/integrations/llms/ollama.ipynb b/docs/docs/integrations/llms/ollama.ipynb index 9637c965ccd6f..79ea75898789d 100644 --- a/docs/docs/integrations/llms/ollama.ipynb +++ b/docs/docs/integrations/llms/ollama.ipynb @@ -18,84 +18,164 @@ "\n", "First, follow [these instructions](https://github.com/jmorganca/ollama) to set up and run a local Ollama instance:\n", "\n", - "* [Download](https://ollama.ai/download)\n", - "* Fetch a model via `ollama pull `\n", - "* e.g., for `Llama-7b`: `ollama pull llama2` (see full list [here](https://ollama.ai/library)\n", - "* This will download the most basic version of the model typically (e.g., smallest # parameters)\n", - "* On Mac, it will download to \n", + "* [Download](https://ollama.ai/download) and install Ollama onto the available supported platforms (including Windows Subsystem for Linux)\n", + "* Fetch available LLM model via `ollama pull `\n", + " * View a list of available models via the [model library](https://ollama.ai/library)\n", + " * e.g., for `Llama-7b`: `ollama pull llama2`\n", + "* This will download the default tagged version of the model. Typically, the default points to the latest, smallest sized-parameter model.\n", "\n", - "`~/.ollama/models/manifests/registry.ollama.ai/library//latest`\n", + "> On Mac, the models will be download to `~/.ollama/models`\n", + "> \n", + "> On Linux (or WSL), the models will be stored at `/usr/share/ollama/.ollama/models`\n", "\n", - "* And we specify a particular version, e.g., for `ollama pull vicuna:13b-v1.5-16k-q4_0`\n", - "* The file is here with the model version in place of `latest`\n", + "* Specify the exact version of the model of interest as such `ollama pull vicuna:13b-v1.5-16k-q4_0` (View the [various tags for the `Vicuna`](https://ollama.ai/library/vicuna/tags) model in this instance)\n", + "* To view all pulled models, use `ollama list`\n", + "* To chat directly with a model from the command line, use `ollama run `\n", + "* View the [Ollama documentation](https://github.com/jmorganca/ollama) for more commands. Run `ollama help` in the terminal to see available commands too.\n", "\n", - "`~/.ollama/models/manifests/registry.ollama.ai/library/vicuna/13b-v1.5-16k-q4_0`\n", + "## Usage\n", "\n", - "You can easily access models in a few ways:\n", + "You can see a full list of supported parameters on the [API reference page](https://api.python.langchain.com/en/latest/llms/langchain.llms.ollama.Ollama.html).\n", "\n", - "1/ if the app is running:\n", + "If you are using a LLaMA `chat` model (e.g., `ollama pull llama2:7b-chat`) then you can use the `ChatOllama` interface.\n", "\n", - "* All of your local models are automatically served on `localhost:11434`\n", - "* Select your model when setting `llm = Ollama(..., model=\":\")`\n", - "* If you set `llm = Ollama(..., model=\"` to start interacting via the command line directly\n", "\n", + "### via an API\n", "\n", - "## Usage\n", + "Send an `application/json` request to the API endpoint of Ollama to interact.\n", "\n", - "You can see a full list of supported parameters on the [API reference page](https://api.python.langchain.com/en/latest/llms/langchain.llms.ollama.Ollama.html)." + "```bash\n", + "curl http://localhost:11434/api/generate -d '{\n", + " \"model\": \"llama2\",\n", + " \"prompt\":\"Why is the sky blue?\"\n", + "}'\n", + "```\n", + "\n", + "See the Ollama [API documentation](https://github.com/jmorganca/ollama/blob/main/docs/api.md) for all endpoints.\n", + "\n", + "#### via LangChain\n", + "\n", + "See a typical basic example of using Ollama chat model in your LangChain application." ] }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 2, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "\"Sure! Here's a quick one:\\n\\nWhy don't scientists trust atoms?\\nBecause they make up everything!\\n\\nI hope that brought a smile to your face!\"" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "from langchain.callbacks.manager import CallbackManager\n", - "from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler\n", "from langchain_community.llms import Ollama\n", "\n", - "llm = Ollama(model=\"llama2\")" + "llm = Ollama(model=\"llama2\")\n", + "\n", + "llm.invoke(\"Tell me a joke\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Optionally, pass `StreamingStdOutCallbackHandler` to stream tokens:\n", - "\n", - "```\n", - "llm = Ollama(\n", - " model=\"llama2\",\n", - " callback_manager=CallbackManager([StreamingStdOutCallbackHandler()]),\n", - ")\n", - "```" + "To stream tokens, use the `.stream(...)` method:" ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 4, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "' Artificial intelligence (AI) has a rich and varied history that spans several decades. październik 1950s and has evolved significantly over time. Here is a brief overview of the major milestones in the history of AI:\\n\\n1. 1950s: The Dartmouth Conference - Considered the birthplace of AI, this conference brought together computer scientists, mathematicians, and cognitive scientists to discuss the possibilities of creating machines that could simulate human intelligence. Attendees included John McCarthy, Marvin Minsky, Nathaniel Rochester, and Claude Shannon.\\n2. 1951: The Turing Test - Alan Turing proposed a test to measure a machine\\'s ability to exhibit intelligent behavior equivalent to, or indistinguishable from, that of a human. The Turing Test has since become a benchmark for measuring the success of AI systems.\\n3. 1956: The First AI Program - John McCarthy created the first AI program, called the Logical Theorist, which was designed to reason and solve problems using logical deduction.\\n4. 1960s: Rule-Based Expert Systems - Researchers developed rule-based expert systems, which used a set of rules to reason and make decisions. These systems were widely used in industries such as banking and healthcare.\\n5. 1970s: Machine Learning -Machine learning, a subfield of AI, emerged as a way for machines to learn from data without being explicitly programmed. This led to the development of algorithms such as decision trees and neural networks.\\n6. 1980s: Expert Systems - The development of expert systems, which were designed to mimic the decision-making abilities of human experts, reached its peak in the 1980s. These systems were widely used in industries such as banking and healthcare.\\n7. 1990s: AI Winter - Despite the progress made in AI research, the field experienced a decline in funding and interest in the 1990s, known as the \"AI winter.\"\\n8. 2000s: AI Resurgence - The resurgence of AI began in the early 2000s with the development of new algorithms and techniques, such as support vector machines and deep learning. This led to a renewed interest in AI research and applications.\\n9. 2010s: Rise of Deep Learning - The development of deep learning algorithms, which are capable of learning and improving on their own by analyzing large amounts of data, has been a major factor in the recent progress made in AI. These algorithms have been used in applications such as image recognition, natural language processing, and autonomous vehicles.\\n10. Present Day: AI Continues to Advance - AI is continuing to advance at a rapid pace, with new techniques and applications emerging all the time. Areas of research include natural language processing, computer vision, robotics, and more.\\n\\nSome notable people who have made significant contributions to the field of AI include:\\n\\n1. Alan Turing - Considered one of the pioneers of AI, Turing proposed the Turing Test and developed the concept of a universal machine.\\n2. John McCarthy - McCarthy is known as the \"father of AI\" for his work in developing the field of AI. He coined the term \"Artificial Intelligence\" and was instrumental in organizing the Dartmouth Conference.\\n3. Marvin Minsky - Minsky was a pioneer in the field of neural networks and co-founder of the MIT AI Laboratory.\\n4. Nathaniel Rochester - Rochester was a computer scientist and cognitive scientist who worked on early AI projects, including the development of the Logical Theorist.\\n5. Claude Shannon - Shannon was a mathematician and electrical engineer who is known for his work on information theory, which has had a significant impact on the field of AI.\\n6. Yann LeCun - LeCun is a computer scientist and the director of AI Research at Facebook. He is also the Silver Professor of Computer Science at New York University, and a professor at the Courant Institute of Mathematical Sciences.\\n7. Geoffrey Hinton - Hinton is a computer scientist and cognitive psychologist who is known for his work on artificial neural networks. He is a pioneer in the field of deep learning and has made significant contributions to the development of convolutional neural networks (CNNs).\\n8. Yoshua Bengio - Bengio is a computer scientist and a pioneer in the field of deep learning. He is known for his work on recurrent neural networks (RNNs) and has made significant contributions to the development of CNNs and RNNs.\\n9. Andrew Ng - Ng is a computer scientist and entrepreneur who has made significant contributions to the field of AI. He is known for his work on deep learning and has worked at Google, where he founded the Google Brain deep learning project, and at Baidu, where he led the company\\'s AI group.\\n10. Demis Hassabis - Hassabis is a computer scientist and entrepreneur who is known for his work on deep learning and artificial intelligence. He is the co-founder of DeepMind, which was acquired by Alphabet in 2014, and has made significant contributions to the field of AI.\\n\\nThese are just a few examples of notable people who have made significant contributions to the field of AI. There are many other researchers and scientists who have also made important advancements in the field.'" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "S\n", + "ure\n", + ",\n", + " here\n", + "'\n", + "s\n", + " one\n", + ":\n", + "\n", + "\n", + "\n", + "\n", + "Why\n", + " don\n", + "'\n", + "t\n", + " scient\n", + "ists\n", + " trust\n", + " atoms\n", + "?\n", + "\n", + "\n", + "B\n", + "ecause\n", + " they\n", + " make\n", + " up\n", + " everything\n", + "!\n", + "\n", + "\n", + "\n", + "\n", + "I\n", + " hope\n", + " you\n", + " found\n", + " that\n", + " am\n", + "using\n", + "!\n", + " Do\n", + " you\n", + " want\n", + " to\n", + " hear\n", + " another\n", + " one\n", + "?\n", + "\n" + ] } ], "source": [ - "llm(\"Tell me about the history of AI\")" + "query = \"Tell me a joke\"\n", + "\n", + "for chunks in llm.stream(query):\n", + " print(chunks)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To learn more about the LangChain Expressive Language and the available methods on an LLM, see the [LCEL Interface](https://python.langchain.com/docs/expression_language/interface)" ] }, { @@ -106,16 +186,14 @@ "\n", "Ollama has support for multi-modal LLMs, such as [bakllava](https://ollama.ai/library/bakllava) and [llava](https://ollama.ai/library/llava).\n", "\n", - "```\n", - "ollama pull bakllava\n", - "```\n", + "`ollama pull bakllava`\n", "\n", "Be sure to update Ollama so that you have the most recent version to support multi-modal." ] }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ @@ -132,7 +210,7 @@ { "data": { "text/html": [ - "" + "" ], "text/plain": [ "" @@ -176,7 +254,7 @@ " display(HTML(image_html))\n", "\n", "\n", - "file_path = \"/Users/rlm/Desktop/Eval_Sets/multi_modal_presentations/DDOG/img_23.jpg\"\n", + "file_path = \"../../../static/img/ollama_example_img.jpg\"\n", "pil_image = Image.open(file_path)\n", "image_b64 = convert_to_base64(pil_image)\n", "plt_img_base64(image_b64)" @@ -184,7 +262,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 8, "metadata": {}, "outputs": [ { @@ -193,7 +271,7 @@ "'90%'" ] }, - "execution_count": 5, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } @@ -220,7 +298,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.1" + "version": "3.9.1" } }, "nbformat": 4, diff --git a/docs/docs/integrations/llms/watsonxllm.ipynb b/docs/docs/integrations/llms/watsonxllm.ipynb index be5d0841cd2e2..f0b142cf96d46 100644 --- a/docs/docs/integrations/llms/watsonxllm.ipynb +++ b/docs/docs/integrations/llms/watsonxllm.ipynb @@ -176,7 +176,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 3, "id": "c7d80c05", "metadata": {}, "outputs": [], @@ -197,17 +197,18 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 4, "id": "dc076c56", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "'How many breeds of dog are there?'" + "{'topic': 'dog',\n", + " 'text': 'What is the name of the dog that is the most popular in the world?'}" ] }, - "execution_count": 5, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } @@ -216,7 +217,7 @@ "from langchain.chains import LLMChain\n", "\n", "llm_chain = LLMChain(prompt=prompt, llm=watsonx_llm)\n", - "llm_chain.run(\"dog\")" + "llm_chain.invoke(\"dog\")" ] }, { @@ -248,7 +249,7 @@ "source": [ "# Calling a single prompt\n", "\n", - "watsonx_llm(\"Who is man's best friend?\")" + "watsonx_llm.invoke(\"Who is man's best friend?\")" ] }, { @@ -327,7 +328,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.18" + "version": "3.10.13" } }, "nbformat": 4, diff --git a/docs/docs/integrations/memory/firestore_chat_message_history.ipynb b/docs/docs/integrations/memory/firestore_chat_message_history.ipynb new file mode 100644 index 0000000000000..8bdd586cd4896 --- /dev/null +++ b/docs/docs/integrations/memory/firestore_chat_message_history.ipynb @@ -0,0 +1,147 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "91c6a7ef", + "metadata": {}, + "source": [ + "# Google Cloud Firestore\n", + "\n", + "> [`Cloud Firestore`](https://cloud.google.com/firestore) is a NoSQL document database built for automatic scaling, high performance, and ease of application development.\n", + "\n", + "This notebook goes over how to use Firestore to store chat message history." + ] + }, + { + "cell_type": "markdown", + "id": "2d6ed3c8-b70a-498c-bc9e-41b91797d3b7", + "metadata": {}, + "source": [ + "## Setting up" + ] + }, + { + "cell_type": "markdown", + "id": "b8eca282", + "metadata": {}, + "source": [ + "To run this notebook, you will need a Google Cloud Project, a Firestore database instance in Native Mode, and Google credentials, see [Firestore Quickstarts](https://cloud.google.com/firestore/docs/quickstarts)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5a7f3b3f-d9b8-4577-a7ef-bdd8ecaedb70", + "metadata": {}, + "outputs": [], + "source": [ + "!pip install firebase-admin" + ] + }, + { + "cell_type": "markdown", + "id": "a8e63850-3e14-46fe-a59e-be6d6bf8fe61", + "metadata": {}, + "source": [ + "## Basic Usage" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "d15e3302", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_community.chat_message_histories.firestore import (\n", + " FirestoreChatMessageHistory,\n", + ")\n", + "\n", + "message_history = FirestoreChatMessageHistory(\n", + " collection_name=\"langchain-chat-history\",\n", + " session_id=\"user-session-id\",\n", + " user_id=\"user-id\",\n", + ")\n", + "\n", + "message_history.add_user_message(\"hi!\")\n", + "message_history.add_ai_message(\"whats up?\")" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "64fc465e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[HumanMessage(content='hi!'),\n", + " HumanMessage(content='hi!'),\n", + " AIMessage(content='whats up?')]" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "message_history.messages" + ] + }, + { + "cell_type": "markdown", + "id": "4be8576e", + "metadata": {}, + "source": [ + "## Custom Firestore Client" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12999273", + "metadata": {}, + "outputs": [], + "source": [ + "import firebase_admin\n", + "from firebase_admin import credentials, firestore\n", + "\n", + "# Use a service account.\n", + "cred = credentials.Certificate(\"path/to/serviceAccount.json\")\n", + "\n", + "app = firebase_admin.initialize_app(cred)\n", + "client = firestore.client(app=app)\n", + "\n", + "message_history = FirestoreChatMessageHistory(\n", + " collection_name=\"langchain-chat-history\",\n", + " session_id=\"user-session-id\",\n", + " user_id=\"user-id\",\n", + " firestore_client=client,\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.5" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/docs/integrations/memory/mongodb_chat_message_history.ipynb b/docs/docs/integrations/memory/mongodb_chat_message_history.ipynb index 4e01086b7a9ab..8356154fcda4c 100644 --- a/docs/docs/integrations/memory/mongodb_chat_message_history.ipynb +++ b/docs/docs/integrations/memory/mongodb_chat_message_history.ipynb @@ -11,7 +11,7 @@ ">\n", ">`MongoDB` is developed by MongoDB Inc. and licensed under the Server Side Public License (SSPL). - [Wikipedia](https://en.wikipedia.org/wiki/MongoDB)\n", "\n", - "This notebook goes over how to use Mongodb to store chat message history.\n" + "This notebook goes over how to use the `MongoDBChatMessageHistory` class to store chat message history in a Mongodb database.\n" ] }, { @@ -19,76 +19,230 @@ "id": "2d6ed3c8-b70a-498c-bc9e-41b91797d3b7", "metadata": {}, "source": [ - "## Setting up" + "## Setup\n", + "\n", + "The integration lives in the `langchain-community` package, so we need to install that. We also need to install the `pymongo` package.\n", + "\n", + "```bash\n", + "pip install -U --quiet langchain-community pymongo\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "09c33ad3-9ab1-48b5-bead-9a44f3d86eeb", + "metadata": {}, + "source": [ + "It's also helpful (but not needed) to set up [LangSmith](https://smith.langchain.com/) for best-in-class observability" ] }, { "cell_type": "code", "execution_count": null, - "id": "5a7f3b3f-d9b8-4577-a7ef-bdd8ecaedb70", + "id": "0976204d-c681-4288-bfe5-a550e0340f35", "metadata": {}, "outputs": [], "source": [ - "%pip install --upgrade --quiet pymongo" + "# os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "# os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass()" + ] + }, + { + "cell_type": "markdown", + "id": "71a0a5aa-8f12-462a-bcd0-c611d76566f8", + "metadata": {}, + "source": [ + "## Usage\n", + "\n", + "To use the storage you need to provide only 2 things:\n", + "\n", + "1. Session Id - a unique identifier of the session, like user name, email, chat id etc.\n", + "2. Connection string - a string that specifies the database connection. It will be passed to MongoDB create_engine function.\n", + "\n", + "If you want to customize where the chat histories go, you can also pass:\n", + "1. *database_name* - name of the database to use\n", + "1. *collection_name* - collection to use within that database" ] }, { "cell_type": "code", "execution_count": 3, - "id": "47a601d2", - "metadata": {}, + "id": "0179847d-76b6-43bc-b15c-7fecfcb27ac7", + "metadata": { + "ExecuteTime": { + "end_time": "2023-08-28T10:04:38.077748Z", + "start_time": "2023-08-28T10:04:36.105894Z" + }, + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "outputs": [], "source": [ - "# Provide the connection string to connect to the MongoDB database\n", - "connection_string = \"mongodb://mongo_user:password123@mongo:27017\"" + "from langchain_community.chat_message_histories import MongoDBChatMessageHistory\n", + "\n", + "chat_message_history = MongoDBChatMessageHistory(\n", + " session_id=\"test_session\",\n", + " connection_string=\"mongodb://mongo_user:password123@mongo:27017\",\n", + " database_name=\"my_db\",\n", + " collection_name=\"chat_histories\",\n", + ")\n", + "\n", + "chat_message_history.add_user_message(\"Hello\")\n", + "chat_message_history.add_ai_message(\"Hi\")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "6e7b8653-a8d2-49a7-97ba-4296f7e717e9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[HumanMessage(content='Hello'), AIMessage(content='Hi')]" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "chat_message_history.messages" ] }, { "cell_type": "markdown", - "id": "a8e63850-3e14-46fe-a59e-be6d6bf8fe61", + "id": "e352d786-0811-48ec-832a-9f1c0b70690e", "metadata": {}, "source": [ - "## Example" + "## Chaining\n", + "\n", + "We can easily combine this message history class with [LCEL Runnables](/docs/expression_language/how_to/message_history)\n", + "\n", + "To do this we will want to use OpenAI, so we need to install that. You will also need to set the OPENAI_API_KEY environment variable to your OpenAI key.\n" ] }, { "cell_type": "code", - "execution_count": 4, - "id": "d15e3302", + "execution_count": 5, + "id": "6558418b-0ece-4d01-9661-56d562d78f7a", "metadata": {}, "outputs": [], "source": [ - "from langchain.memory import MongoDBChatMessageHistory\n", + "from typing import Optional\n", "\n", - "message_history = MongoDBChatMessageHistory(\n", - " connection_string=connection_string, session_id=\"test-session\"\n", - ")\n", + "from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n", + "from langchain_core.runnables.history import RunnableWithMessageHistory\n", + "from langchain_openai import ChatOpenAI" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "86ddfd3f-e8cf-477a-a7fd-91be3b8aa928", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", "\n", - "message_history.add_user_message(\"hi!\")\n", + "assert os.environ[\n", + " \"OPENAI_API_KEY\"\n", + "], \"Set the OPENAI_API_KEY environment variable with your OpenAI API key.\"" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "82149122-61d3-490d-9bdb-bb98606e8ba1", + "metadata": {}, + "outputs": [], + "source": [ + "prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\"system\", \"You are a helpful assistant.\"),\n", + " MessagesPlaceholder(variable_name=\"history\"),\n", + " (\"human\", \"{question}\"),\n", + " ]\n", + ")\n", "\n", - "message_history.add_ai_message(\"whats up?\")" + "chain = prompt | ChatOpenAI()" ] }, { "cell_type": "code", - "execution_count": 5, - "id": "64fc465e", + "execution_count": 11, + "id": "2df90853-b67c-490f-b7f8-b69d69270b9c", + "metadata": {}, + "outputs": [], + "source": [ + "chain_with_history = RunnableWithMessageHistory(\n", + " chain,\n", + " lambda session_id: MongoDBChatMessageHistory(\n", + " session_id=\"test_session\",\n", + " connection_string=\"mongodb://mongo_user:password123@mongo:27017\",\n", + " database_name=\"my_db\",\n", + " collection_name=\"chat_histories\",\n", + " ),\n", + " input_messages_key=\"question\",\n", + " history_messages_key=\"history\",\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "0ce596b8-3b78-48fd-9f92-46dccbbfd58b", + "metadata": {}, + "outputs": [], + "source": [ + "# This is where we configure the session id\n", + "config = {\"configurable\": {\"session_id\": \"\"}}" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "38e1423b-ba86-4496-9151-25932fab1a8b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "AIMessage(content='Hi Bob! How can I assist you today?')" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "chain_with_history.invoke({\"question\": \"Hi! I'm bob\"}, config=config)" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "2ee4ee62-a216-4fb1-bf33-57476a84cf16", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[HumanMessage(content='hi!', additional_kwargs={}, example=False),\n", - " AIMessage(content='whats up?', additional_kwargs={}, example=False)]" + "AIMessage(content='Your name is Bob. Is there anything else I can help you with, Bob?')" ] }, - "execution_count": 5, + "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "message_history.messages" + "chain_with_history.invoke({\"question\": \"Whats my name\"}, config=config)" ] } ], diff --git a/docs/docs/integrations/memory/redis_chat_message_history.ipynb b/docs/docs/integrations/memory/redis_chat_message_history.ipynb index 0b68c886414b1..3e79998e729bd 100644 --- a/docs/docs/integrations/memory/redis_chat_message_history.ipynb +++ b/docs/docs/integrations/memory/redis_chat_message_history.ipynb @@ -139,7 +139,9 @@ "\n", "chain_with_history = RunnableWithMessageHistory(\n", " chain,\n", - " RedisChatMessageHistory,\n", + " lambda session_id: RedisChatMessageHistory(\n", + " session_id, url=\"redis://localhost:6379\"\n", + " ),\n", " input_messages_key=\"question\",\n", " history_messages_key=\"history\",\n", ")\n", diff --git a/docs/docs/integrations/memory/sqlite.ipynb b/docs/docs/integrations/memory/sqlite.ipynb index d25dbeefffe26..200335cc4df4e 100644 --- a/docs/docs/integrations/memory/sqlite.ipynb +++ b/docs/docs/integrations/memory/sqlite.ipynb @@ -16,172 +16,203 @@ }, { "cell_type": "code", - "execution_count": 1, - "id": "d0a07a30-028f-4e16-8b11-45b2416f7b0f", + "execution_count": null, + "id": "5c923f56-24a9-4f8f-9b91-138cc025c47e", "metadata": {}, "outputs": [], "source": [ - "%pip install --upgrade --quiet sqlite3" + "# os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "# os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass()" ] }, { - "cell_type": "code", - "execution_count": 1, - "id": "db59b901", - "metadata": { - "id": "2wUMSUoF8ffn" - }, - "outputs": [], + "cell_type": "markdown", + "id": "61fda020-23a2-4605-afad-58260535ec8c", + "metadata": {}, "source": [ - "from langchain.chains import ConversationChain\n", - "from langchain.memory import ConversationEntityMemory\n", - "from langchain.memory.entity import SQLiteEntityStore\n", - "from langchain.memory.prompt import ENTITY_MEMORY_CONVERSATION_TEMPLATE\n", - "from langchain_openai import OpenAI" + "## Usage\n", + "\n", + "To use the storage you need to provide only 2 things:\n", + "\n", + "1. Session Id - a unique identifier of the session, like user name, email, chat id etc.\n", + "2. Connection string - a string that specifies the database connection. For SQLite, that string is `slqlite:///` followed by the name of the database file. If that file doesn't exist, it will be created." ] }, { "cell_type": "code", - "execution_count": 2, - "id": "ca6dee29", + "execution_count": 1, + "id": "4576e914a866fb40", "metadata": { - "id": "8TpJZti99gxV" + "ExecuteTime": { + "end_time": "2023-08-28T10:04:38.077748Z", + "start_time": "2023-08-28T10:04:36.105894Z" + }, + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [], "source": [ - "entity_store = SQLiteEntityStore()\n", - "llm = OpenAI(temperature=0)\n", - "memory = ConversationEntityMemory(llm=llm, entity_store=entity_store)\n", - "conversation = ConversationChain(\n", - " llm=llm,\n", - " prompt=ENTITY_MEMORY_CONVERSATION_TEMPLATE,\n", - " memory=memory,\n", - " verbose=True,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "f9b4c3a0", - "metadata": { - "id": "HEAHG1L79ca1" - }, - "source": [ - "Notice the usage of `EntitySqliteStore` as parameter to `entity_store` on the `memory` property." + "from langchain_community.chat_message_histories import SQLChatMessageHistory\n", + "\n", + "chat_message_history = SQLChatMessageHistory(\n", + " session_id=\"test_session_id\", connection_string=\"sqlite:///sqlite.db\"\n", + ")\n", + "\n", + "chat_message_history.add_user_message(\"Hello\")\n", + "chat_message_history.add_ai_message(\"Hi\")" ] }, { "cell_type": "code", - "execution_count": 3, - "id": "297e78a6", + "execution_count": 2, + "id": "b476688cbb32ba90", "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 437 + "ExecuteTime": { + "end_time": "2023-08-28T10:04:38.929396Z", + "start_time": "2023-08-28T10:04:38.915727Z" }, - "id": "BzXphJWf_TAZ", - "outputId": "de7fc966-e0fd-4daf-a9bd-4743455ea774" + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "\n", - "\u001b[1m> Entering new ConversationChain chain...\u001b[0m\n", - "Prompt after formatting:\n", - "\u001b[32;1m\u001b[1;3mYou are an assistant to a human, powered by a large language model trained by OpenAI.\n", - "\n", - "You are designed to be able to assist with a wide range of tasks, from answering simple questions to providing in-depth explanations and discussions on a wide range of topics. As a language model, you are able to generate human-like text based on the input you receive, allowing you to engage in natural-sounding conversations and provide responses that are coherent and relevant to the topic at hand.\n", - "\n", - "You are constantly learning and improving, and your capabilities are constantly evolving. You are able to process and understand large amounts of text, and can use this knowledge to provide accurate and informative responses to a wide range of questions. You have access to some personalized information provided by the human in the Context section below. Additionally, you are able to generate your own text based on the input you receive, allowing you to engage in discussions and provide explanations and descriptions on a wide range of topics.\n", - "\n", - "Overall, you are a powerful tool that can help with a wide range of tasks and provide valuable insights and information on a wide range of topics. Whether the human needs help with a specific question or just wants to have a conversation about a particular topic, you are here to assist.\n", - "\n", - "Context:\n", - "{'Deven': 'Deven is working on a hackathon project with Sam.', 'Sam': 'Sam is working on a hackathon project with Deven.'}\n", - "\n", - "Current conversation:\n", - "\n", - "Last line:\n", - "Human: Deven & Sam are working on a hackathon project\n", - "You:\u001b[0m\n", - "\n", - "\u001b[1m> Finished chain.\u001b[0m\n" - ] - }, { "data": { "text/plain": [ - "' That sounds like a great project! What kind of project are they working on?'" + "[HumanMessage(content='Hello'), AIMessage(content='Hi')]" ] }, - "execution_count": 3, + "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "conversation.run(\"Deven & Sam are working on a hackathon project\")" + "chat_message_history.messages" + ] + }, + { + "cell_type": "markdown", + "id": "e400509a-1957-4d1d-bbd6-01e8dc3dccb3", + "metadata": {}, + "source": [ + "## Chaining\n", + "\n", + "We can easily combine this message history class with [LCEL Runnables](/docs/expression_language/how_to/message_history)\n", + "\n", + "To do this we will want to use OpenAI, so we need to install that. We will also need to set the OPENAI_API_KEY environment variable to your OpenAI key.\n", + "\n", + "```bash\n", + "pip install -U langchain-openai\n", + "\n", + "export OPENAI_API_KEY='sk-xxxxxxx'\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "6558418b-0ece-4d01-9661-56d562d78f7a", + "metadata": {}, + "outputs": [], + "source": [ + "from typing import Optional\n", + "\n", + "from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n", + "from langchain_core.runnables.history import RunnableWithMessageHistory\n", + "from langchain_openai import ChatOpenAI" ] }, { "cell_type": "code", "execution_count": 4, - "id": "7e71f1dc", - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 35 - }, - "id": "YsFE3hBjC6gl", - "outputId": "56ab5ca9-e343-41b5-e69d-47541718a9b4" - }, + "id": "82149122-61d3-490d-9bdb-bb98606e8ba1", + "metadata": {}, + "outputs": [], + "source": [ + "prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\"system\", \"You are a helpful assistant.\"),\n", + " MessagesPlaceholder(variable_name=\"history\"),\n", + " (\"human\", \"{question}\"),\n", + " ]\n", + ")\n", + "\n", + "chain = prompt | ChatOpenAI()" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "2df90853-b67c-490f-b7f8-b69d69270b9c", + "metadata": {}, + "outputs": [], + "source": [ + "chain_with_history = RunnableWithMessageHistory(\n", + " chain,\n", + " lambda session_id: SQLChatMessageHistory(\n", + " session_id=session_id, connection_string=\"sqlite:///sqlite.db\"\n", + " ),\n", + " input_messages_key=\"question\",\n", + " history_messages_key=\"history\",\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "0ce596b8-3b78-48fd-9f92-46dccbbfd58b", + "metadata": {}, + "outputs": [], + "source": [ + "# This is where we configure the session id\n", + "config = {\"configurable\": {\"session_id\": \"\"}}" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "38e1423b-ba86-4496-9151-25932fab1a8b", + "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "'Deven is working on a hackathon project with Sam.'" + "AIMessage(content='Hello Bob! How can I assist you today?')" ] }, - "execution_count": 4, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "conversation.memory.entity_store.get(\"Deven\")" + "chain_with_history.invoke({\"question\": \"Hi! I'm bob\"}, config=config)" ] }, { "cell_type": "code", - "execution_count": 5, - "id": "316f2e8d", + "execution_count": 10, + "id": "2ee4ee62-a216-4fb1-bf33-57476a84cf16", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "'Sam is working on a hackathon project with Deven.'" + "AIMessage(content='Your name is Bob! Is there anything specific you would like assistance with, Bob?')" ] }, - "execution_count": 5, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "conversation.memory.entity_store.get(\"Sam\")" + "chain_with_history.invoke({\"question\": \"Whats my name\"}, config=config)" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b85f8427", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/docs/docs/integrations/memory/streamlit_chat_message_history.ipynb b/docs/docs/integrations/memory/streamlit_chat_message_history.ipynb index 21de8c78ac48a..987d6aecccea4 100644 --- a/docs/docs/integrations/memory/streamlit_chat_message_history.ipynb +++ b/docs/docs/integrations/memory/streamlit_chat_message_history.ipynb @@ -10,7 +10,6 @@ ">[Streamlit](https://docs.streamlit.io/) is an open-source Python library that makes it easy to create and share beautiful, \n", "custom web apps for machine learning and data science.\n", "\n", - "\n", "This notebook goes over how to store and use chat message history in a `Streamlit` app. `StreamlitChatMessageHistory` will store messages in\n", "[Streamlit session state](https://docs.streamlit.io/library/api-reference/session-state)\n", "at the specified `key=`. The default key is `\"langchain_messages\"`.\n", @@ -20,6 +19,12 @@ "- For more on Streamlit check out their\n", "[getting started documentation](https://docs.streamlit.io/library/get-started).\n", "\n", + "The integration lives in the `langchain-community` package, so we need to install that. We also need to install `streamlit`.\n", + "\n", + "```\n", + "pip install -U langchain-community streamlit\n", + "```\n", + "\n", "You can see the [full app example running here](https://langchain-st-memory.streamlit.app/), and more examples in\n", "[github.com/langchain-ai/streamlit-agent](https://github.com/langchain-ai/streamlit-agent)." ] @@ -31,7 +36,7 @@ "metadata": {}, "outputs": [], "source": [ - "from langchain.memory import StreamlitChatMessageHistory\n", + "from langchain_community.chat_message_histories import StreamlitChatMessageHistory\n", "\n", "history = StreamlitChatMessageHistory(key=\"chat_messages\")\n", "\n", @@ -54,7 +59,9 @@ "id": "b60dc735", "metadata": {}, "source": [ - "You can integrate `StreamlitChatMessageHistory` into `ConversationBufferMemory` and chains or agents as usual. The history will be persisted across re-runs of the Streamlit app within a given user session. A given `StreamlitChatMessageHistory` will NOT be persisted or shared across user sessions." + "We can easily combine this message history class with [LCEL Runnables](https://python.langchain.com/docs/expression_language/how_to/message_history).\n", + "\n", + "The history will be persisted across re-runs of the Streamlit app within a given user session. A given `StreamlitChatMessageHistory` will NOT be persisted or shared across user sessions." ] }, { @@ -64,13 +71,11 @@ "metadata": {}, "outputs": [], "source": [ - "from langchain.memory import ConversationBufferMemory\n", "from langchain_community.chat_message_histories import StreamlitChatMessageHistory\n", "\n", "# Optionally, specify your own session_state key for storing messages\n", "msgs = StreamlitChatMessageHistory(key=\"special_app_key\")\n", "\n", - "memory = ConversationBufferMemory(memory_key=\"history\", chat_memory=msgs)\n", "if len(msgs.messages) == 0:\n", " msgs.add_ai_message(\"How can I help you?\")" ] @@ -82,19 +87,34 @@ "metadata": {}, "outputs": [], "source": [ - "from langchain.chains import LLMChain\n", - "from langchain.prompts import PromptTemplate\n", - "from langchain_openai import OpenAI\n", - "\n", - "template = \"\"\"You are an AI chatbot having a conversation with a human.\n", + "from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n", + "from langchain_core.runnables.history import RunnableWithMessageHistory\n", + "from langchain_openai import ChatOpenAI\n", "\n", - "{history}\n", - "Human: {human_input}\n", - "AI: \"\"\"\n", - "prompt = PromptTemplate(input_variables=[\"history\", \"human_input\"], template=template)\n", + "prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\"system\", \"You are an AI chatbot having a conversation with a human.\"),\n", + " MessagesPlaceholder(variable_name=\"history\"),\n", + " (\"human\", \"{question}\"),\n", + " ]\n", + ")\n", "\n", - "# Add the memory to an LLMChain as usual\n", - "llm_chain = LLMChain(llm=OpenAI(), prompt=prompt, memory=memory)" + "chain = prompt | ChatOpenAI()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dac3d94f", + "metadata": {}, + "outputs": [], + "source": [ + "chain_with_history = RunnableWithMessageHistory(\n", + " chain,\n", + " lambda session_id: msgs, # Always return the instance created earlier\n", + " input_messages_key=\"question\",\n", + " history_messages_key=\"history\",\n", + ")" ] }, { @@ -121,8 +141,9 @@ " st.chat_message(\"human\").write(prompt)\n", "\n", " # As usual, new messages are added to StreamlitChatMessageHistory when the Chain is called.\n", - " response = llm_chain.run(prompt)\n", - " st.chat_message(\"ai\").write(response)" + " config = {\"configurable\": {\"session_id\": \"any\"}}\n", + " response = chain_with_history.invoke({\"question\": prompt}, config)\n", + " st.chat_message(\"ai\").write(response.content)" ] }, { diff --git a/docs/docs/integrations/memory/tidb_chat_message_history.ipynb b/docs/docs/integrations/memory/tidb_chat_message_history.ipynb new file mode 100644 index 0000000000000..df3bc3da22786 --- /dev/null +++ b/docs/docs/integrations/memory/tidb_chat_message_history.ipynb @@ -0,0 +1,266 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# TiDB\n", + "\n", + "> [TiDB](https://github.com/pingcap/tidb) is an open-source, cloud-native, distributed, MySQL-Compatible database for elastic scale and real-time analytics.\n", + "\n", + "This notebook introduces how to use TiDB to store chat message history. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "Firstly, we will install the following dependencies:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet langchain langchain_openai" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Configuring your OpenAI Key" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import getpass\n", + "import os\n", + "\n", + "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"Input your OpenAI API key:\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, we will configure the connection to a TiDB. In this notebook, we will follow the standard connection method provided by TiDB Cloud to establish a secure and efficient database connection." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "# copy from tidb cloud console\n", + "tidb_connection_string_template = \"mysql+pymysql://:@:4000/?ssl_ca=/etc/ssl/cert.pem&ssl_verify_cert=true&ssl_verify_identity=true\"\n", + "tidb_password = getpass.getpass(\"Input your TiDB password:\")\n", + "tidb_connection_string = tidb_connection_string_template.replace(\n", + " \"\", tidb_password\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Generating historical data\n", + "\n", + "Creating a set of historical data, which will serve as the foundation for our upcoming demonstrations." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "from datetime import datetime\n", + "\n", + "from langchain_community.chat_message_histories import TiDBChatMessageHistory\n", + "\n", + "history = TiDBChatMessageHistory(\n", + " connection_string=tidb_connection_string,\n", + " session_id=\"code_gen\",\n", + " earliest_time=datetime.utcnow(), # Optional to set earliest_time to load messages after this time point.\n", + ")\n", + "\n", + "history.add_user_message(\"How's our feature going?\")\n", + "history.add_ai_message(\n", + " \"It's going well. We are working on testing now. It will be released in Feb.\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[HumanMessage(content=\"How's our feature going?\"),\n", + " AIMessage(content=\"It's going well. We are working on testing now. It will be released in Feb.\")]" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "history.messages" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Chatting with historical data\n", + "\n", + "Let’s build upon the historical data generated earlier to create a dynamic chat interaction. \n", + "\n", + "Firstly, Creating a Chat Chain with LangChain:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n", + "from langchain_openai import ChatOpenAI\n", + "\n", + "prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\n", + " \"system\",\n", + " \"You're an assistant who's good at coding. You're helping a startup build\",\n", + " ),\n", + " MessagesPlaceholder(variable_name=\"history\"),\n", + " (\"human\", \"{question}\"),\n", + " ]\n", + ")\n", + "chain = prompt | ChatOpenAI()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Building a Runnable on History:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_core.runnables.history import RunnableWithMessageHistory\n", + "\n", + "chain_with_history = RunnableWithMessageHistory(\n", + " chain,\n", + " lambda session_id: TiDBChatMessageHistory(\n", + " session_id=session_id, connection_string=tidb_connection_string\n", + " ),\n", + " input_messages_key=\"question\",\n", + " history_messages_key=\"history\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Initiating the Chat:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "AIMessage(content='There are 31 days in January, so there are 30 days until our feature is released in February.')" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "response = chain_with_history.invoke(\n", + " {\"question\": \"Today is Jan 1st. How many days until our feature is released?\"},\n", + " config={\"configurable\": {\"session_id\": \"code_gen\"}},\n", + ")\n", + "response" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Checking the history data" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[HumanMessage(content=\"How's our feature going?\"),\n", + " AIMessage(content=\"It's going well. We are working on testing now. It will be released in Feb.\"),\n", + " HumanMessage(content='Today is Jan 1st. How many days until our feature is released?'),\n", + " AIMessage(content='There are 31 days in January, so there are 30 days until our feature is released in February.')]" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "history.reload_cache()\n", + "history.messages" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "langchain", + "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.10.13" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/docs/integrations/platforms/google.mdx b/docs/docs/integrations/platforms/google.mdx index 040ace8aac286..5d09b18f56b07 100644 --- a/docs/docs/integrations/platforms/google.mdx +++ b/docs/docs/integrations/platforms/google.mdx @@ -186,7 +186,7 @@ from langchain_community.document_loaders import GoogleSpeechToTextLoader ### Google Vertex AI Vector Search > [Google Vertex AI Vector Search](https://cloud.google.com/vertex-ai/docs/matching-engine/overview), -> formerly known as `Vertex AI Matching Engine`, provides the industry's leading high-scale +> formerly known as `Vertex AI Matching Engine`, provides the industry's leading high-scale > low latency vector database. These vector databases are commonly > referred to as vector similarity-matching or an approximate nearest neighbor (ANN) service. @@ -207,10 +207,14 @@ from langchain_community.vectorstores import MatchingEngine > [Google BigQuery](https://cloud.google.com/bigquery), > BigQuery is a serverless and cost-effective enterprise data warehouse in Google Cloud. > -> Google BigQuery Vector Search +> Google BigQuery Vector Search > BigQuery vector search lets you use GoogleSQL to do semantic search, using vector indexes for fast but approximate results, or using brute force for exact results. -> It can calculate Euclidean or Cosine distance. With LangChain, we default to use Euclidean distance. +> It can calculate Euclidean or Cosine distance. With LangChain, we default to use Euclidean distance. + +> This is a private preview (experimental) feature. Please submit this +> [enrollment form](https://docs.google.com/forms/d/18yndSb4dTf2H0orqA9N7NAchQEDQekwWiD5jYfEkGWk/viewform?edit_requested=true) +> if you want to enroll BigQuery Vector Search Experimental. We need to install several python packages. @@ -228,7 +232,7 @@ from langchain.vectorstores import BigQueryVectorSearch >[Google ScaNN](https://github.com/google-research/google-research/tree/master/scann) > (Scalable Nearest Neighbors) is a python package. -> +> >`ScaNN` is a method for efficient vector similarity search at scale. >`ScaNN` includes search space pruning and quantization for Maximum Inner @@ -285,9 +289,9 @@ from langchain.retrievers import GoogleVertexAISearchRetriever ### Document AI Warehouse > [Google Cloud Document AI Warehouse](https://cloud.google.com/document-ai-warehouse) -> allows enterprises to search, store, govern, and manage documents and their AI-extracted +> allows enterprises to search, store, govern, and manage documents and their AI-extracted > data and metadata in a single platform. -> +> ```python from langchain.retrievers import GoogleDocumentAIWarehouseRetriever @@ -304,9 +308,9 @@ documents = docai_wh_retriever.get_relevant_documents( ### Google Cloud Text-to-Speech ->[Google Cloud Text-to-Speech](https://cloud.google.com/text-to-speech) enables developers to -> synthesize natural-sounding speech with 100+ voices, available in multiple languages and variants. -> It applies DeepMind’s groundbreaking research in WaveNet and Google’s powerful neural networks +>[Google Cloud Text-to-Speech](https://cloud.google.com/text-to-speech) enables developers to +> synthesize natural-sounding speech with 100+ voices, available in multiple languages and variants. +> It applies DeepMind’s groundbreaking research in WaveNet and Google’s powerful neural networks > to deliver the highest fidelity possible. We need to install a python package. @@ -354,7 +358,7 @@ from langchain.tools import GooglePlacesTool ### Google Search - Set up a Custom Search Engine, following [these instructions](https://stackoverflow.com/questions/37083058/programmatically-searching-google-in-python-using-custom-search) -- Get an API Key and Custom Search Engine ID from the previous step, and set them as environment variables +- Get an API Key and Custom Search Engine ID from the previous step, and set them as environment variables `GOOGLE_API_KEY` and `GOOGLE_CSE_ID` respectively. ```python @@ -444,12 +448,12 @@ from langchain_community.utilities.google_trends import GoogleTrendsAPIWrapper ### Google Document AI ->[Document AI](https://cloud.google.com/document-ai/docs/overview) is a `Google Cloud Platform` -> service that transforms unstructured data from documents into structured data, making it easier +>[Document AI](https://cloud.google.com/document-ai/docs/overview) is a `Google Cloud Platform` +> service that transforms unstructured data from documents into structured data, making it easier > to understand, analyze, and consume. -We need to set up a [`GCS` bucket and create your own OCR processor](https://cloud.google.com/document-ai/docs/create-processor) -The `GCS_OUTPUT_PATH` should be a path to a folder on GCS (starting with `gs://`) +We need to set up a [`GCS` bucket and create your own OCR processor](https://cloud.google.com/document-ai/docs/create-processor) +The `GCS_OUTPUT_PATH` should be a path to a folder on GCS (starting with `gs://`) and a processor name should look like `projects/PROJECT_NUMBER/locations/LOCATION/processors/PROCESSOR_ID`. We can get it either programmatically or copy from the `Prediction endpoint` section of the `Processor details` tab in the Google Cloud Console. @@ -507,6 +511,23 @@ See a [usage example and authorization instructions](/docs/integrations/toolkits from langchain_community.agent_toolkits import GmailToolkit ``` +## Memory + +### Cloud Firestore + +> [`Cloud Firestore`](https://cloud.google.com/firestore) is a NoSQL document database built for automatic scaling, high performance, and ease of application development. + +First, we need to install the python package. + +```bash +pip install firebase-admin +``` + +See a [usage example and authorization instructions](/docs/integrations/memory/firestore_chat_message_history). + +```python +from langchain_community.chat_message_histories.firestore import FirestoreChatMessageHistory +``` ## Chat Loaders @@ -560,7 +581,7 @@ from langchain_community.utilities import GoogleSerperAPIWrapper ### YouTube >[YouTube Search](https://github.com/joetats/youtube_search) package searches `YouTube` videos avoiding using their heavily rate-limited API. -> +> >It uses the form on the YouTube homepage and scrapes the resulting page. We need to install a python package. diff --git a/docs/docs/integrations/providers/activeloop_deeplake.mdx b/docs/docs/integrations/providers/activeloop_deeplake.mdx index 565eb2132c8d2..121ddfb537fae 100644 --- a/docs/docs/integrations/providers/activeloop_deeplake.mdx +++ b/docs/docs/integrations/providers/activeloop_deeplake.mdx @@ -13,7 +13,7 @@ Activeloop Deep Lake supports SelfQuery Retrieval: ## More Resources 1. [Ultimate Guide to LangChain & Deep Lake: Build ChatGPT to Answer Questions on Your Financial Data](https://www.activeloop.ai/resources/ultimate-guide-to-lang-chain-deep-lake-build-chat-gpt-to-answer-questions-on-your-financial-data/) -2. [Twitter the-algorithm codebase analysis with Deep Lake](/docs/use_cases/question_answering/code/twitter-the-algorithm-analysis-deeplake) +2. [Twitter the-algorithm codebase analysis with Deep Lake](https://github.com/langchain-ai/langchain/blob/master/cookbook/twitter-the-algorithm-analysis-deeplake.ipynb) 3. Here is [whitepaper](https://www.deeplake.ai/whitepaper) and [academic paper](https://arxiv.org/pdf/2209.10785.pdf) for Deep Lake 4. Here is a set of additional resources available for review: [Deep Lake](https://github.com/activeloopai/deeplake), [Get started](https://docs.activeloop.ai/getting-started) and [Tutorials](https://docs.activeloop.ai/hub-tutorials) diff --git a/docs/docs/integrations/providers/anyscale.mdx b/docs/docs/integrations/providers/anyscale.mdx index a3b8c4cc4e318..087422e129beb 100644 --- a/docs/docs/integrations/providers/anyscale.mdx +++ b/docs/docs/integrations/providers/anyscale.mdx @@ -1,17 +1,34 @@ # Anyscale -This page covers how to use the Anyscale ecosystem within LangChain. -It is broken into two parts: installation and setup, and then references to specific Anyscale wrappers. +>[Anyscale](https://www.anyscale.com) is a platform to run, fine tune and scale LLMs via production-ready APIs. +> [Anyscale Endpoints](https://docs.anyscale.com/endpoints/overview) serve many open-source models in a cost-effective way. + +`Anyscale` also provides [an example](https://docs.anyscale.com/endpoints/model-serving/examples/langchain-integration) +how to setup `LangChain` with `Anyscale` for advanced chat agents. ## Installation and Setup + - Get an Anyscale Service URL, route and API key and set them as environment variables (`ANYSCALE_SERVICE_URL`,`ANYSCALE_SERVICE_ROUTE`, `ANYSCALE_SERVICE_TOKEN`). -- Please see [the Anyscale docs](https://docs.anyscale.com/productionize/services-v2/get-started) for more details. +- Please see [the Anyscale docs](https://www.anyscale.com/get-started) for more details. + +We have to install the `openai` package: + +```bash +pip install openai +``` + +## LLM + +See a [usage example](/docs/integrations/llms/anyscale). + +```python +from langchain_community.llms.anyscale import Anyscale +``` -## Wrappers +## Chat Models -### LLM +See a [usage example](/docs/integrations/chat/anyscale). -There exists an Anyscale LLM wrapper, which you can access with ```python -from langchain_community.llms import Anyscale +from langchain_community.chat_models.anyscale import ChatAnyscale ``` diff --git a/docs/docs/integrations/providers/astradb.mdx b/docs/docs/integrations/providers/astradb.mdx index e4c69d0e42884..20a94d736b361 100644 --- a/docs/docs/integrations/providers/astradb.mdx +++ b/docs/docs/integrations/providers/astradb.mdx @@ -20,10 +20,10 @@ pip install "astrapy>=0.5.3" ```python from langchain_community.vectorstores import AstraDB vector_store = AstraDB( - embedding=my_embedding, - collection_name="my_store", - api_endpoint="...", - token="...", + embedding=my_embedding, + collection_name="my_store", + api_endpoint="...", + token="...", ) ``` @@ -40,7 +40,7 @@ set_llm_cache(AstraDBCache( )) ``` -Learn more in the [example notebook](/docs/integrations/llms/llm_caching) (scroll to the Astra DB section). +Learn more in the [example notebook](/docs/integrations/llms/llm_caching#astra-db-caches) (scroll to the Astra DB section). ### Semantic LLM Cache @@ -55,14 +55,14 @@ set_llm_cache(AstraDBSemanticCache( )) ``` -Learn more in the [example notebook](/docs/integrations/llms/llm_caching) (scroll to the appropriate section). +Learn more in the [example notebook](/docs/integrations/llms/llm_caching#astra-db-caches) (scroll to the appropriate section). ### Chat message history ```python from langchain.memory import AstraDBChatMessageHistory message_history = AstraDBChatMessageHistory( - session_id="test-session" + session_id="test-session", api_endpoint="...", token="...", ) @@ -75,14 +75,62 @@ Learn more in the [example notebook](/docs/integrations/memory/astradb_chat_mess ```python from langchain_community.document_loaders import AstraDBLoader loader = AstraDBLoader( + collection_name="my_collection", api_endpoint="...", - token="...", - collection_name="my_collection" + token="..." ) ``` Learn more in the [example notebook](/docs/integrations/document_loaders/astradb). +### Self-querying retriever + +```python +from langchain_community.vectorstores import AstraDB +from langchain.retrievers.self_query.base import SelfQueryRetriever + +vector_store = AstraDB( + embedding=my_embedding, + collection_name="my_store", + api_endpoint="...", + token="...", +) + +retriever = SelfQueryRetriever.from_llm( + my_llm, + vector_store, + document_content_description, + metadata_field_info +) +``` + +Learn more in the [example notebook](/docs/integrations/retrievers/self_query/astradb). + +### Store + +```python +from langchain_community.storage import AstraDBStore +store = AstraDBStore( + collection_name="my_kv_store", + api_endpoint="...", + token="..." +) +``` + +Learn more in the [example notebook](/docs/integrations/stores/astradb#astradbstore). + +### Byte Store + +```python +from langchain_community.storage import AstraDBByteStore +store = AstraDBByteStore( + collection_name="my_kv_store", + api_endpoint="...", + token="..." +) +``` + +Learn more in the [example notebook](/docs/integrations/stores/astradb#astradbbytestore). ## Apache Cassandra and Astra DB through CQL @@ -98,12 +146,12 @@ Hence, a different set of connectors, outlined below, shall be used. ```python from langchain_community.vectorstores import Cassandra vector_store = Cassandra( - embedding=my_embedding, - table_name="my_store", + embedding=my_embedding, + table_name="my_store", ) ``` -Learn more in the [example notebook](/docs/integrations/vectorstores/astradb) (scroll down to the CQL-specific section). +Learn more in the [example notebook](/docs/integrations/vectorstores/astradb#apache-cassandra-and-astra-db-through-cql) (scroll down to the CQL-specific section). ### Memory @@ -123,7 +171,7 @@ from langchain.cache import CassandraCache langchain.llm_cache = CassandraCache() ``` -Learn more in the [example notebook](/docs/integrations/llms/llm_caching) (scroll to the Cassandra section). +Learn more in the [example notebook](/docs/integrations/llms/llm_caching#cassandra-caches) (scroll to the Cassandra section). ### Semantic LLM Cache @@ -131,9 +179,9 @@ Learn more in the [example notebook](/docs/integrations/llms/llm_caching) (scrol ```python from langchain.cache import CassandraSemanticCache cassSemanticCache = CassandraSemanticCache( - embedding=my_embedding, - table_name="my_store", + embedding=my_embedding, + table_name="my_store", ) ``` -Learn more in the [example notebook](/docs/integrations/llms/llm_caching) (scroll to the appropriate section). +Learn more in the [example notebook](/docs/integrations/llms/llm_caching#cassandra-caches) (scroll to the appropriate section). diff --git a/docs/docs/integrations/providers/chroma.mdx b/docs/docs/integrations/providers/chroma.mdx index 76c2f74244f90..ab7af6029bd6b 100644 --- a/docs/docs/integrations/providers/chroma.mdx +++ b/docs/docs/integrations/providers/chroma.mdx @@ -18,11 +18,11 @@ whether for semantic search or example selection. from langchain_community.vectorstores import Chroma ``` -For a more detailed walkthrough of the Chroma wrapper, see [this notebook](/docs/integrations/vectorstores/chroma_self_query) +For a more detailed walkthrough of the Chroma wrapper, see [this notebook](/docs/integrations/vectorstores/chroma) ## Retriever -See a [usage example](/docs/integrations/retrievers/self_query/chroma). +See a [usage example](/docs/integrations/retrievers/self_query/chroma_self_query). ```python from langchain.retrievers import SelfQueryRetriever diff --git a/docs/docs/integrations/providers/deepinfra.mdx b/docs/docs/integrations/providers/deepinfra.mdx index d370862aa65bd..06af21f287ee8 100644 --- a/docs/docs/integrations/providers/deepinfra.mdx +++ b/docs/docs/integrations/providers/deepinfra.mdx @@ -17,6 +17,8 @@ google/flan\* models can be viewed [here](https://deepinfra.com/models?type=text You can view a [list of request and response parameters](https://deepinfra.com/meta-llama/Llama-2-70b-chat-hf/api). +Chat models [follow openai api](https://deepinfra.com/meta-llama/Llama-2-70b-chat-hf/api?example=openai-http) + ## Wrappers ### LLM @@ -34,3 +36,11 @@ There is also an DeepInfra Embeddings wrapper, you can access with ```python from langchain_community.embeddings import DeepInfraEmbeddings ``` + +### Chat Models + +There is a chat-oriented wrapper as well, accessible with + +```python +from langchain_community.chat_models import ChatDeepInfra +``` diff --git a/docs/docs/integrations/providers/kdbai.mdx b/docs/docs/integrations/providers/kdbai.mdx new file mode 100644 index 0000000000000..a5f06d0128748 --- /dev/null +++ b/docs/docs/integrations/providers/kdbai.mdx @@ -0,0 +1,24 @@ +# KDB.AI + +>[KDB.AI](https://kdb.ai) is a powerful knowledge-based vector database and search engine that allows you to build scalable, reliable AI applications, using real-time data, by providing advanced search, recommendation and personalization. + + +## Installation and Setup + +Install the Python SDK: + +```bash +pip install kdbai-client +``` + + +## Vector store + +There exists a wrapper around KDB.AI indexes, allowing you to use it as a vectorstore, +whether for semantic search or example selection. + +```python +from langchain_community.vectorstores import KDBAI +``` + +For a more detailed walkthrough of the KDB.AI vectorstore, see [this notebook](/docs/integrations/vectorstores/kdbai) diff --git a/docs/docs/integrations/providers/konko.mdx b/docs/docs/integrations/providers/konko.mdx index 1735aa0d01c30..efdc5cb05a480 100644 --- a/docs/docs/integrations/providers/konko.mdx +++ b/docs/docs/integrations/providers/konko.mdx @@ -60,21 +60,27 @@ konko.Model.list() ## Calling a model -Find a model on the [Konko Introduction page](https://docs.konko.ai/docs#available-models) - -For example, for this [LLama 2 model](https://docs.konko.ai/docs/meta-llama-2-13b-chat). The model id would be: `"meta-llama/Llama-2-13b-chat-hf"` +Find a model on the [Konko Introduction page](https://docs.konko.ai/docs/list-of-models) Another way to find the list of models running on the Konko instance is through this [endpoint](https://docs.konko.ai/reference/listmodels). -From here, we can initialize our model: +## Examples of Endpoint Usage -```python -chat_instance = ChatKonko(max_tokens=10, model = 'meta-llama/Llama-2-13b-chat-hf') -``` -And run it: +- **ChatCompletion with Mistral-7B:** + ```python + chat_instance = ChatKonko(max_tokens=10, model = 'mistralai/mistral-7b-instruct-v0.1') + msg = HumanMessage(content="Hi") + chat_response = chat_instance([msg]) + + ``` -```python -msg = HumanMessage(content="Hi") -chat_response = chat_instance([msg]) -``` +- **Completion with mistralai/Mistral-7B-v0.1:** + ```python + from langchain.llms import Konko + llm = Konko(max_tokens=800, model='mistralai/Mistral-7B-v0.1') + prompt = "Generate a Product Description for Apple Iphone 15" + response = llm(prompt) + ``` + +For further assistance, contact [support@konko.ai](mailto:support@konko.ai) or join our [Discord](https://discord.gg/TXV2s3z7RZ). \ No newline at end of file diff --git a/docs/docs/integrations/providers/motherduck.mdx b/docs/docs/integrations/providers/motherduck.mdx index 5ef5c65e9cd0f..ee39a117bb291 100644 --- a/docs/docs/integrations/providers/motherduck.mdx +++ b/docs/docs/integrations/providers/motherduck.mdx @@ -33,7 +33,7 @@ db = SQLDatabase.from_uri(conn_str) db_chain = SQLDatabaseChain.from_llm(OpenAI(temperature=0), db, verbose=True) ``` -From here, see the [SQL Chain](/docs/use_cases/tabular/sqlite) documentation on how to use. +From here, see the [SQL Chain](/docs/use_cases/sql/) documentation on how to use. ## LLMCache diff --git a/docs/docs/integrations/providers/ragatouille.ipynb b/docs/docs/integrations/providers/ragatouille.ipynb index a4089861b4168..46f77ed5a5dea 100644 --- a/docs/docs/integrations/providers/ragatouille.ipynb +++ b/docs/docs/integrations/providers/ragatouille.ipynb @@ -66,7 +66,7 @@ "source": [ "## Document Compressor\n", "\n", - "We can also use RAGatouille off-the-shelf as a reranker. This will allow us to use ColBERT to rerank retrieved results from any generic retriever. The benefits of this are that we can do this on top of any existing index, so that we don't need to create a new idex. We can do this by using the [document compressor](/docs/modules/data_connections/retrievers/contextual_compression) abstraction in LangChain." + "We can also use RAGatouille off-the-shelf as a reranker. This will allow us to use ColBERT to rerank retrieved results from any generic retriever. The benefits of this are that we can do this on top of any existing index, so that we don't need to create a new idex. We can do this by using the [document compressor](/docs/modules/data_connection/retrievers/contextual_compression) abstraction in LangChain." ] }, { diff --git a/docs/docs/integrations/providers/tigergraph.mdx b/docs/docs/integrations/providers/tigergraph.mdx new file mode 100644 index 0000000000000..d637d0f3dbc9e --- /dev/null +++ b/docs/docs/integrations/providers/tigergraph.mdx @@ -0,0 +1,34 @@ +# TigerGraph + +This page covers how to use the TigerGraph ecosystem within LangChain. + +What is TigerGraph? + +**TigerGraph in a nutshell:** + +- TigerGraph is a natively distributed and high-performance graph database. +- The storage of data in a graph format of vertices and edges leads to rich relationships, ideal for grouding LLM responses. +- Get started quickly with TigerGraph by visiting [their website](https://tigergraph.com/). + +## Installation and Setup + +- Install the Python SDK with `pip install pyTigerGraph` + +## Wrappers + +### TigerGraph Store +To utilize the TigerGraph InquiryAI functionality, you can import `TigerGraph` from `langchain_community.graphs`. + +```python +import pyTigerGraph as tg +conn = tg.TigerGraphConnection(host="DATABASE_HOST_HERE", graphname="GRAPH_NAME_HERE", username="USERNAME_HERE", password="PASSWORD_HERE") + +### ==== CONFIGURE INQUIRYAI HOST ==== +conn.ai.configureInquiryAIHost("INQUIRYAI_HOST_HERE") + +from langchain_community.graphs import TigerGraph +graph = TigerGraph(conn) +result = graph.query("How many servers are there?") +print(result) +``` + diff --git a/docs/docs/integrations/retrievers/activeloop.ipynb b/docs/docs/integrations/retrievers/activeloop.ipynb index e85d4a150ff08..42b71c4a0ca7f 100644 --- a/docs/docs/integrations/retrievers/activeloop.ipynb +++ b/docs/docs/integrations/retrievers/activeloop.ipynb @@ -51,7 +51,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Also you'll need to create a [Activeloop]((https://activeloop.ai/)) account." + "Also you'll need to create a [Activeloop](https://activeloop.ai) account." ] }, { diff --git a/docs/docs/integrations/retrievers/cohere-reranker.ipynb b/docs/docs/integrations/retrievers/cohere-reranker.ipynb index afdf5e2f5881c..24c58ff5d4861 100644 --- a/docs/docs/integrations/retrievers/cohere-reranker.ipynb +++ b/docs/docs/integrations/retrievers/cohere-reranker.ipynb @@ -24,7 +24,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "b37bd138-4f3c-4d2c-bc4b-be705ce27a09", "metadata": { "tags": [] @@ -40,7 +40,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 13, "id": "c47b0b26-6d51-4beb-aedb-ad09740a9a2b", "metadata": {}, "outputs": [], @@ -55,19 +55,12 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "2268c17f-5cc3-457b-928b-0d470154c3a8", - "metadata": {}, - "outputs": [], - "source": [ - "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "28e8dc12", - "metadata": {}, + "execution_count": 14, + "id": "6fa3d916", + "metadata": { + "jp-MarkdownHeadingCollapsed": true, + "tags": [] + }, "outputs": [], "source": [ "# Helper function for printing docs\n", @@ -95,8 +88,8 @@ }, { "cell_type": "code", - "execution_count": 22, - "id": "9fbcc58f", + "execution_count": 15, + "id": "b7648612", "metadata": {}, "outputs": [ { @@ -111,28 +104,20 @@ "----------------------------------------------------------------------------------------------------\n", "Document 2:\n", "\n", - "As I said last year, especially to our younger transgender Americans, I will always have your back as your President, so you can be yourself and reach your God-given potential. \n", + "We cannot let this happen. \n", "\n", - "While it often appears that we never agree, that isn’t true. I signed 80 bipartisan bills into law last year. From preventing government shutdowns to protecting Asian-Americans from still-too-common hate crimes to reforming military justice.\n", + "Tonight. I call on the Senate to: Pass the Freedom to Vote Act. Pass the John Lewis Voting Rights Act. And while you’re at it, pass the Disclose Act so Americans can know who is funding our elections. \n", + "\n", + "Tonight, I’d like to honor someone who has dedicated his life to serve this country: Justice Stephen Breyer—an Army veteran, Constitutional scholar, and retiring Justice of the United States Supreme Court. Justice Breyer, thank you for your service.\n", "----------------------------------------------------------------------------------------------------\n", "Document 3:\n", "\n", - "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", + "As I said last year, especially to our younger transgender Americans, I will always have your back as your President, so you can be yourself and reach your God-given potential. \n", "\n", - "And if we are to advance liberty and justice, we need to secure the Border and fix the immigration system.\n", + "While it often appears that we never agree, that isn’t true. I signed 80 bipartisan bills into law last year. From preventing government shutdowns to protecting Asian-Americans from still-too-common hate crimes to reforming military justice.\n", "----------------------------------------------------------------------------------------------------\n", "Document 4:\n", "\n", - "He met the Ukrainian people. \n", - "\n", - "From President Zelenskyy to every Ukrainian, their fearlessness, their courage, their determination, inspires the world. \n", - "\n", - "Groups of citizens blocking tanks with their bodies. Everyone from students to retirees teachers turned soldiers defending their homeland. \n", - "\n", - "In 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", - "----------------------------------------------------------------------------------------------------\n", - "Document 5:\n", - "\n", "I 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", "\n", "I’ve worked on these issues a long time. \n", @@ -141,199 +126,190 @@ "\n", "So let’s not abandon our streets. Or choose between safety and equal justice.\n", "----------------------------------------------------------------------------------------------------\n", - "Document 6:\n", - "\n", - "Vice President Harris and I ran for office with a new economic vision for America. \n", + "Document 5:\n", "\n", - "Invest in America. Educate Americans. Grow the workforce. Build the economy from the bottom up \n", - "and the middle out, not from the top down. \n", + "He met the Ukrainian people. \n", "\n", - "Because we know that when the middle class grows, the poor have a ladder up and the wealthy do very well. \n", + "From President Zelenskyy to every Ukrainian, their fearlessness, their courage, their determination, inspires the world. \n", "\n", - "America used to have the best roads, bridges, and airports on Earth. \n", + "Groups of citizens blocking tanks with their bodies. Everyone from students to retirees teachers turned soldiers defending their homeland. \n", "\n", - "Now our infrastructure is ranked 13th in the world.\n", + "In 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", "----------------------------------------------------------------------------------------------------\n", - "Document 7:\n", + "Document 6:\n", "\n", - "And tonight, I’m announcing that the Justice Department will name a chief prosecutor for pandemic fraud. \n", + "So let’s not abandon our streets. Or choose between safety and equal justice. \n", "\n", - "By the end of this year, the deficit will be down to less than half what it was before I took office. \n", + "Let’s come together to protect our communities, restore trust, and hold law enforcement accountable. \n", "\n", - "The only president ever to cut the deficit by more than one trillion dollars in a single year. \n", + "That’s why the Justice Department required body cameras, banned chokeholds, and restricted no-knock warrants for its officers.\n", + "----------------------------------------------------------------------------------------------------\n", + "Document 7:\n", "\n", - "Lowering your costs also means demanding more competition. \n", + "But that trickle-down theory led to weaker economic growth, lower wages, bigger deficits, and the widest gap between those at the top and everyone else in nearly a century. \n", "\n", - "I’m a capitalist, but capitalism without competition isn’t capitalism. \n", + "Vice President Harris and I ran for office with a new economic vision for America. \n", "\n", - "It’s exploitation—and it drives up prices.\n", + "Invest in America. Educate Americans. Grow the workforce. Build the economy from the bottom up \n", + "and the middle out, not from the top down.\n", "----------------------------------------------------------------------------------------------------\n", "Document 8:\n", "\n", - "For the past 40 years we were told that if we gave tax breaks to those at the very top, the benefits would trickle down to everyone else. \n", - "\n", - "But that trickle-down theory led to weaker economic growth, lower wages, bigger deficits, and the widest gap between those at the top and everyone else in nearly a century. \n", + "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", "\n", - "Vice President Harris and I ran for office with a new economic vision for America.\n", + "And if we are to advance liberty and justice, we need to secure the Border and fix the immigration system.\n", "----------------------------------------------------------------------------------------------------\n", "Document 9:\n", "\n", - "All told, we created 369,000 new manufacturing jobs in America just last year. \n", + "The widow of Sergeant First Class Heath Robinson. \n", "\n", - "Powered by people I’ve met like JoJo Burgess, from generations of union steelworkers from Pittsburgh, who’s here with us tonight. \n", + "He was born a soldier. Army National Guard. Combat medic in Kosovo and Iraq. \n", "\n", - "As Ohio Senator Sherrod Brown says, “It’s time to bury the label “Rust Belt.” \n", + "Stationed near Baghdad, just yards from burn pits the size of football fields. \n", "\n", - "It’s time. \n", + "Heath’s widow Danielle is here with us tonight. They loved going to Ohio State football games. He loved building Legos with their daughter. \n", + "\n", + "But cancer from prolonged exposure to burn pits ravaged Heath’s lungs and body. \n", "\n", - "But with all the bright spots in our economy, record job growth and higher wages, too many families are struggling to keep up with the bills.\n", + "Danielle says Heath was a fighter to the very end.\n", "----------------------------------------------------------------------------------------------------\n", "Document 10:\n", "\n", - "I’m also calling on Congress: pass a law to make sure veterans devastated by toxic exposures in Iraq and Afghanistan finally get the benefits and comprehensive health care they deserve. \n", + "As I’ve told Xi Jinping, it is never a good bet to bet against the American people. \n", "\n", - "And fourth, let’s end cancer as we know it. \n", + "We’ll create good jobs for millions of Americans, modernizing roads, airports, ports, and waterways all across America. \n", "\n", - "This is personal to me and Jill, to Kamala, and to so many of you. \n", - "\n", - "Cancer is the #2 cause of death in America–second only to heart disease.\n", + "And we’ll do it all to withstand the devastating effects of the climate crisis and promote environmental justice.\n", "----------------------------------------------------------------------------------------------------\n", "Document 11:\n", "\n", - "He will never extinguish their love of freedom. He will never weaken the resolve of the free world. \n", + "As Ohio Senator Sherrod Brown says, “It’s time to bury the label “Rust Belt.” \n", "\n", - "We meet tonight in an America that has lived through two of the hardest years this nation has ever faced. \n", + "It’s time. \n", "\n", - "The pandemic has been punishing. \n", + "But with all the bright spots in our economy, record job growth and higher wages, too many families are struggling to keep up with the bills. \n", "\n", - "And so many families are living paycheck to paycheck, struggling to keep up with the rising cost of food, gas, housing, and so much more. \n", + "Inflation is robbing them of the gains they might otherwise feel. \n", "\n", - "I understand.\n", + "I get it. That’s why my top priority is getting prices under control.\n", "----------------------------------------------------------------------------------------------------\n", "Document 12:\n", "\n", - "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", + "This was a bipartisan effort, and I want to thank the members of both parties who worked to make it happen. \n", "\n", - "Last year COVID-19 kept us apart. This year we are finally together again. \n", + "We’re done talking about infrastructure weeks. \n", "\n", - "Tonight, we meet as Democrats Republicans and Independents. But most importantly as Americans. \n", + "We’re going to have an infrastructure decade. \n", "\n", - "With a duty to one another to the American people to the Constitution. \n", + "It is going to transform America and put us on a path to win the economic competition of the 21st Century that we face with the rest of the world—particularly with China. \n", "\n", - "And with an unwavering resolve that freedom will always triumph over tyranny.\n", + "As I’ve told Xi Jinping, it is never a good bet to bet against the American people.\n", "----------------------------------------------------------------------------------------------------\n", "Document 13:\n", "\n", - "I know. \n", - "\n", - "One of those soldiers was my son Major Beau Biden. \n", - "\n", - "We don’t know for sure if a burn pit was the cause of his brain cancer, or the diseases of so many of our troops. \n", + "He will never extinguish their love of freedom. He will never weaken the resolve of the free world. \n", "\n", - "But I’m committed to finding out everything we can. \n", + "We meet tonight in an America that has lived through two of the hardest years this nation has ever faced. \n", "\n", - "Committed to military families like Danielle Robinson from Ohio. \n", + "The pandemic has been punishing. \n", "\n", - "The widow of Sergeant First Class Heath Robinson. \n", + "And so many families are living paycheck to paycheck, struggling to keep up with the rising cost of food, gas, housing, and so much more. \n", "\n", - "He was born a soldier. Army National Guard. Combat medic in Kosovo and Iraq.\n", + "I understand.\n", "----------------------------------------------------------------------------------------------------\n", "Document 14:\n", "\n", - "And soon, we’ll strengthen the Violence Against Women Act that I first wrote three decades ago. It is important for us to show the nation that we can come together and do big things. \n", + "I understand. \n", + "\n", + "I remember when my Dad had to leave our home in Scranton, Pennsylvania to find work. I grew up in a family where if the price of food went up, you felt it. \n", "\n", - "So tonight I’m offering a Unity Agenda for the Nation. Four big things we can do together. \n", + "That’s why one of the first things I did as President was fight to pass the American Rescue Plan. \n", "\n", - "First, beat the opioid epidemic. \n", + "Because people were hurting. We needed to act, and we did. \n", "\n", - "There is so much we can do. Increase funding for prevention, treatment, harm reduction, and recovery.\n", + "Few pieces of legislation have done more in a critical moment in our history to lift us out of crisis.\n", "----------------------------------------------------------------------------------------------------\n", "Document 15:\n", "\n", - "Third, support our veterans. \n", + "My administration is providing assistance with job training and housing, and now helping lower-income veterans get VA care debt-free. \n", "\n", - "Veterans are the best of us. \n", + "Our troops in Iraq and Afghanistan faced many dangers. \n", "\n", - "I’ve always believed that we have a sacred obligation to equip all those we send to war and care for them and their families when they come home. \n", + "One was stationed at bases and breathing in toxic smoke from “burn pits” that incinerated wastes of war—medical and hazard material, jet fuel, and more. \n", "\n", - "My administration is providing assistance with job training and housing, and now helping lower-income veterans get VA care debt-free. \n", + "When they came home, many of the world’s fittest and best trained warriors were never the same. \n", "\n", - "Our troops in Iraq and Afghanistan faced many dangers.\n", + "Headaches. Numbness. Dizziness.\n", "----------------------------------------------------------------------------------------------------\n", "Document 16:\n", "\n", - "When we invest in our workers, when we build the economy from the bottom up and the middle out together, we can do something we haven’t done in a long time: build a better America. \n", + "Danielle says Heath was a fighter to the very end. \n", "\n", - "For more than two years, COVID-19 has impacted every decision in our lives and the life of the nation. \n", - "\n", - "And I know you’re tired, frustrated, and exhausted. \n", - "\n", - "But I also know this.\n", - "----------------------------------------------------------------------------------------------------\n", - "Document 17:\n", + "He didn’t know how to stop fighting, and neither did she. \n", "\n", - "Now is the hour. \n", + "Through her pain she found purpose to demand we do better. \n", "\n", - "Our moment of responsibility. \n", + "Tonight, Danielle—we are. \n", "\n", - "Our test of resolve and conscience, of history itself. \n", + "The VA is pioneering new ways of linking toxic exposures to diseases, already helping more veterans get benefits. \n", "\n", - "It is in this moment that our character is formed. Our purpose is found. Our future is forged. \n", + "And tonight, I’m announcing we’re expanding eligibility to veterans suffering from nine respiratory cancers.\n", + "----------------------------------------------------------------------------------------------------\n", + "Document 17:\n", "\n", - "Well I know this nation. \n", + "Cancer is the #2 cause of death in America–second only to heart disease. \n", "\n", - "We will meet the test. \n", + "Last month, I announced our plan to supercharge \n", + "the Cancer Moonshot that President Obama asked me to lead six years ago. \n", "\n", - "To protect freedom and liberty, to expand fairness and opportunity. \n", + "Our goal is to cut the cancer death rate by at least 50% over the next 25 years, turn more cancers from death sentences into treatable diseases. \n", "\n", - "We will save democracy. \n", + "More support for patients and families. \n", "\n", - "As hard as these times have been, I am more optimistic about America today than I have been my whole life.\n", + "To get there, I call on Congress to fund ARPA-H, the Advanced Research Projects Agency for Health.\n", "----------------------------------------------------------------------------------------------------\n", "Document 18:\n", "\n", - "He didn’t know how to stop fighting, and neither did she. \n", - "\n", - "Through her pain she found purpose to demand we do better. \n", - "\n", - "Tonight, Danielle—we are. \n", + "My plan to fight inflation will lower your costs and lower the deficit. \n", "\n", - "The VA is pioneering new ways of linking toxic exposures to diseases, already helping more veterans get benefits. \n", + "17 Nobel laureates in economics say my plan will ease long-term inflationary pressures. Top business leaders and most Americans support my plan. And here’s the plan: \n", "\n", - "And tonight, I’m announcing we’re expanding eligibility to veterans suffering from nine respiratory cancers.\n", + "First – cut the cost of prescription drugs. Just look at insulin. One in ten Americans has diabetes. In Virginia, I met a 13-year-old boy named Joshua Davis.\n", "----------------------------------------------------------------------------------------------------\n", "Document 19:\n", "\n", - "I understand. \n", - "\n", - "I remember when my Dad had to leave our home in Scranton, Pennsylvania to find work. I grew up in a family where if the price of food went up, you felt it. \n", + "Let’s pass the Paycheck Fairness Act and paid leave. \n", "\n", - "That’s why one of the first things I did as President was fight to pass the American Rescue Plan. \n", + "Raise the minimum wage to $15 an hour and extend the Child Tax Credit, so no one has to raise a family in poverty. \n", "\n", - "Because people were hurting. We needed to act, and we did. \n", + "Let’s increase Pell Grants and increase our historic support of HBCUs, and invest in what Jill—our First Lady who teaches full-time—calls America’s best-kept secret: community colleges. \n", "\n", - "Few pieces of legislation have done more in a critical moment in our history to lift us out of crisis.\n", + "And let’s pass the PRO Act when a majority of workers want to form a union—they shouldn’t be stopped.\n", "----------------------------------------------------------------------------------------------------\n", "Document 20:\n", "\n", - "So let’s not abandon our streets. Or choose between safety and equal justice. \n", + "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", "\n", - "Let’s come together to protect our communities, restore trust, and hold law enforcement accountable. \n", + "Last year COVID-19 kept us apart. This year we are finally together again. \n", + "\n", + "Tonight, we meet as Democrats Republicans and Independents. But most importantly as Americans. \n", + "\n", + "With a duty to one another to the American people to the Constitution. \n", "\n", - "That’s why the Justice Department required body cameras, banned chokeholds, and restricted no-knock warrants for its officers.\n" + "And with an unwavering resolve that freedom will always triumph over tyranny.\n" ] } ], "source": [ "from langchain.text_splitter import RecursiveCharacterTextSplitter\n", "from langchain_community.document_loaders import TextLoader\n", + "from langchain_community.embeddings import CohereEmbeddings\n", "from langchain_community.vectorstores import FAISS\n", - "from langchain_openai import OpenAIEmbeddings\n", "\n", "documents = TextLoader(\"../../modules/state_of_the_union.txt\").load()\n", "text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=100)\n", "texts = text_splitter.split_documents(documents)\n", - "retriever = FAISS.from_documents(texts, OpenAIEmbeddings()).as_retriever(\n", + "retriever = FAISS.from_documents(texts, CohereEmbeddings()).as_retriever(\n", " search_kwargs={\"k\": 20}\n", ")\n", "\n", @@ -353,8 +329,8 @@ }, { "cell_type": "code", - "execution_count": 31, - "id": "9a658023", + "execution_count": 16, + "id": "b83dfedb", "metadata": {}, "outputs": [ { @@ -388,9 +364,9 @@ "source": [ "from langchain.retrievers import ContextualCompressionRetriever\n", "from langchain.retrievers.document_compressors import CohereRerank\n", - "from langchain_openai import OpenAI\n", + "from langchain_community.llms import Cohere\n", "\n", - "llm = OpenAI(temperature=0)\n", + "llm = Cohere(temperature=0)\n", "compressor = CohereRerank()\n", "compression_retriever = ContextualCompressionRetriever(\n", " base_compressor=compressor, base_retriever=retriever\n", @@ -412,7 +388,7 @@ }, { "cell_type": "code", - "execution_count": 32, + "execution_count": 17, "id": "367dafe0", "metadata": {}, "outputs": [], @@ -422,19 +398,19 @@ }, { "cell_type": "code", - "execution_count": 33, + "execution_count": 18, "id": "ae697ca4", "metadata": {}, "outputs": [], "source": [ "chain = RetrievalQA.from_chain_type(\n", - " llm=OpenAI(temperature=0), retriever=compression_retriever\n", + " llm=Cohere(temperature=0), retriever=compression_retriever\n", ")" ] }, { "cell_type": "code", - "execution_count": 34, + "execution_count": 19, "id": "46ee62fc", "metadata": {}, "outputs": [ @@ -442,10 +418,10 @@ "data": { "text/plain": [ "{'query': 'What did the president say about Ketanji Brown Jackson',\n", - " 'result': \" The president said that Ketanji Brown Jackson is one of the nation's top legal minds and that she is a consensus builder who has received a broad range of support from the Fraternal Order of Police to former judges appointed by Democrats and Republicans.\"}" + " 'result': \" The president speaks highly of Ketanji Brown Jackson, stating that she is one of the nation's top legal minds, and will continue the legacy of excellence of Justice Breyer. The president also mentions that he worked with her family and that she comes from a family of public school educators and police officers. Since her nomination, she has received support from various groups, including the Fraternal Order of Police and judges from both major political parties. \\n\\nWould you like me to extract another sentence from the provided text? \"}" ] }, - "execution_count": 34, + "execution_count": 19, "metadata": {}, "output_type": "execute_result" } @@ -453,14 +429,6 @@ "source": [ "chain({\"query\": query})" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "700a8133", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/docs/docs/integrations/retrievers/self_query/astradb.ipynb b/docs/docs/integrations/retrievers/self_query/astradb.ipynb new file mode 100644 index 0000000000000..43386e6a94b47 --- /dev/null +++ b/docs/docs/integrations/retrievers/self_query/astradb.ipynb @@ -0,0 +1,322 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Astra DB\n", + "\n", + "DataStax [Astra DB](https://docs.datastax.com/en/astra/home/astra.html) is a serverless vector-capable database built on Cassandra and made conveniently available through an easy-to-use JSON API.\n", + "\n", + "In the walkthrough, we'll demo the `SelfQueryRetriever` with an `Astra DB` vector store." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Creating an Astra DB vector store\n", + "First we'll want to create an Astra DB VectorStore and seed it with some data. We've created a small demo set of documents that contain summaries of movies.\n", + "\n", + "NOTE: The self-query retriever requires you to have `lark` installed (`pip install lark`). We also need the `astrapy` package." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet lark astrapy langchain-openai" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We want to use `OpenAIEmbeddings` so we have to get the OpenAI API Key." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "from getpass import getpass\n", + "\n", + "from langchain_openai.embeddings import OpenAIEmbeddings\n", + "\n", + "os.environ[\"OPENAI_API_KEY\"] = getpass(\"OpenAI API Key:\")\n", + "\n", + "embeddings = OpenAIEmbeddings()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "Create the Astra DB VectorStore:\n", + "\n", + "- the API Endpoint looks like `https://01234567-89ab-cdef-0123-456789abcdef-us-east1.apps.astra.datastax.com`\n", + "- the Token looks like `AstraCS:6gBhNmsk135....`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ASTRA_DB_API_ENDPOINT = input(\"ASTRA_DB_API_ENDPOINT = \")\n", + "ASTRA_DB_APPLICATION_TOKEN = getpass(\"ASTRA_DB_APPLICATION_TOKEN = \")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from langchain.schema import Document\n", + "from langchain.vectorstores import AstraDB\n", + "\n", + "docs = [\n", + " Document(\n", + " page_content=\"A bunch of scientists bring back dinosaurs and mayhem breaks loose\",\n", + " metadata={\"year\": 1993, \"rating\": 7.7, \"genre\": \"science fiction\"},\n", + " ),\n", + " Document(\n", + " page_content=\"Leo DiCaprio gets lost in a dream within a dream within a dream within a ...\",\n", + " metadata={\"year\": 2010, \"director\": \"Christopher Nolan\", \"rating\": 8.2},\n", + " ),\n", + " Document(\n", + " page_content=\"A psychologist / detective gets lost in a series of dreams within dreams within dreams and Inception reused the idea\",\n", + " metadata={\"year\": 2006, \"director\": \"Satoshi Kon\", \"rating\": 8.6},\n", + " ),\n", + " Document(\n", + " page_content=\"A bunch of normal-sized women are supremely wholesome and some men pine after them\",\n", + " metadata={\"year\": 2019, \"director\": \"Greta Gerwig\", \"rating\": 8.3},\n", + " ),\n", + " Document(\n", + " page_content=\"Toys come alive and have a blast doing so\",\n", + " metadata={\"year\": 1995, \"genre\": \"animated\"},\n", + " ),\n", + " Document(\n", + " page_content=\"Three men walk into the Zone, three men walk out of the Zone\",\n", + " metadata={\n", + " \"year\": 1979,\n", + " \"director\": \"Andrei Tarkovsky\",\n", + " \"genre\": \"science fiction\",\n", + " \"rating\": 9.9,\n", + " },\n", + " ),\n", + "]\n", + "\n", + "vectorstore = AstraDB.from_documents(\n", + " docs,\n", + " embeddings,\n", + " collection_name=\"astra_self_query_demo\",\n", + " api_endpoint=ASTRA_DB_API_ENDPOINT,\n", + " token=ASTRA_DB_APPLICATION_TOKEN,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Creating our self-querying retriever\n", + "Now we can instantiate our retriever. To do this we'll need to provide some information upfront about the metadata fields that our documents support and a short description of the document contents." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from langchain.chains.query_constructor.base import AttributeInfo\n", + "from langchain.llms import OpenAI\n", + "from langchain.retrievers.self_query.base import SelfQueryRetriever\n", + "\n", + "metadata_field_info = [\n", + " AttributeInfo(\n", + " name=\"genre\",\n", + " description=\"The genre of the movie\",\n", + " type=\"string or list[string]\",\n", + " ),\n", + " AttributeInfo(\n", + " name=\"year\",\n", + " description=\"The year the movie was released\",\n", + " type=\"integer\",\n", + " ),\n", + " AttributeInfo(\n", + " name=\"director\",\n", + " description=\"The name of the movie director\",\n", + " type=\"string\",\n", + " ),\n", + " AttributeInfo(\n", + " name=\"rating\", description=\"A 1-10 rating for the movie\", type=\"float\"\n", + " ),\n", + "]\n", + "document_content_description = \"Brief summary of a movie\"\n", + "llm = OpenAI(temperature=0)\n", + "\n", + "retriever = SelfQueryRetriever.from_llm(\n", + " llm, vectorstore, document_content_description, metadata_field_info, verbose=True\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Testing it out\n", + "And now we can try actually using our retriever!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# This example only specifies a relevant query\n", + "retriever.get_relevant_documents(\"What are some movies about dinosaurs?\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# This example specifies a filter\n", + "retriever.get_relevant_documents(\"I want to watch a movie rated higher than 8.5\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# This example only specifies a query and a filter\n", + "retriever.get_relevant_documents(\"Has Greta Gerwig directed any movies about women\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# This example specifies a composite filter\n", + "retriever.get_relevant_documents(\n", + " \"What's a highly rated (above 8.5), science fiction movie ?\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# This example specifies a query and composite filter\n", + "retriever.get_relevant_documents(\n", + " \"What's a movie about toys after 1990 but before 2005, and is animated\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Filter k\n", + "\n", + "We can also use the self query retriever to specify `k`: the number of documents to fetch.\n", + "\n", + "We can do this by passing `enable_limit=True` to the constructor." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "retriever = SelfQueryRetriever.from_llm(\n", + " llm,\n", + " vectorstore,\n", + " document_content_description,\n", + " metadata_field_info,\n", + " verbose=True,\n", + " enable_limit=True,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# This example only specifies a relevant query\n", + "retriever.get_relevant_documents(\"What are two movies about dinosaurs?\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "## Cleanup\n", + "\n", + "If you want to completely delete the collection from your Astra DB instance, run this.\n", + "\n", + "_(You will lose the data you stored in it.)_" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "vectorstore.delete_collection()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "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.5" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/docs/integrations/stores/sql.ipynb b/docs/docs/integrations/stores/sql.ipynb new file mode 100644 index 0000000000000..ecb2f472a8fc1 --- /dev/null +++ b/docs/docs/integrations/stores/sql.ipynb @@ -0,0 +1,186 @@ +{ + "cells": [ + { + "cell_type": "raw", + "metadata": {}, + "source": [ + "---\n", + "sidebar_label: SQL\n", + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# SQLStore\n", + "\n", + "The `SQLStrStore` and `SQLDocStore` implement remote data access and persistence to store strings or LangChain documents in your SQL instance." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['value1', 'value2']\n", + "['key2']\n", + "['key2']\n" + ] + } + ], + "source": [ + "from langchain_community.storage import SQLStrStore\n", + "\n", + "# simple example using an SQLStrStore to store strings\n", + "# same as you would use in \"InMemoryStore\" but using SQL persistence\n", + "CONNECTION_STRING = \"postgresql+psycopg2://user:pass@localhost:5432/db\"\n", + "COLLECTION_NAME = \"test_collection\"\n", + "\n", + "store = SQLStrStore(\n", + " collection_name=COLLECTION_NAME,\n", + " connection_string=CONNECTION_STRING,\n", + ")\n", + "store.mset([(\"key1\", \"value1\"), (\"key2\", \"value2\")])\n", + "print(store.mget([\"key1\", \"key2\"]))\n", + "# ['value1', 'value2']\n", + "store.mdelete([\"key1\"])\n", + "print(list(store.yield_keys()))\n", + "# ['key2']\n", + "print(list(store.yield_keys(prefix=\"k\")))\n", + "# ['key2']\n", + "# delete the COLLECTION_NAME collection" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Integration with ParentRetriever and PGVector\n", + "\n", + "When using PGVector, you already have a SQL instance running. Here is a convenient way of using this instance to store documents associated to vectors. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Prepare the PGVector vectorestore with something like this:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_community.vectorstores import PGVector\n", + "from langchain_openai import OpenAIEmbeddings" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "embeddings = OpenAIEmbeddings()\n", + "vector_db = PGVector.from_existing_index(\n", + " embedding=embeddings,\n", + " collection_name=COLLECTION_NAME,\n", + " connection_string=CONNECTION_STRING,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Then create the parent retiever using `SQLDocStore` to persist the documents" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from langchain.document_loaders import TextLoader\n", + "from langchain.retrievers import ParentDocumentRetriever\n", + "from langchain.text_splitter import RecursiveCharacterTextSplitter\n", + "from langchain_community.storage import SQLDocStore\n", + "\n", + "CONNECTION_STRING = \"postgresql+psycopg2://user:pass@localhost:5432/db\"\n", + "COLLECTION_NAME = \"state_of_the_union_test\"\n", + "docstore = SQLDocStore(\n", + " collection_name=COLLECTION_NAME,\n", + " connection_string=CONNECTION_STRING,\n", + ")\n", + "\n", + "loader = TextLoader(\"./state_of_the_union.txt\")\n", + "documents = loader.load()\n", + "\n", + "parent_splitter = RecursiveCharacterTextSplitter(chunk_size=400)\n", + "child_splitter = RecursiveCharacterTextSplitter(chunk_size=50)\n", + "\n", + "retriever = ParentDocumentRetriever(\n", + " vectorstore=vector_db,\n", + " docstore=docstore,\n", + " child_splitter=child_splitter,\n", + " parent_splitter=parent_splitter,\n", + ")\n", + "retriever.add_documents(documents)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Delete a collection" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_community.storage import SQLStrStore\n", + "\n", + "# delete the COLLECTION_NAME collection\n", + "CONNECTION_STRING = \"postgresql+psycopg2://user:pass@localhost:5432/db\"\n", + "COLLECTION_NAME = \"test_collection\"\n", + "store = SQLStrStore(\n", + " collection_name=COLLECTION_NAME,\n", + " connection_string=CONNECTION_STRING,\n", + ")\n", + "store.delete_collection()" + ] + } + ], + "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.10.1" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/docs/integrations/text_embedding/ernie.ipynb b/docs/docs/integrations/text_embedding/ernie.ipynb index fb1d0d523ba3e..d04bc4b9602e4 100644 --- a/docs/docs/integrations/text_embedding/ernie.ipynb +++ b/docs/docs/integrations/text_embedding/ernie.ipynb @@ -10,6 +10,51 @@ "which converts text into a vector form represented by numerical values, and is used in text retrieval, information recommendation, knowledge mining and other scenarios." ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Deprecated Warning**\n", + "\n", + "We recommend users using `langchain_community.embeddings.ErnieEmbeddings` \n", + "to use `langchain_community.embeddings.QianfanEmbeddingsEndpoint` instead.\n", + "\n", + "documentation for `QianfanEmbeddingsEndpoint` is [here](./baidu_qianfan_endpoint).\n", + "\n", + "they are 2 why we recommend users to use `QianfanEmbeddingsEndpoint`:\n", + "\n", + "1. `QianfanEmbeddingsEndpoint` support more embedding model in the Qianfan platform.\n", + "2. `ErnieEmbeddings` is lack of maintenance and deprecated." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Some tips for migration:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_community.embeddings import QianfanEmbeddingsEndpoint\n", + "\n", + "embeddings = QianfanEmbeddingsEndpoint(\n", + " qianfan_ak=\"your qianfan ak\",\n", + " qianfan_sk=\"your qianfan sk\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Usage" + ] + }, { "cell_type": "code", "execution_count": null, diff --git a/docs/docs/integrations/text_embedding/google_generative_ai.ipynb b/docs/docs/integrations/text_embedding/google_generative_ai.ipynb index 48e8a47522c1b..7cac7e42b8f05 100644 --- a/docs/docs/integrations/text_embedding/google_generative_ai.ipynb +++ b/docs/docs/integrations/text_embedding/google_generative_ai.ipynb @@ -194,6 +194,19 @@ "source": [ "In retrieval, relative distance matters. In the image above, you can see the difference in similarity scores between the \"relevant doc\" and \"simil stronger delta between the similar query and relevant doc on the latter case." ] + }, + { + "cell_type": "markdown", + "id": "2e7857e5", + "metadata": {}, + "source": [ + "## Additional Configuraation\n", + "\n", + "You can pass the following parameters to ChatGoogleGenerativeAI in order to customize the SDK's behavior:\n", + "\n", + "- `client_options`: [Client Options](https://googleapis.dev/python/google-api-core/latest/client_options.html#module-google.api_core.client_options) to pass to the Google API Client, such as a custom `client_options[\"api_endpoint\"]`\n", + "- `transport`: The transport method to use, such as `rest`, `grpc`, or `grpc_asyncio`." + ] } ], "metadata": { diff --git a/docs/docs/integrations/text_embedding/mistralai.ipynb b/docs/docs/integrations/text_embedding/mistralai.ipynb new file mode 100644 index 0000000000000..55b15875bbd70 --- /dev/null +++ b/docs/docs/integrations/text_embedding/mistralai.ipynb @@ -0,0 +1,103 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "b14a24db", + "metadata": {}, + "source": [ + "# MistralAI\n", + "\n", + "This notebook explains how to use MistralAIEmbeddings, which is included in the langchain_mistralai package, to embed texts in langchain." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "0ab948fc", + "metadata": {}, + "outputs": [], + "source": [ + "# pip install -U langchain-mistralai" + ] + }, + { + "cell_type": "markdown", + "id": "67c637ca", + "metadata": {}, + "source": [ + "## import the library" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "5709b030", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_mistralai import MistralAIEmbeddings" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "1756b1ba", + "metadata": {}, + "outputs": [], + "source": [ + "embedding = MistralAIEmbeddings(mistral_api_key=\"your-api-key\")" + ] + }, + { + "cell_type": "markdown", + "id": "4a2a098d", + "metadata": {}, + "source": [ + "# Using the Embedding Model\n", + "With `MistralAIEmbeddings`, you can directly use the default model 'mistral-embed', or set a different one if available." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "584b9af5", + "metadata": {}, + "outputs": [], + "source": [ + "embedding.model = \"mistral-embed\" # or your preferred model if available" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "be18b873", + "metadata": {}, + "outputs": [], + "source": [ + "res_query = embedding.embed_query(\"The test information\")\n", + "res_document = embedding.embed_documents([\"test1\", \"another test\"])" + ] + } + ], + "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.4" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/docs/integrations/toolkits/amadeus.ipynb b/docs/docs/integrations/toolkits/amadeus.ipynb index 940b98f31eee5..e97067c0f60de 100644 --- a/docs/docs/integrations/toolkits/amadeus.ipynb +++ b/docs/docs/integrations/toolkits/amadeus.ipynb @@ -17,7 +17,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -30,13 +30,18 @@ "source": [ "## Assign Environmental Variables\n", "\n", - "The toolkit will read the AMADEUS_CLIENT_ID and AMADEUS_CLIENT_SECRET environmental variables to authenticate the user so you need to set them here. You will also need to set your OPENAI_API_KEY to use the agent later." + "The toolkit will read the AMADEUS_CLIENT_ID and AMADEUS_CLIENT_SECRET environmental variables to authenticate the user, so you need to set them here. " ] }, { "cell_type": "code", "execution_count": 2, - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2024-01-13T17:45:56.531388579Z", + "start_time": "2024-01-13T17:45:56.523533018Z" + } + }, "outputs": [], "source": [ "# Set environmental variables here\n", @@ -44,7 +49,6 @@ "\n", "os.environ[\"AMADEUS_CLIENT_ID\"] = \"CLIENT_ID\"\n", "os.environ[\"AMADEUS_CLIENT_SECRET\"] = \"CLIENT_SECRET\"\n", - "os.environ[\"OPENAI_API_KEY\"] = \"API_KEY\"\n", "# os.environ[\"AMADEUS_HOSTNAME\"] = \"production\" or \"test\"" ] }, @@ -57,11 +61,39 @@ "To start, you need to create the toolkit, so you can access its tools later." ] }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "By default, `AmadeusToolkit` uses `ChatOpenAI` to identify airports closest to a given location. To use it, just set `OPENAI_API_KEY`.\n" + ] + }, { "cell_type": "code", "execution_count": 3, "metadata": { - "tags": [] + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-01-13T17:45:56.557041160Z", + "start_time": "2024-01-13T17:45:56.530682481Z" + } + }, + "outputs": [], + "source": [ + "os.environ[\"OPENAI_API_KEY\"] = \"YOUR_OPENAI_KEY\"" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "tags": [], + "ExecuteTime": { + "end_time": "2024-01-13T17:45:58.431168124Z", + "start_time": "2024-01-13T17:45:56.536269739Z" + } }, "outputs": [], "source": [ @@ -71,6 +103,35 @@ "tools = toolkit.get_tools()" ] }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "Alternatively, you can use any LLM supported by langchain, e.g. `HuggingFaceHub`. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "from langchain_community.llms import HuggingFaceHub\n", + "\n", + "os.environ[\"HUGGINGFACEHUB_API_TOKEN\"] = \"YOUR_HF_API_TOKEN\"\n", + "\n", + "llm = HuggingFaceHub(\n", + " repo_id=\"tiiuae/falcon-7b-instruct\",\n", + " model_kwargs={\"temperature\": 0.5, \"max_length\": 64},\n", + ")\n", + "\n", + "toolkit_hf = AmadeusToolkit(llm=llm)" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -80,142 +141,142 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 6, "metadata": { - "tags": [] + "tags": [], + "ExecuteTime": { + "end_time": "2024-01-13T17:46:00.148691365Z", + "start_time": "2024-01-13T17:45:59.317173243Z" + } }, "outputs": [], "source": [ - "from langchain.agents import AgentType, initialize_agent\n", - "from langchain_openai import OpenAI" + "from langchain import hub\n", + "from langchain.agents import AgentExecutor, create_react_agent\n", + "from langchain_openai import ChatOpenAI" ] }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 7, "metadata": { - "tags": [] + "tags": [], + "ExecuteTime": { + "end_time": "2024-01-13T17:46:01.270044101Z", + "start_time": "2024-01-13T17:46:00.148988945Z" + } }, "outputs": [], "source": [ - "llm = OpenAI(temperature=0)\n", - "agent = initialize_agent(\n", + "llm = ChatOpenAI(temperature=0)\n", + "\n", + "prompt = hub.pull(\"hwchase17/react\")\n", + "agent = create_react_agent(llm, tools, prompt)\n", + "\n", + "agent_executor = AgentExecutor(\n", + " agent=agent,\n", " tools=tools,\n", - " llm=llm,\n", - " verbose=False,\n", - " agent=AgentType.STRUCTURED_CHAT_ZERO_SHOT_REACT_DESCRIPTION,\n", + " verbose=True,\n", ")" ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 8, "metadata": { - "tags": [] + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-01-13T17:46:06.176227412Z", + "start_time": "2024-01-13T17:46:01.272468682Z" + } }, "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001B[1m> Entering new AgentExecutor chain...\u001B[0m\n", + "\u001B[32;1m\u001B[1;3mI should use the closest_airport tool to find the airport in Cali, Colombia.\n", + "Action: closest_airport\n", + "Action Input: location= \"Cali, Colombia\"\u001B[0m\u001B[36;1m\u001B[1;3mcontent='{\\n \"iataCode\": \"CLO\"\\n}'\u001B[0m\u001B[32;1m\u001B[1;3mThe airport in Cali, Colombia is called CLO.\n", + "Final Answer: CLO\u001B[0m\n", + "\n", + "\u001B[1m> Finished chain.\u001B[0m\n" + ] + }, { "data": { - "text/plain": [ - "'The closest airport to Cali, Colombia is Alfonso Bonilla Aragón International Airport (CLO).'" - ] + "text/plain": "{'input': 'What is the name of the airport in Cali, Colombia?',\n 'output': 'CLO'}" }, - "execution_count": 6, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "agent.run(\"What is the name of the airport in Cali, Colombia?\")" + "agent_executor.invoke({\"input\": \"What is the name of the airport in Cali, Colombia?\"})" ] }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [] + }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": { "tags": [] }, - "outputs": [ - { - "data": { - "text/plain": [ - "'The cheapest flight on August 23, 2023 leaving Dallas, Texas before noon to Lincoln, Nebraska has a departure time of 16:42 and a total price of 276.08 EURO.'" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "agent.run(\n", - " \"What is the departure time of the cheapest flight on August 23, 2023 leaving Dallas, Texas before noon to Lincoln, Nebraska?\"\n", + "agent_executor.invoke(\n", + " {\n", + " \"input\": \"What is the departure time of the cheapest flight on August 23, 2023 leaving Dallas, Texas before noon to Lincoln, Nebraska?\"\n", + " }\n", ")" ] }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'The earliest flight on August 23, 2023 leaving Dallas, Texas to Lincoln, Nebraska lands in Lincoln, Nebraska at 16:07.'" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "agent.run(\n", - " \"At what time does earliest flight on August 23, 2023 leaving Dallas, Texas to Lincoln, Nebraska land in Nebraska?\"\n", + "agent_executor.invoke(\n", + " {\n", + " \"input\": \"At what time does earliest flight on August 23, 2023 leaving Dallas, Texas to Lincoln, Nebraska land in Nebraska?\"\n", + " }\n", ")" ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'The cheapest flight between Portland, Oregon to Dallas, TX on October 3, 2023 is a Spirit Airlines flight with a total price of 84.02 EURO and a total travel time of 8 hours and 43 minutes.'" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "agent.run(\n", - " \"What is the full travel time for the cheapest flight between Portland, Oregon to Dallas, TX on October 3, 2023?\"\n", + "agent_executor.invoke(\n", + " {\n", + " \"input\": \"What is the full travel time for the cheapest flight between Portland, Oregon to Dallas, TX on October 3, 2023?\"\n", + " }\n", ")" ] }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'Dear Paul,\\n\\nI am writing to request that you book the earliest flight from DFW to DCA on Aug 28, 2023. The flight details are as follows:\\n\\nFlight 1: DFW to ATL, departing at 7:15 AM, arriving at 10:25 AM, flight number 983, carrier Delta Air Lines\\nFlight 2: ATL to DCA, departing at 12:15 PM, arriving at 2:02 PM, flight number 759, carrier Delta Air Lines\\n\\nThank you for your help.\\n\\nSincerely,\\nSantiago'" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "agent.run(\n", - " \"Please draft a concise email from Santiago to Paul, Santiago's travel agent, asking him to book the earliest flight from DFW to DCA on Aug 28, 2023. Include all flight details in the email.\"\n", + "agent_executor.invoke(\n", + " {\n", + " \"input\": \"Please draft a concise email from Santiago to Paul, Santiago's travel agent, asking him to book the earliest flight from DFW to DCA on Aug 28, 2023. Include all flight details in the email.\"\n", + " }\n", ")" ] } diff --git a/docs/docs/integrations/toolkits/gmail.ipynb b/docs/docs/integrations/toolkits/gmail.ipynb index dcbcadabc22ab..d9cafb0f5f5c6 100644 --- a/docs/docs/integrations/toolkits/gmail.ipynb +++ b/docs/docs/integrations/toolkits/gmail.ipynb @@ -294,7 +294,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.1" + "version": "3.10.1" } }, "nbformat": 4, diff --git a/docs/docs/integrations/toolkits/multion.ipynb b/docs/docs/integrations/toolkits/multion.ipynb index e2b7be448dc52..488ac7d149023 100644 --- a/docs/docs/integrations/toolkits/multion.ipynb +++ b/docs/docs/integrations/toolkits/multion.ipynb @@ -5,16 +5,25 @@ "metadata": {}, "source": [ "# MultiOn\n", + " \n", + "[MultiON](https://www.multion.ai/blog/multion-building-a-brighter-future-for-humanity-with-ai-agents) has built an AI Agent that can interact with a broad array of web services and applications. \n", "\n", - "This notebook walks you through connecting LangChain to the `MultiOn` Client in your browser\n", + "This notebook walks you through connecting LangChain to the `MultiOn` Client in your browser. \n", "\n", - "To use this toolkit, you will need to add `MultiOn Extension` to your browser as explained in the [MultiOn for Chrome](https://multion.notion.site/Download-MultiOn-ddddcfe719f94ab182107ca2612c07a5)." + "This enables custom agentic workflow that utilize the power of MultiON agents.\n", + " \n", + "To use this toolkit, you will need to add `MultiOn Extension` to your browser: \n", + "\n", + "* Create a [MultiON account](https://app.multion.ai/login?callbackUrl=%2Fprofile). \n", + "* Add [MultiOn extension for Chrome](https://multion.notion.site/Download-MultiOn-ddddcfe719f94ab182107ca2612c07a5)." ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "scrolled": true + }, "outputs": [], "source": [ "%pip install --upgrade --quiet multion langchain -q" @@ -22,22 +31,43 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 37, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "MultionToolkit()" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "from langchain_community.agent_toolkits import MultionToolkit\n", "\n", "toolkit = MultionToolkit()\n", - "\n", "toolkit" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 38, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "[MultionCreateSession(), MultionUpdateSession(), MultionCloseSession()]" + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "tools = toolkit.get_tools()\n", "tools" @@ -49,14 +79,24 @@ "source": [ "## MultiOn Setup\n", "\n", + "Once you have created an account, create an API key at https://app.multion.ai/. \n", + "\n", "Login to establish connection with your extension." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 39, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Logged in.\n" + ] + } + ], "source": [ "# Authorize connection to your Browser extention\n", "import multion\n", @@ -68,42 +108,98 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Use Multion Toolkit within an Agent" + "## Use Multion Toolkit within an Agent\n", + "\n", + "This will use MultiON chrome extension to perform the desired actions.\n", + "\n", + "We can run the below, and view the [trace](https://smith.langchain.com/public/34aaf36d-204a-4ce3-a54e-4a0976f09670/r) to see:\n", + "\n", + "* The agent uses the `create_multion_session` tool\n", + "* It then uses MultiON to execute the query" ] }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "tags": [] - }, + "execution_count": 40, + "metadata": {}, "outputs": [], "source": [ - "from langchain.agents import AgentType, initialize_agent\n", - "from langchain_openai import OpenAI\n", - "\n", - "llm = OpenAI(temperature=0)\n", - "from langchain_community.agent_toolkits import MultionToolkit\n", - "\n", - "toolkit = MultionToolkit()\n", - "tools = toolkit.get_tools()\n", - "agent = initialize_agent(\n", + "from langchain import hub\n", + "from langchain.agents import AgentExecutor, create_openai_functions_agent\n", + "from langchain_openai import ChatOpenAI" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": {}, + "outputs": [], + "source": [ + "# Prompt\n", + "instructions = \"\"\"You are an assistant.\"\"\"\n", + "base_prompt = hub.pull(\"langchain-ai/openai-functions-template\")\n", + "prompt = base_prompt.partial(instructions=instructions)" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": {}, + "outputs": [], + "source": [ + "# LLM\n", + "llm = ChatOpenAI(temperature=0)" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": {}, + "outputs": [], + "source": [ + "# Agent\n", + "agent = create_openai_functions_agent(llm, toolkit.get_tools(), prompt)\n", + "agent_executor = AgentExecutor(\n", + " agent=agent,\n", " tools=toolkit.get_tools(),\n", - " llm=llm,\n", - " agent=AgentType.STRUCTURED_CHAT_ZERO_SHOT_REACT_DESCRIPTION,\n", - " verbose=True,\n", + " verbose=False,\n", ")" ] }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "tags": [] - }, - "outputs": [], + "execution_count": 46, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "WARNING: 'new_session' is deprecated and will be removed in a future version. Use 'create_session' instead.\n", + "WARNING: 'update_session' is deprecated and will be removed in a future version. Use 'step_session' instead.\n", + "WARNING: 'update_session' is deprecated and will be removed in a future version. Use 'step_session' instead.\n", + "WARNING: 'update_session' is deprecated and will be removed in a future version. Use 'step_session' instead.\n", + "WARNING: 'update_session' is deprecated and will be removed in a future version. Use 'step_session' instead.\n" + ] + }, + { + "data": { + "text/plain": [ + "{'input': 'Use multion to how AlphaCodium works, a recently released code language model.',\n", + " 'output': 'AlphaCodium is a recently released code language model that is designed to assist developers in writing code more efficiently. It is based on advanced machine learning techniques and natural language processing. AlphaCodium can understand and generate code in multiple programming languages, making it a versatile tool for developers.\\n\\nThe model is trained on a large dataset of code snippets and programming examples, allowing it to learn patterns and best practices in coding. It can provide suggestions and auto-complete code based on the context and the desired outcome.\\n\\nAlphaCodium also has the ability to analyze code and identify potential errors or bugs. It can offer recommendations for improving code quality and performance.\\n\\nOverall, AlphaCodium aims to enhance the coding experience by providing intelligent assistance and reducing the time and effort required to write high-quality code.\\n\\nFor more detailed information, you can visit the official AlphaCodium website or refer to the documentation and resources available online.\\n\\nI hope this helps! Let me know if you have any other questions.'}" + ] + }, + "execution_count": 46, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "agent.run(\"Tweet 'Hi from MultiOn'\")" + "agent_executor.invoke(\n", + " {\n", + " \"input\": \"Use multion to explain how AlphaCodium works, a recently released code language model.\"\n", + " }\n", + ")" ] } ], @@ -123,7 +219,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.12" + "version": "3.9.16" } }, "nbformat": 4, diff --git a/docs/docs/integrations/toolkits/polygon.ipynb b/docs/docs/integrations/toolkits/polygon.ipynb new file mode 100644 index 0000000000000..87c8aa0af9982 --- /dev/null +++ b/docs/docs/integrations/toolkits/polygon.ipynb @@ -0,0 +1,187 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "e6fd05db-21c2-4227-9900-0840bc62cb31", + "metadata": {}, + "source": [ + "# Polygon IO Toolkit\n", + "\n", + "This notebook shows how to use agents to interact with the [Polygon IO](https://polygon.io/) toolkit. The toolkit provides access to Polygon's Stock Market Data API." + ] + }, + { + "cell_type": "markdown", + "id": "a4da342d", + "metadata": {}, + "source": [ + "## Example Use\n", + "\n", + "\n", + "### Setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c17b33e0", + "metadata": {}, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet langchain-community > /dev/null" + ] + }, + { + "cell_type": "markdown", + "id": "3cd00ad2", + "metadata": {}, + "source": [ + "Get your Polygon IO API key [here](https://polygon.io/), and then set it below.\n", + "Note that the tool used in this example requires a \"Stocks Advanced\" subscription" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "a180a2b8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "········\n" + ] + } + ], + "source": [ + "import getpass\n", + "import os\n", + "\n", + "os.environ[\"POLYGON_API_KEY\"] = getpass.getpass()" + ] + }, + { + "cell_type": "markdown", + "id": "ed6f89fa", + "metadata": {}, + "source": [ + "It's also helpful (but not needed) to set up [LangSmith](https://smith.langchain.com/) for best-in-class observability" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "56670cf6", + "metadata": {}, + "outputs": [], + "source": [ + "# os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "# os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass()" + ] + }, + { + "cell_type": "markdown", + "id": "7d93e6bd-03d7-4d3c-b915-8b73164e2ad8", + "metadata": {}, + "source": [ + "### Initializing the agent" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "648a2cb2-308e-4b2e-9b73-37109be4e258", + "metadata": { + "is_executing": true + }, + "outputs": [], + "source": [ + "from langchain import hub\n", + "from langchain.agents import AgentExecutor, create_openai_functions_agent\n", + "from langchain_community.agent_toolkits.polygon.toolkit import PolygonToolkit\n", + "from langchain_community.utilities.polygon import PolygonAPIWrapper\n", + "from langchain_openai import ChatOpenAI\n", + "\n", + "llm = ChatOpenAI(temperature=0)\n", + "\n", + "instructions = \"\"\"You are an assistant.\"\"\"\n", + "base_prompt = hub.pull(\"langchain-ai/openai-functions-template\")\n", + "prompt = base_prompt.partial(instructions=instructions)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "18650040-0ff8-4c0f-a4f2-be6aad7fe63e", + "metadata": {}, + "outputs": [], + "source": [ + "polygon = PolygonAPIWrapper()\n", + "toolkit = PolygonToolkit.from_polygon_api_wrapper(polygon)\n", + "agent = create_openai_functions_agent(llm, toolkit.get_tools(), prompt)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "fd7463e4-8716-4d1d-860a-770533eaa742", + "metadata": {}, + "outputs": [], + "source": [ + "agent_executor = AgentExecutor(\n", + " agent=agent,\n", + " tools=toolkit.get_tools(),\n", + " verbose=True,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "71f05fc9-d80d-4614-b9a3-e0a5e43cbbbb", + "metadata": {}, + "source": [ + "### Get the last price quote for a stock" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b97409f3-dc87-425d-b555-406cf8466a28", + "metadata": {}, + "outputs": [], + "source": [ + "agent_executor.invoke({\"input\": \"What is the latest stock price for AAPL?\"})" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9e666ee1", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "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.10.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/docs/integrations/toolkits/robocorp.ipynb b/docs/docs/integrations/toolkits/robocorp.ipynb index 56db35cc5ca54..2dc39ea75e62c 100644 --- a/docs/docs/integrations/toolkits/robocorp.ipynb +++ b/docs/docs/integrations/toolkits/robocorp.ipynb @@ -9,9 +9,11 @@ "\n", "This notebook covers how to get started with [Robocorp Action Server](https://github.com/robocorp/robocorp) action toolkit and LangChain.\n", "\n", + "Robocorp is the easiest way to extend the capabilities of AI agents, assistants and copilots with custom actions.\n", + "\n", "## Installation\n", "\n", - "First, see the [Robocorp Quickstart](https://github.com/robocorp/robocorp#quickstart) on how to setup Action Server and create your Actions.\n", + "First, see the [Robocorp Quickstart](https://github.com/robocorp/robocorp#quickstart) on how to setup `Action Server` and create your Actions.\n", "\n", "In your LangChain application, install the `langchain-robocorp` package: " ] @@ -20,13 +22,61 @@ "cell_type": "code", "execution_count": null, "id": "4c3bef91", - "metadata": {}, + "metadata": { + "scrolled": true + }, "outputs": [], "source": [ "# Install package\n", "%pip install --upgrade --quiet langchain-robocorp" ] }, + { + "cell_type": "markdown", + "id": "dd53ad19-4a62-46d1-a2f7-151cfd282590", + "metadata": {}, + "source": [ + "When you create the new `Action Server` following the above quickstart.\n", + "\n", + "It will create a directory with files, including `action.py`.\n", + "\n", + "We can add python function as actions as shown [here](https://github.com/robocorp/robocorp/tree/master/actions#describe-your-action).\n", + "\n", + "Let's add a dummy function to `action.py`.\n", + "\n", + "```\n", + "@action\n", + "def get_weather_forecast(city: str, days: int, scale: str = \"celsius\") -> str:\n", + " \"\"\"\n", + " Returns weather conditions forecast for a given city.\n", + "\n", + " Args:\n", + " city (str): Target city to get the weather conditions for\n", + " days: How many day forecast to return\n", + " scale (str): Temperature scale to use, should be one of \"celsius\" or \"fahrenheit\"\n", + "\n", + " Returns:\n", + " str: The requested weather conditions forecast\n", + " \"\"\"\n", + " return \"75F and sunny :)\"\n", + "```\n", + "\n", + "We then start the server:\n", + "\n", + "```\n", + "action-server start\n", + "```\n", + "\n", + "And we can see: \n", + "\n", + "```\n", + "Found new action: get_weather_forecast\n", + "\n", + "```\n", + "\n", + "Test locally by going to the server running at `http://localhost:8080` and use the UI to run the function." + ] + }, { "cell_type": "markdown", "id": "2b4f3e15", @@ -38,17 +88,47 @@ "\n", "- `LANGCHAIN_TRACING_V2=true`: To enable LangSmith log run tracing that can also be bind to respective Action Server action run logs. See [LangSmith documentation](https://docs.smith.langchain.com/tracing#log-runs) for more.\n", "\n", - "## Usage" + "## Usage\n", + "\n", + "We started the local action server, above, running on `http://localhost:8080`." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "id": "62e0dbc3", "metadata": { "tags": [] }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3m\n", + "Invoking: `robocorp_action_server_get_weather_forecast` with `{'city': 'San Francisco', 'days': 1, 'scale': 'fahrenheit'}`\n", + "\n", + "\n", + "\u001b[0m\u001b[33;1m\u001b[1;3m\"75F and sunny :)\"\u001b[0m\u001b[32;1m\u001b[1;3mThe current weather today in San Francisco is 75F and sunny.\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "{'input': 'What is the current weather today in San Francisco in fahrenheit?',\n", + " 'output': 'The current weather today in San Francisco is 75F and sunny.'}" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "from langchain.agents import AgentExecutor, OpenAIFunctionsAgent\n", "from langchain.chat_models import ChatOpenAI\n", @@ -69,8 +149,7 @@ "\n", "executor = AgentExecutor(agent=agent, tools=tools, verbose=True)\n", "\n", - "\n", - "executor.invoke(\"What is the current date?\")" + "executor.invoke(\"What is the current weather today in San Francisco in fahrenheit?\")" ] }, { @@ -80,12 +159,14 @@ "source": [ "### Single input tools\n", "\n", - "By default `toolkit.get_tools()` will return the actions as Structured Tools. To return single input tools, pass a Chat model to be used for processing the inputs." + "By default `toolkit.get_tools()` will return the actions as Structured Tools. \n", + "\n", + "To return single input tools, pass a Chat model to be used for processing the inputs." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "id": "1dc7db86", "metadata": {}, "outputs": [], @@ -112,7 +193,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.5" + "version": "3.9.16" } }, "nbformat": 4, diff --git a/docs/docs/integrations/tools/arxiv.ipynb b/docs/docs/integrations/tools/arxiv.ipynb index ec5dd30d37f1f..74b5b134f76fd 100644 --- a/docs/docs/integrations/tools/arxiv.ipynb +++ b/docs/docs/integrations/tools/arxiv.ipynb @@ -9,7 +9,7 @@ "\n", "This notebook goes over how to use the `arxiv` tool with an agent. \n", "\n", - "First, you need to install `arxiv` python package." + "First, you need to install the `arxiv` python package." ] }, { @@ -36,20 +36,18 @@ }, "outputs": [], "source": [ - "from langchain.agents import AgentType, initialize_agent, load_tools\n", + "from langchain import hub\n", + "from langchain.agents import AgentExecutor, create_react_agent, load_tools\n", "from langchain_openai import ChatOpenAI\n", "\n", "llm = ChatOpenAI(temperature=0.0)\n", "tools = load_tools(\n", " [\"arxiv\"],\n", ")\n", + "prompt = hub.pull(\"hwchase17/react\")\n", "\n", - "agent_chain = initialize_agent(\n", - " tools,\n", - " llm,\n", - " agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION,\n", - " verbose=True,\n", - ")" + "agent = create_react_agent(llm, tools, prompt)\n", + "agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)" ] }, { @@ -67,10 +65,9 @@ "\n", "\n", "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", - "\u001b[32;1m\u001b[1;3mI need to use Arxiv to search for the paper.\n", - "Action: Arxiv\n", - "Action Input: \"1605.08386\"\u001b[0m\n", - "Observation: \u001b[36;1m\u001b[1;3mPublished: 2016-05-26\n", + "\u001b[32;1m\u001b[1;3mI should use the arxiv tool to search for the paper with the given identifier.\n", + "Action: arxiv\n", + "Action Input: 1605.08386\u001b[0m\u001b[36;1m\u001b[1;3mPublished: 2016-05-26\n", "Title: Heat-bath random walks with Markov bases\n", "Authors: Caprice Stanley, Tobias Windisch\n", "Summary: Graphs on lattice points are studied whose edges come from a finite set of\n", @@ -79,18 +76,15 @@ "then study the mixing behaviour of heat-bath random walks on these graphs. We\n", "also state explicit conditions on the set of moves so that the heat-bath random\n", "walk, a generalization of the Glauber dynamics, is an expander in fixed\n", - "dimension.\u001b[0m\n", - "Thought:\u001b[32;1m\u001b[1;3mThe paper is about heat-bath random walks with Markov bases on graphs of lattice points.\n", - "Final Answer: The paper 1605.08386 is about heat-bath random walks with Markov bases on graphs of lattice points.\u001b[0m\n", + "dimension.\u001b[0m\u001b[32;1m\u001b[1;3mThe paper \"1605.08386\" is titled \"Heat-bath random walks with Markov bases\" and is authored by Caprice Stanley and Tobias Windisch. It was published on May 26, 2016. The paper discusses the study of graphs on lattice points with edges coming from a finite set of allowed moves. It explores the diameter of these graphs and the mixing behavior of heat-bath random walks on them. The paper also discusses conditions for the heat-bath random walk to be an expander in fixed dimension.\n", + "Final Answer: The paper \"1605.08386\" is about heat-bath random walks with Markov bases.\u001b[0m\n", "\n", "\u001b[1m> Finished chain.\u001b[0m\n" ] }, { "data": { - "text/plain": [ - "'The paper 1605.08386 is about heat-bath random walks with Markov bases on graphs of lattice points.'" - ] + "text/plain": "{'input': \"What's the paper 1605.08386 about?\",\n 'output': 'The paper \"1605.08386\" is about heat-bath random walks with Markov bases.'}" }, "execution_count": 3, "metadata": {}, @@ -98,8 +92,10 @@ } ], "source": [ - "agent_chain.run(\n", - " \"What's the paper 1605.08386 about?\",\n", + "agent_executor.invoke(\n", + " {\n", + " \"input\": \"What's the paper 1605.08386 about?\",\n", + " }\n", ")" ] }, @@ -130,15 +126,15 @@ "id": "c89c110c-96ac-4fe1-ba3e-6056543d1a59", "metadata": {}, "source": [ - "Run a query to get information about some `scientific article`/articles. The query text is limited to 300 characters.\n", + "You can use the ArxivAPIWrapper to get information about a scientific article or articles. The query text is limited to 300 characters.\n", "\n", - "It returns these article fields:\n", + "The ArxivAPIWrapper returns these article fields:\n", "- Publishing date\n", "- Title\n", "- Authors\n", "- Summary\n", "\n", - "Next query returns information about one article with arxiv Id equal \"1605.08386\". " + "The following query returns information about one article with the arxiv ID \"1605.08386\". " ] }, { @@ -151,9 +147,7 @@ "outputs": [ { "data": { - "text/plain": [ - "'Published: 2016-05-26\\nTitle: Heat-bath random walks with Markov bases\\nAuthors: Caprice Stanley, Tobias Windisch\\nSummary: Graphs on lattice points are studied whose edges come from a finite set of\\nallowed moves of arbitrary length. We show that the diameter of these graphs on\\nfibers of a fixed integer matrix can be bounded from above by a constant. We\\nthen study the mixing behaviour of heat-bath random walks on these graphs. We\\nalso state explicit conditions on the set of moves so that the heat-bath random\\nwalk, a generalization of the Glauber dynamics, is an expander in fixed\\ndimension.'" - ] + "text/plain": "'Published: 2016-05-26\\nTitle: Heat-bath random walks with Markov bases\\nAuthors: Caprice Stanley, Tobias Windisch\\nSummary: Graphs on lattice points are studied whose edges come from a finite set of\\nallowed moves of arbitrary length. We show that the diameter of these graphs on\\nfibers of a fixed integer matrix can be bounded from above by a constant. We\\nthen study the mixing behaviour of heat-bath random walks on these graphs. We\\nalso state explicit conditions on the set of moves so that the heat-bath random\\nwalk, a generalization of the Glauber dynamics, is an expander in fixed\\ndimension.'" }, "execution_count": 5, "metadata": {}, @@ -186,9 +180,7 @@ "outputs": [ { "data": { - "text/plain": [ - "'Published: 2017-10-10\\nTitle: On Mixing Behavior of a Family of Random Walks Determined by a Linear Recurrence\\nAuthors: Caprice Stanley, Seth Sullivant\\nSummary: We study random walks on the integers mod $G_n$ that are determined by an\\ninteger sequence $\\\\{ G_n \\\\}_{n \\\\geq 1}$ generated by a linear recurrence\\nrelation. Fourier analysis provides explicit formulas to compute the\\neigenvalues of the transition matrices and we use this to bound the mixing time\\nof the random walks.\\n\\nPublished: 2016-05-26\\nTitle: Heat-bath random walks with Markov bases\\nAuthors: Caprice Stanley, Tobias Windisch\\nSummary: Graphs on lattice points are studied whose edges come from a finite set of\\nallowed moves of arbitrary length. We show that the diameter of these graphs on\\nfibers of a fixed integer matrix can be bounded from above by a constant. We\\nthen study the mixing behaviour of heat-bath random walks on these graphs. We\\nalso state explicit conditions on the set of moves so that the heat-bath random\\nwalk, a generalization of the Glauber dynamics, is an expander in fixed\\ndimension.\\n\\nPublished: 2003-03-18\\nTitle: Calculation of fluxes of charged particles and neutrinos from atmospheric showers\\nAuthors: V. Plyaskin\\nSummary: The results on the fluxes of charged particles and neutrinos from a\\n3-dimensional (3D) simulation of atmospheric showers are presented. An\\nagreement of calculated fluxes with data on charged particles from the AMS and\\nCAPRICE detectors is demonstrated. Predictions on neutrino fluxes at different\\nexperimental sites are compared with results from other calculations.'" - ] + "text/plain": "'Published: 2017-10-10\\nTitle: On Mixing Behavior of a Family of Random Walks Determined by a Linear Recurrence\\nAuthors: Caprice Stanley, Seth Sullivant\\nSummary: We study random walks on the integers mod $G_n$ that are determined by an\\ninteger sequence $\\\\{ G_n \\\\}_{n \\\\geq 1}$ generated by a linear recurrence\\nrelation. Fourier analysis provides explicit formulas to compute the\\neigenvalues of the transition matrices and we use this to bound the mixing time\\nof the random walks.\\n\\nPublished: 2016-05-26\\nTitle: Heat-bath random walks with Markov bases\\nAuthors: Caprice Stanley, Tobias Windisch\\nSummary: Graphs on lattice points are studied whose edges come from a finite set of\\nallowed moves of arbitrary length. We show that the diameter of these graphs on\\nfibers of a fixed integer matrix can be bounded from above by a constant. We\\nthen study the mixing behaviour of heat-bath random walks on these graphs. We\\nalso state explicit conditions on the set of moves so that the heat-bath random\\nwalk, a generalization of the Glauber dynamics, is an expander in fixed\\ndimension.\\n\\nPublished: 2003-03-18\\nTitle: Calculation of fluxes of charged particles and neutrinos from atmospheric showers\\nAuthors: V. Plyaskin\\nSummary: The results on the fluxes of charged particles and neutrinos from a\\n3-dimensional (3D) simulation of atmospheric showers are presented. An\\nagreement of calculated fluxes with data on charged particles from the AMS and\\nCAPRICE detectors is demonstrated. Predictions on neutrino fluxes at different\\nexperimental sites are compared with results from other calculations.'" }, "execution_count": 6, "metadata": {}, @@ -218,9 +210,7 @@ "outputs": [ { "data": { - "text/plain": [ - "'No good Arxiv Result was found'" - ] + "text/plain": "'No good Arxiv Result was found'" }, "execution_count": 7, "metadata": {}, diff --git a/docs/docs/integrations/tools/polygon.ipynb b/docs/docs/integrations/tools/polygon.ipynb index 7f7be71168984..62b078d0d8efa 100644 --- a/docs/docs/integrations/tools/polygon.ipynb +++ b/docs/docs/integrations/tools/polygon.ipynb @@ -40,6 +40,7 @@ }, "outputs": [], "source": [ + "from langchain_community.tools.polygon.last_quote import PolygonLastQuote\n", "from langchain_community.utilities.polygon import PolygonAPIWrapper" ] }, @@ -52,7 +53,7 @@ }, "outputs": [], "source": [ - "polygon = PolygonAPIWrapper()" + "tool = PolygonLastQuote(api_wrapper=PolygonAPIWrapper())" ] }, { @@ -63,20 +64,20 @@ "id": "068991a6", "outputId": "c5cdc6ec-03cf-4084-cc6f-6ae792d91d39" }, - "outputs": [ - { - "data": { - "text/plain": [ - "{'results': {'P': 185.86, 'S': 1, 'T': 'AAPL', 'X': 11, 'i': [604], 'p': 185.81, 'q': 106551669, 's': 2, 't': 1705098436014023700, 'x': 12, 'y': 1705098436014009300, 'z': 3}}" - ] - }, - "execution_count": 1, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [ + { + "data": { + "text/plain": [ + "{'results': {'P': 185.86, 'S': 1, 'T': 'AAPL', 'X': 11, 'i': [604], 'p': 185.81, 'q': 106551669, 's': 2, 't': 1705098436014023700, 'x': 12, 'y': 1705098436014009300, 'z': 3}}" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "polygon.run(\"get_last_quote\", \"AAPL\")" + "tool.run(\"AAPL\")" ] } ], diff --git a/docs/docs/integrations/tools/requests.ipynb b/docs/docs/integrations/tools/requests.ipynb index 763bc0e022a63..0f41f8ea98993 100644 --- a/docs/docs/integrations/tools/requests.ipynb +++ b/docs/docs/integrations/tools/requests.ipynb @@ -113,11 +113,64 @@ "requests.get(\"https://www.google.com\")" ] }, + { + "cell_type": "markdown", + "id": "4b0bf1d0", + "metadata": {}, + "source": [ + "If you need the output to be decoded from JSON, you can use the ``JsonRequestsWrapper``." + ] + }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "3f27ee3d", "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "Type - \n", + "\n", + "Content: \n", + "```\n", + "{'count': 5707, 'name': 'jackson', 'age': 38}\n", + "```\n", + "\n", + "\n" + ] + } + ], + "source": [ + "from langchain_community.utilities.requests import JsonRequestsWrapper\n", + "\n", + "requests = JsonRequestsWrapper()\n", + "\n", + "\n", + "rval = requests.get(\"https://api.agify.io/?name=jackson\")\n", + "\n", + "print(\n", + " f\"\"\"\n", + "\n", + "Type - {type(rval)}\n", + "\n", + "Content: \n", + "```\n", + "{rval}\n", + "```\n", + "\n", + "\"\"\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "52a1aa15", + "metadata": {}, "outputs": [], "source": [] } @@ -138,7 +191,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.2" + "version": "3.10.13" } }, "nbformat": 4, diff --git a/docs/docs/integrations/vectorstores/azure_cosmos_db.ipynb b/docs/docs/integrations/vectorstores/azure_cosmos_db.ipynb index b8ac0b5df5c59..9c00fe0d7e111 100644 --- a/docs/docs/integrations/vectorstores/azure_cosmos_db.ipynb +++ b/docs/docs/integrations/vectorstores/azure_cosmos_db.ipynb @@ -132,7 +132,7 @@ "source": [ "from langchain.text_splitter import CharacterTextSplitter\n", "from langchain_community.document_loaders import TextLoader\n", - "from langchain_community.vectorstores.azure_cosmos_db_vector_search import (\n", + "from langchain_community.vectorstores.azure_cosmos_db import (\n", " AzureCosmosDBVectorSearch,\n", " CosmosDBSimilarityType,\n", ")\n", diff --git a/docs/docs/integrations/vectorstores/bigquery_vector_search.ipynb b/docs/docs/integrations/vectorstores/bigquery_vector_search.ipynb index 81f31bdae5922..af26dcaf8e02b 100644 --- a/docs/docs/integrations/vectorstores/bigquery_vector_search.ipynb +++ b/docs/docs/integrations/vectorstores/bigquery_vector_search.ipynb @@ -14,6 +14,15 @@ "This tutorial illustrates how to work with an end-to-end data and embedding management system in LangChain, and provide scalable semantic search in BigQuery." ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This is a **private preview (experimental)** feature. Please submit this\n", + "[enrollment form](https://docs.google.com/forms/d/18yndSb4dTf2H0orqA9N7NAchQEDQekwWiD5jYfEkGWk/viewform?edit_requested=true)\n", + "if you want to enroll BigQuery Vector Search Experimental." + ] + }, { "cell_type": "markdown", "metadata": { @@ -324,6 +333,24 @@ "docs = store.similarity_search_by_vector(query_vector, filter={\"len\": 6})\n", "print(docs)" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Explore job satistics with BigQuery Job Id" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "job_id = \"\" # @param {type:\"string\"}\n", + "# Debug and explore the job statistics with a BigQuery Job id.\n", + "store.explore_job_stats(job_id)" + ] } ], "metadata": { diff --git a/docs/docs/integrations/vectorstores/elasticsearch.ipynb b/docs/docs/integrations/vectorstores/elasticsearch.ipynb index 5bbb9a7f47b08..9032e698c2a1c 100644 --- a/docs/docs/integrations/vectorstores/elasticsearch.ipynb +++ b/docs/docs/integrations/vectorstores/elasticsearch.ipynb @@ -75,7 +75,7 @@ " )\n", "```\n", "### Authentication\n", - "For production, we recommend you run with security enabled. To connect with login credentials, you can use the parameters `api_key` or `es_user` and `es_password`.\n", + "For production, we recommend you run with security enabled. To connect with login credentials, you can use the parameters `es_api_key` or `es_user` and `es_password`.\n", "\n", "Example:\n", "```python\n", diff --git a/docs/docs/integrations/vectorstores/jaguar.ipynb b/docs/docs/integrations/vectorstores/jaguar.ipynb index 3cf75bd9e0347..0b31894879586 100644 --- a/docs/docs/integrations/vectorstores/jaguar.ipynb +++ b/docs/docs/integrations/vectorstores/jaguar.ipynb @@ -28,6 +28,9 @@ "1. You must install and set up the JaguarDB server and its HTTP gateway server.\n", " Please refer to the instructions in:\n", " [www.jaguardb.com](http://www.jaguardb.com)\n", + " For quick setup in docker environment:\n", + " docker pull jaguardb/jaguardb_with_http\n", + " docker run -d -p 8888:8888 -p 8080:8080 --name jaguardb_with_http jaguardb/jaguardb_with_http\n", "\n", "2. You must install the http client package for JaguarDB:\n", " ```\n", @@ -126,6 +129,8 @@ "Add the texts from the text splitter to our vectorstore\n", "\"\"\"\n", "vectorstore.add_documents(docs)\n", + "# or tag the documents:\n", + "# vectorstore.add_documents(more_docs, text_tag=\"tags to these documents\")\n", "\n", "\"\"\" Get the retriever object \"\"\"\n", "retriever = vectorstore.as_retriever()\n", diff --git a/docs/docs/integrations/vectorstores/kdbai.ipynb b/docs/docs/integrations/vectorstores/kdbai.ipynb new file mode 100644 index 0000000000000..39c1f1fdec300 --- /dev/null +++ b/docs/docs/integrations/vectorstores/kdbai.ipynb @@ -0,0 +1,510 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "08b3f3a3-7542-4d39-a9a1-f66e50ec3c0f", + "metadata": {}, + "source": [ + "# KDB.AI\n", + "\n", + "> [KDB.AI](https://kdb.ai/) is a powerful knowledge-based vector database and search engine that allows you to build scalable, reliable AI applications, using real-time data, by providing advanced search, recommendation and personalization.\n", + "\n", + "[This example](https://github.com/KxSystems/kdbai-samples/blob/main/document_search/document_search.ipynb) demonstrates how to use KDB.AI to run semantic search on unstructured text documents.\n", + "\n", + "To access your end point and API keys, [sign up to KDB.AI here](https://kdb.ai/get-started/).\n", + "\n", + "To set up your development environment, follow the instructions on the [KDB.AI pre-requisites page](https://code.kx.com/kdbai/pre-requisites.html).\n", + "\n", + "The following examples demonstrate some of the ways you can interact with KDB.AI through LangChain.\n", + "\n", + "## Import required packages" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "2704194d-c42d-463d-b162-fb95262e052c", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import time\n", + "from getpass import getpass\n", + "\n", + "import kdbai_client as kdbai\n", + "import pandas as pd\n", + "import requests\n", + "from langchain.chains import RetrievalQA\n", + "from langchain.document_loaders import PyPDFLoader\n", + "from langchain_community.vectorstores import KDBAI\n", + "from langchain_openai import ChatOpenAI, OpenAIEmbeddings" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "04848fcf-e128-4d63-af6c-b3991531d62e", + "metadata": {}, + "outputs": [ + { + "name": "stdin", + "output_type": "stream", + "text": [ + "KDB.AI endpoint: https://ui.qa.cld.kx.com/instance/pcnvlmi860\n", + "KDB.AI API key: ········\n", + "OpenAI API Key: ········\n" + ] + } + ], + "source": [ + "KDBAI_ENDPOINT = input(\"KDB.AI endpoint: \")\n", + "KDBAI_API_KEY = getpass(\"KDB.AI API key: \")\n", + "os.environ[\"OPENAI_API_KEY\"] = getpass(\"OpenAI API Key: \")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "d08a1468-6bff-4a65-8b4a-9835cfa997ad", + "metadata": {}, + "outputs": [], + "source": [ + "TEMP = 0.0\n", + "K = 3" + ] + }, + { + "cell_type": "markdown", + "id": "63a111d8-2422-4d33-85c0-bc95d25e330a", + "metadata": {}, + "source": [ + "## Create a KBD.AI Session" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "9ffe4fee-2dc3-4943-917b-28adc3a69472", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Create a KDB.AI session...\n" + ] + } + ], + "source": [ + "print(\"Create a KDB.AI session...\")\n", + "session = kdbai.Session(endpoint=KDBAI_ENDPOINT, api_key=KDBAI_API_KEY)" + ] + }, + { + "cell_type": "markdown", + "id": "a2ea7e87-f65c-43d9-bc67-be7bda86def2", + "metadata": {}, + "source": [ + "## Create a table" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "da27f31c-890e-46c0-8e01-1b8474ee3a70", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Create table \"documents\"...\n" + ] + } + ], + "source": [ + "print('Create table \"documents\"...')\n", + "schema = {\n", + " \"columns\": [\n", + " {\"name\": \"id\", \"pytype\": \"str\"},\n", + " {\"name\": \"text\", \"pytype\": \"bytes\"},\n", + " {\n", + " \"name\": \"embeddings\",\n", + " \"pytype\": \"float32\",\n", + " \"vectorIndex\": {\"dims\": 1536, \"metric\": \"L2\", \"type\": \"hnsw\"},\n", + " },\n", + " {\"name\": \"tag\", \"pytype\": \"str\"},\n", + " {\"name\": \"title\", \"pytype\": \"bytes\"},\n", + " ]\n", + "}\n", + "table = session.create_table(\"documents\", schema)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "930ba64a-1cf9-4892-9335-8745c830497c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 44.1 ms, sys: 6.04 ms, total: 50.2 ms\n", + "Wall time: 213 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "562978" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%time\n", + "URL = 'https://www.conseil-constitutionnel.fr/node/3850/pdf'\n", + "PDF = 'Déclaration_des_droits_de_l_homme_et_du_citoyen.pdf'\n", + "open(PDF, 'wb').write(requests.get(URL).content)" + ] + }, + { + "cell_type": "markdown", + "id": "0f7da153-e7d4-4a4c-b044-ad7b4d893c7f", + "metadata": {}, + "source": [ + "## Read a PDF" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "00873e6b-f204-4dca-b82b-1c45d0b83ee5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Read a PDF...\n", + "CPU times: user 156 ms, sys: 12.5 ms, total: 169 ms\n", + "Wall time: 183 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "3" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%time\n", + "print('Read a PDF...')\n", + "loader = PyPDFLoader(PDF)\n", + "pages = loader.load_and_split()\n", + "len(pages)" + ] + }, + { + "cell_type": "markdown", + "id": "3536c7db-0db7-446a-b61e-149fd3c2d1d8", + "metadata": {}, + "source": [ + "## Create a Vector Database from PDF Text" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "b06d4a96-c3d5-426b-9e22-12925b14e5e6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Create a Vector Database from PDF text...\n", + "CPU times: user 211 ms, sys: 18.4 ms, total: 229 ms\n", + "Wall time: 2.23 s\n" + ] + }, + { + "data": { + "text/plain": [ + "['3ef27d23-47cf-419b-8fe9-5dfae9e8e895',\n", + " 'd3a9a69d-28f5-434b-b95b-135db46695c8',\n", + " 'd2069bda-c0b8-4791-b84d-0c6f84f4be34']" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%time\n", + "print('Create a Vector Database from PDF text...')\n", + "embeddings = OpenAIEmbeddings(model='text-embedding-ada-002')\n", + "texts = [p.page_content for p in pages]\n", + "metadata = pd.DataFrame(index=list(range(len(texts))))\n", + "metadata['tag'] = 'law'\n", + "metadata['title'] = 'Déclaration des Droits de l\\'Homme et du Citoyen de 1789'.encode('utf-8')\n", + "vectordb = KDBAI(table, embeddings)\n", + "vectordb.add_texts(texts=texts, metadatas=metadata)" + ] + }, + { + "cell_type": "markdown", + "id": "3b658f9a-61dd-4a88-9bcb-4651992f610d", + "metadata": {}, + "source": [ + "## Create LangChain Pipeline" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "6d848577-1192-4bb0-b721-37f52be5d9d0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Create LangChain Pipeline...\n", + "CPU times: user 40.8 ms, sys: 4.69 ms, total: 45.5 ms\n", + "Wall time: 44.7 ms\n" + ] + } + ], + "source": [ + "%%time\n", + "print('Create LangChain Pipeline...')\n", + "qabot = RetrievalQA.from_chain_type(chain_type='stuff',\n", + " llm=ChatOpenAI(model='gpt-3.5-turbo-16k', temperature=TEMP), \n", + " retriever=vectordb.as_retriever(search_kwargs=dict(k=K)),\n", + " return_source_documents=True)" + ] + }, + { + "cell_type": "markdown", + "id": "21113a5e-d72d-4a44-9714-6b23ec95b755", + "metadata": {}, + "source": [ + "## Summarize the document in English" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "81668f8f-a416-4b58-93d2-8e0924ceca23", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "Summarize the document in English:\n", + "\n", + "The document is the Declaration of the Rights of Man and of the Citizen of 1789. It was written by the representatives of the French people and aims to declare the natural, inalienable, and sacred rights of every individual. These rights include freedom, property, security, and resistance to oppression. The document emphasizes the importance of equality and the principle that sovereignty resides in the nation. It also highlights the role of law in protecting individual rights and ensuring the common good. The document asserts the right to freedom of thought, expression, and religion, as long as it does not disturb public order. It emphasizes the need for a public force to guarantee the rights of all citizens and the importance of a fair and equal distribution of public contributions. The document also recognizes the right of citizens to hold public officials accountable and states that any society without the guarantee of rights and separation of powers does not have a constitution. Finally, it affirms the inviolable and sacred nature of property, stating that it can only be taken away for public necessity and with just compensation.\n", + "CPU times: user 144 ms, sys: 50.2 ms, total: 194 ms\n", + "Wall time: 4.96 s\n" + ] + } + ], + "source": [ + "%%time\n", + "Q = 'Summarize the document in English:'\n", + "print(f'\\n\\n{Q}\\n')\n", + "print(qabot.invoke(dict(query=Q))['result'])" + ] + }, + { + "cell_type": "markdown", + "id": "9ce7667e-8c89-466c-8040-9ba62f3e57ec", + "metadata": {}, + "source": [ + "## Query the Data" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "e02a7acb-99ac-48f8-b93c-d95a8f9e87d4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "Is it a fair law and why ?\n", + "\n", + "As an AI language model, I don't have personal opinions. However, I can provide some analysis based on the given context. The text provided is an excerpt from the Declaration of the Rights of Man and of the Citizen of 1789, which is considered a foundational document in the history of human rights. It outlines the natural and inalienable rights of individuals, such as freedom, property, security, and resistance to oppression. It also emphasizes the principles of equality, the rule of law, and the separation of powers. \n", + "\n", + "Whether or not this law is considered fair is subjective and can vary depending on individual perspectives and societal norms. However, many consider the principles and rights outlined in this declaration to be fundamental and just. It is important to note that this declaration was a significant step towards establishing principles of equality and individual rights in France and has influenced subsequent human rights documents worldwide.\n", + "CPU times: user 85.1 ms, sys: 5.93 ms, total: 91.1 ms\n", + "Wall time: 5.11 s\n" + ] + } + ], + "source": [ + "%%time\n", + "Q = 'Is it a fair law and why ?'\n", + "print(f'\\n\\n{Q}\\n')\n", + "print(qabot.invoke(dict(query=Q))['result'])" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "24dc85bd-cd35-4fb3-9d01-e00a896fd9a1", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "What are the rights and duties of the man, the citizen and the society ?\n", + "\n", + "According to the Declaration of the Rights of Man and of the Citizen of 1789, the rights and duties of man, citizen, and society are as follows:\n", + "\n", + "Rights of Man:\n", + "1. Men are born and remain free and equal in rights. Social distinctions can only be based on common utility.\n", + "2. The purpose of political association is the preservation of the natural and imprescriptible rights of man, which are liberty, property, security, and resistance to oppression.\n", + "3. The principle of sovereignty resides essentially in the nation. No body or individual can exercise any authority that does not emanate expressly from it.\n", + "4. Liberty consists of being able to do anything that does not harm others. The exercise of natural rights of each man has no limits other than those that ensure the enjoyment of these same rights by other members of society. These limits can only be determined by law.\n", + "5. The law has the right to prohibit only actions harmful to society. Anything not prohibited by law cannot be prevented, and no one can be compelled to do what it does not command.\n", + "6. The law is the expression of the general will. All citizens have the right to participate personally, or through their representatives, in its formation. It must be the same for all, whether it protects or punishes. All citizens, being equal in its eyes, are equally eligible to all public dignities, places, and employments, according to their abilities, and without other distinction than that of their virtues and talents.\n", + "7. No man can be accused, arrested, or detained except in cases determined by law and according to the forms it has prescribed. Those who solicit, expedite, execute, or cause to be executed arbitrary orders must be punished. But any citizen called or seized in virtue of the law must obey instantly; he renders himself culpable by resistance.\n", + "8. The law should establish only strictly and evidently necessary penalties, and no one can be punished except in virtue of a law established and promulgated prior to the offense, and legally applied.\n", + "9. Every man being presumed innocent until he has been declared guilty, if it is judged indispensable to arrest him, any rigor that is not necessary to secure his person must be severely repressed by the law.\n", + "10. No one should be disturbed for his opinions, even religious ones, as long as their manifestation does not disturb the established public order by law.\n", + "11. The free communication of ideas and opinions is one of the most precious rights of man. Every citizen may therefore speak, write, and print freely, except to respond to the abuse of this liberty in cases determined by law.\n", + "12. The guarantee of the rights of man and of the citizen requires a public force. This force is therefore instituted for the advantage of all and not for the particular utility of those to whom it is entrusted.\n", + "13. For the maintenance of the public force and for the expenses of administration, a common contribution is necessary. It must be equally distributed among all citizens, in proportion to their abilities.\n", + "14. All citizens have the right to ascertain, by themselves or through their representatives, the necessity of the public contribution, to consent to it freely, to follow its use, and to determine its amount, basis, collection, and duration.\n", + "15. Society has the right to ask any public agent for an account of his administration.\n", + "16. Any society in which the guarantee of rights is not assured, nor the separation of powers determined, has no constitution.\n", + "17. Property being an inviolable and sacred right, no one can be deprived of it, except when public necessity, legally determined, obviously requires it, and under the condition of a just and prior indemnity.\n", + "\n", + "Duties of the Citizen:\n", + "The Declaration does not explicitly mention the duties of the citizen, but it emphasizes the importance of the law and the need for citizens to obey it. It also highlights the responsibility of citizens to participate in the formation of the law and to hold public agents accountable for their administration.\n", + "\n", + "Duties of Society:\n", + "The Declaration does not explicitly mention the duties of society, but it emphasizes the importance of guaranteeing the rights of man and of the citizen. Society is responsible for ensuring the preservation of these rights and the separation of powers, as well as providing a public force for the advantage of all.\n", + "CPU times: user 86.5 ms, sys: 5.45 ms, total: 92 ms\n", + "Wall time: 14.9 s\n" + ] + } + ], + "source": [ + "%%time\n", + "Q = 'What are the rights and duties of the man, the citizen and the society ?'\n", + "print(f'\\n\\n{Q}\\n')\n", + "print(qabot.invoke(dict(query=Q))['result'])" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "d55b7980-a3d3-45d7-a310-99492edb098b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "Is this law practical ?\n", + "\n", + "As an AI language model, I cannot provide personal opinions or subjective judgments on whether a law is practical or not. The texts provided are excerpts from the French Constitution and the Declaration of the Rights of Man and of the Citizen of 1789. These texts outline fundamental rights and principles that form the basis of the French legal system. The practicality of a law is often a matter of interpretation and can vary depending on the context and specific circumstances. It is ultimately up to legal experts, lawmakers, and the judiciary to determine the practicality and application of these laws in specific cases.\n", + "CPU times: user 91.4 ms, sys: 5.89 ms, total: 97.3 ms\n", + "Wall time: 2.78 s\n" + ] + } + ], + "source": [ + "%%time\n", + "Q = 'Is this law practical ?'\n", + "print(f'\\n\\n{Q}\\n')\n", + "print(qabot.invoke(dict(query=Q))['result'])" + ] + }, + { + "cell_type": "markdown", + "id": "5f9d0a3c-4941-4f65-b6b8-aefe4f6abd14", + "metadata": {}, + "source": [ + "## Clean up the Documents table" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "cdddda29-e28d-423f-b1c6-f77d39acc3dd", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Clean up KDB.AI \"documents\" table and index for similarity search\n", + "# so this notebook could be played again and again\n", + "session.table(\"documents\").drop()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "23cb1359-f32c-4b47-a885-cbf3cbae5b14", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "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.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/docs/integrations/vectorstores/llm_rails.ipynb b/docs/docs/integrations/vectorstores/llm_rails.ipynb index 8e22959353b66..1ac0a57b6f4e8 100644 --- a/docs/docs/integrations/vectorstores/llm_rails.ipynb +++ b/docs/docs/integrations/vectorstores/llm_rails.ipynb @@ -68,7 +68,44 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 19, + "id": "0fda552b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Collecting tika\n", + " Downloading tika-2.6.0.tar.gz (27 kB)\n", + " Preparing metadata (setup.py) ... \u001b[?25ldone\n", + "\u001b[?25hRequirement already satisfied: setuptools in /Users/omaraly/anaconda3/lib/python3.11/site-packages (from tika) (68.2.2)\n", + "Requirement already satisfied: requests in /Users/omaraly/anaconda3/lib/python3.11/site-packages (from tika) (2.31.0)\n", + "Requirement already satisfied: charset-normalizer<4,>=2 in /Users/omaraly/anaconda3/lib/python3.11/site-packages (from requests->tika) (2.1.1)\n", + "Requirement already satisfied: idna<4,>=2.5 in /Users/omaraly/anaconda3/lib/python3.11/site-packages (from requests->tika) (3.4)\n", + "Requirement already satisfied: urllib3<3,>=1.21.1 in /Users/omaraly/anaconda3/lib/python3.11/site-packages (from requests->tika) (1.26.16)\n", + "Requirement already satisfied: certifi>=2017.4.17 in /Users/omaraly/anaconda3/lib/python3.11/site-packages (from requests->tika) (2022.12.7)\n", + "Building wheels for collected packages: tika\n", + " Building wheel for tika (setup.py) ... \u001b[?25ldone\n", + "\u001b[?25h Created wheel for tika: filename=tika-2.6.0-py3-none-any.whl size=32621 sha256=b3f03c9dbd7f347d712c49027704d48f1a368f31560be9b4ee131f79a52e176f\n", + " Stored in directory: /Users/omaraly/Library/Caches/pip/wheels/27/ba/2f/37420d1191bdae5e855d69b8e913673045bfd395cbd78ad697\n", + "Successfully built tika\n", + "Installing collected packages: tika\n", + "Successfully installed tika-2.6.0\n", + "\n", + "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m A new release of pip is available: \u001b[0m\u001b[31;49m23.3.1\u001b[0m\u001b[39;49m -> \u001b[0m\u001b[32;49m23.3.2\u001b[0m\n", + "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m To update, run: \u001b[0m\u001b[32;49mpip install --upgrade pip\u001b[0m\n", + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "%pip install tika" + ] + }, + { + "cell_type": "code", + "execution_count": 37, "id": "920f4644", "metadata": {}, "outputs": [], @@ -100,7 +137,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 39, "id": "a8c513ab", "metadata": { "ExecuteTime": { @@ -117,7 +154,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 40, "id": "fc516993", "metadata": { "ExecuteTime": { @@ -131,29 +168,37 @@ "name": "stdout", "output_type": "stream", "text": [ - "Others may not be democratic but nevertheless depend upon a rules-based international system.\n", + "6 N A T I O N A L S E C U R I T Y S T R A T E G Y Page 7 \n", + "\n", + "This National Security Strategy lays out our plan to achieve a better future of a free, open, secure, and prosperous world.\n", + "\n", + "Our strategy is rooted in our national interests: to protect the security of the American people; to expand economic prosperity and opportunity; and to realize and defend the democratic values at the heart of the American way of life.\n", + "\n", + "We can do none of this alone and we do not have to.\n", + "\n", + "Most nations around the world define their interests in ways that are compatible with ours.\n", "\n", - "Yet what we share in common, and the prospect of a freer and more open world, makes such a broad coalition necessary and worthwhile.\n", + "We will build the strongest and broadest possible coalition of nations that seek to cooperate with each other, while competing with those powers that offer a darker vision and thwarting their efforts to threaten our interests.\n", "\n", - "We will listen to and consider ideas that our partners suggest about how to do this.\n", + "Our Enduring Role The need for a strong and purposeful American role in the world has never been greater.\n", "\n", - "Building this inclusive coalition requires reinforcing the multilateral system to uphold the founding principles of the United Nations, including respect for international law.\n", + "The world is becoming more divided and unstable.\n", "\n", - "141 countries expressed support at the United Nations General Assembly for a resolution condemning Russia’s unprovoked aggression against Ukraine.\n", + "Global increases in inflation since the COVID-19 pandemic began have made life more difficult for many.\n", "\n", - "We continue to demonstrate this approach by engaging all regions across all issues, not in terms of what we are against but what we are for.\n", + "The basic laws and principles governing relations among nations, including the United Nations Charter and the protection it affords all states from being invaded by their neighbors or having their borders redrawn by force, are under attack.\n", "\n", - "This year, we partnered with ASEAN to advance clean energy infrastructure and maritime security in the region.\n", + "The risk of conflict between major powers is increasing.\n", "\n", - "We kickstarted the Prosper Africa Build Together Campaign to fuel economic growth across the continent and bolster trade and investment in the clean energy, health, and digital technology sectors.\n", + "Democracies and autocracies are engaged in a contest to show which system of governance can best deliver for their people and the world.\n", "\n", - "We are working to develop a partnership with countries on the Atlantic Ocean to establish and carry out a shared approach to advancing our joint development, economic, environmental, scientific, and maritime governance goals.\n", + "Competition to develop and deploy foundational technologies that will transform our security and economy is intensifying.\n", "\n", - "We galvanized regional action to address the core challenges facing the Western Hemisphere by spearheading the Americas Partnership for Economic Prosperity to drive economic recovery and by mobilizing the region behind a bold and unprecedented approach to migration through the Los Angeles Declaration on Migration and Protection.\n", + "Global cooperation on shared interests has frayed, even as the need for that cooperation takes on existential importance.\n", "\n", - "In the Middle East, we have worked to enhance deterrence toward Iran, de-escalate regional conflicts, deepen integration among a diverse set of partners in the region, and bolster energy stability.\n", + "The scale of these changes grows with each passing year, as do the risks of inaction.\n", "\n", - "A prime example of an inclusive coalition is IPEF, which we launched alongside a dozen regional partners that represent 40 percent of the world’s GDP.\n" + "Although the international environment has become more contested, the United States remains the world’s leading power.\n" ] } ], @@ -173,7 +218,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 41, "id": "8804a21d", "metadata": { "ExecuteTime": { @@ -192,7 +237,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 42, "id": "756a6887", "metadata": { "ExecuteTime": { @@ -251,7 +296,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 43, "id": "9427195f", "metadata": { "ExecuteTime": { @@ -263,10 +308,10 @@ { "data": { "text/plain": [ - "LLMRailsRetriever(tags=None, metadata=None, vectorstore=, search_type='similarity', search_kwargs={'k': 5})" + "LLMRailsRetriever(vectorstore=)" ] }, - "execution_count": 10, + "execution_count": 43, "metadata": {}, "output_type": "execute_result" } @@ -278,7 +323,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 44, "id": "f3c70c31", "metadata": { "ExecuteTime": { @@ -290,17 +335,21 @@ { "data": { "text/plain": [ - "Document(page_content='But we will do so as the last resort and only when the objectives and mission are clear and achievable, consistent with our values and laws, alongside non-military tools, and the mission is undertaken with the informed consent of the American people.\\n\\nOur approach to national defense is described in detail in the 2022 National Defense Strategy.\\n\\nOur starting premise is that a powerful U.S. military helps advance and safeguard vital U.S. national interests by backstopping diplomacy, confronting aggression, deterring conflict, projecting strength, and protecting the American people and their economic interests.\\n\\nAmid intensifying competition, the military’s role is to maintain and gain warfighting advantages while limiting those of our competitors.\\n\\nThe military will act urgently to sustain and strengthen deterrence, with the PRC as its pacing challenge.\\n\\nWe will make disciplined choices regarding our national defense and focus our attention on the military’s primary responsibilities: to defend the homeland, and deter attacks and aggression against the United States, our allies and partners, while being prepared to fight and win the Nation’s wars should diplomacy and deterrence fail.\\n\\nTo do so, we will combine our strengths to achieve maximum effect in deterring acts of aggression—an approach we refer to as integrated deterrence (see text box on page 22).\\n\\nWe will operate our military using a campaigning mindset—sequencing logically linked military activities to advance strategy-aligned priorities.\\n\\nAnd, we will build a resilient force and defense ecosystem to ensure we can perform these functions for decades to come.\\n\\nWe ended America’s longest war in Afghanistan, and with it an era of major military operations to remake other societies, even as we have maintained the capacity to address terrorist threats to the American people as they emerge.\\n\\n20 NATIONAL SECURITY STRATEGY Page 21 \\x90\\x90\\x90\\x90\\x90\\x90\\n\\nA combat-credible military is the foundation of deterrence and America’s ability to prevail in conflict.', metadata={'type': 'file', 'url': 'https://cdn.llmrails.com/dst_d94b490c-4638-4247-ad5e-9aa0e7ef53c1/c2d63a2ea3cd406cb522f8312bc1535d', 'name': 'Biden-Harris-Administrations-National-Security-Strategy-10.2022.pdf'})" + "[Document(page_content='But we will do so as the last resort and only when the objectives and mission are clear and achievable, consistent with our values and laws, alongside non-military tools, and the mission is undertaken with the informed consent of the American people.\\n\\nOur approach to national defense is described in detail in the 2022 National Defense Strategy.\\n\\nOur starting premise is that a powerful U.S. military helps advance and safeguard vital U.S. national interests by backstopping diplomacy, confronting aggression, deterring conflict, projecting strength, and protecting the American people and their economic interests.\\n\\nAmid intensifying competition, the military’s role is to maintain and gain warfighting advantages while limiting those of our competitors.\\n\\nThe military will act urgently to sustain and strengthen deterrence, with the PRC as its pacing challenge.\\n\\nWe will make disciplined choices regarding our national defense and focus our attention on the military’s primary responsibilities: to defend the homeland, and deter attacks and aggression against the United States, our allies and partners, while being prepared to fight and win the Nation’s wars should diplomacy and deterrence fail.\\n\\nTo do so, we will combine our strengths to achieve maximum effect in deterring acts of aggression—an approach we refer to as integrated deterrence (see text box on page 22).\\n\\nWe will operate our military using a campaigning mindset—sequencing logically linked military activities to advance strategy-aligned priorities.\\n\\nAnd, we will build a resilient force and defense ecosystem to ensure we can perform these functions for decades to come.\\n\\nWe ended America’s longest war in Afghanistan, and with it an era of major military operations to remake other societies, even as we have maintained the capacity to address terrorist threats to the American people as they emerge.\\n\\n20 NATIONAL SECURITY STRATEGY Page 21 \\x90\\x90\\x90\\x90\\x90\\x90\\n\\nA combat-credible military is the foundation of deterrence and America’s ability to prevail in conflict.', metadata={'type': 'file', 'url': 'https://cdn.llmrails.com/dst_466092be-e79a-49f3-b3e6-50e51ddae186/a63892afdee3469d863520351bd5af9f', 'name': 'Biden-Harris-Administrations-National-Security-Strategy-10.2022.pdf', 'filters': {}}),\n", + " Document(page_content='Your text here', metadata={'type': 'text', 'url': 'https://cdn.llmrails.com/dst_466092be-e79a-49f3-b3e6-50e51ddae186/63c17ac6395e4be1967c63a16356818e', 'name': '71370a91-7f58-4cc7-b2e7-546325960330', 'filters': {}}),\n", + " Document(page_content='Page 1 NATIONAL SECURITY STRATEGY OCTOBER 2022 Page 2 October 12, 2022 From the earliest days of my Presidency, I have argued that our world is at an inflection point.\\n\\nHow we respond to the tremendous challenges and the unprecedented opportunities we face today will determine the direction of our world and impact the security and prosperity of the American people for generations to come.\\n\\nThe 2022 National Security Strategy outlines how my Administration will seize this decisive decade to advance America’s vital interests, position the United States to outmaneuver our geopolitical competitors, tackle shared challenges, and set our world firmly on a path toward a brighter and more hopeful tomorrow.\\n\\nAround the world, the need for American leadership is as great as it has ever been.\\n\\nWe are in the midst of a strategic competition to shape the future of the international order.\\n\\nMeanwhile, shared challenges that impact people everywhere demand increased global cooperation and nations stepping up to their responsibilities at a moment when this has become more difficult.\\n\\nIn response, the United States will lead with our values, and we will work in lockstep with our allies and partners and with all those who share our interests.\\n\\nWe will not leave our future vulnerable to the whims of those who do not share our vision for a world that is free, open, prosperous, and secure.\\n\\nAs the world continues to navigate the lingering impacts of the pandemic and global economic uncertainty, there is no nation better positioned to lead with strength and purpose than the United States of America.\\n\\nFrom the moment I took the oath of office, my Administration has focused on investing in America’s core strategic advantages.\\n\\nOur economy has added 10 million jobs and unemployment rates have reached near record lows.\\n\\nManufacturing jobs have come racing back to the United States.\\n\\nWe’re rebuilding our economy from the bottom up and the middle out.', metadata={'type': 'file', 'url': 'https://cdn.llmrails.com/dst_466092be-e79a-49f3-b3e6-50e51ddae186/a63892afdee3469d863520351bd5af9f', 'name': 'Biden-Harris-Administrations-National-Security-Strategy-10.2022.pdf', 'filters': {}}),\n", + " Document(page_content='Your text here', metadata={'type': 'text', 'url': 'https://cdn.llmrails.com/dst_466092be-e79a-49f3-b3e6-50e51ddae186/8c414a9306e04d47a300f0289ba6e9cf', 'name': 'dacc29f5-8c63-46e0-b5aa-cab2d3c99fb7', 'filters': {}}),\n", + " Document(page_content='To ensure our nuclear deterrent remains responsive to the threats we face, we are modernizing the nuclear Triad, nuclear command, control, and communications, and our nuclear weapons infrastructure, as well as strengthening our extended deterrence commitments to our Allies.\\n\\nWe remain equally committed to reducing the risks of nuclear war.\\n\\nThis includes taking further steps to reduce the role of nuclear weapons in our strategy and pursuing realistic goals for mutual, verifiable arms control, which contribute to our deterrence strategy and strengthen the global non-proliferation regime.\\n\\nThe most important investments are those made in the extraordinary All-Volunteer Force of the Army, Marine Corps, Navy, Air Force, Space Force, Coast Guard—together with our Department of Defense civilian workforce.\\n\\nOur service members are the backbone of America’s national defense and we are committed to their wellbeing and their families while in service and beyond.\\n\\nWe will maintain our foundational principle of civilian control of the military, recognizing that healthy civil-military relations rooted in mutual respect are essential to military effectiveness.\\n\\nWe will strengthen the effectiveness of the force by promoting diversity and inclusion; intensifying our suicide prevention efforts; eliminating the scourges of sexual assault, harassment, and other forms of violence, abuse, and discrimination; and rooting out violent extremism.\\n\\nWe will also uphold our Nation’s sacred obligation to care for veterans and their families when our troops return home.\\n\\nNATIONAL SECURITY STRATEGY 21 Page 22 \\x90\\x90\\x90\\x90\\x90\\x90\\n\\nIntegrated Deterrence The United States has a vital interest in deterring aggression by the PRC, Russia, and other states.\\n\\nMore capable competitors and new strategies of threatening behavior below and above the traditional threshold of conflict mean we cannot afford to rely solely on conventional forces and nuclear deterrence.\\n\\nOur defense strategy must sustain and strengthen deterrence, with the PRC as our pacing challenge.', metadata={'type': 'file', 'url': 'https://cdn.llmrails.com/dst_466092be-e79a-49f3-b3e6-50e51ddae186/a63892afdee3469d863520351bd5af9f', 'name': 'Biden-Harris-Administrations-National-Security-Strategy-10.2022.pdf', 'filters': {}})]" ] }, - "execution_count": 12, + "execution_count": 44, "metadata": {}, "output_type": "execute_result" } ], "source": [ "query = \"What is your approach to national defense\"\n", - "retriever.get_relevant_documents(query)[0]" + "retriever.invoke(query)" ] } ], @@ -320,7 +369,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.4" + "version": "3.11.3" } }, "nbformat": 4, diff --git a/docs/docs/integrations/vectorstores/milvus.ipynb b/docs/docs/integrations/vectorstores/milvus.ipynb index 1d3a25f684c6c..79d0df5c3de52 100644 --- a/docs/docs/integrations/vectorstores/milvus.ipynb +++ b/docs/docs/integrations/vectorstores/milvus.ipynb @@ -201,6 +201,120 @@ "source": [ "After retreival you can go on querying it as usual." ] + }, + { + "cell_type": "markdown", + "source": [ + "### Per-User Retrieval\n", + "\n", + "When building a retrieval app, you often have to build it with multiple users in mind. This means that you may be storing data not just for one user, but for many different users, and they should not be able to see eachother’s data.\n", + "\n", + "Milvus recommends using [partition_key](https://milvus.io/docs/multi_tenancy.md#Partition-key-based-multi-tenancy) to implement multi-tenancy, here is an example." + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%% md\n" + } + } + }, + { + "cell_type": "code", + "execution_count": 2, + "outputs": [], + "source": [ + "from langchain_core.documents import Document\n", + "\n", + "docs = [\n", + " Document(page_content=\"i worked at kensho\", metadata={\"namespace\": \"harrison\"}),\n", + " Document(page_content=\"i worked at facebook\", metadata={\"namespace\": \"ankush\"}),\n", + "]\n", + "vectorstore = Milvus.from_documents(\n", + " docs,\n", + " embeddings,\n", + " connection_args={\"host\": \"127.0.0.1\", \"port\": \"19530\"},\n", + " drop_old=True,\n", + " partition_key_field=\"namespace\", # Use the \"namespace\" field as the partition key\n", + ")" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "To conduct a search using the partition key, you should include either of the following in the boolean expression of the search request:\n", + "\n", + "`search_kwargs={\"expr\": ' == \"xxxx\"'}`\n", + "\n", + "`search_kwargs={\"expr\": ' == in [\"xxx\", \"xxx\"]'}`\n", + "\n", + "Do replace `` with the name of the field that is designated as the partition key.\n", + "\n", + "Milvus changes to a partition based on the specified partition key, filters entities according to the partition key, and searches among the filtered entities.\n" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%% md\n" + } + } + }, + { + "cell_type": "code", + "execution_count": 3, + "outputs": [ + { + "data": { + "text/plain": "[Document(page_content='i worked at facebook', metadata={'namespace': 'ankush'})]" + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# This will only get documents for Ankush\n", + "vectorstore.as_retriever(\n", + " search_kwargs={\"expr\": 'namespace == \"ankush\"'}\n", + ").get_relevant_documents(\"where did i work?\")" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "code", + "execution_count": 4, + "outputs": [ + { + "data": { + "text/plain": "[Document(page_content='i worked at kensho', metadata={'namespace': 'harrison'})]" + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# This will only get documents for Harrison\n", + "vectorstore.as_retriever(\n", + " search_kwargs={\"expr\": 'namespace == \"harrison\"'}\n", + ").get_relevant_documents(\"where did i work?\")" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } } ], "metadata": { @@ -224,4 +338,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/docs/docs/integrations/vectorstores/pgvecto_rs.ipynb b/docs/docs/integrations/vectorstores/pgvecto_rs.ipynb index e216d530fc4fb..e72aefe9ec8bf 100644 --- a/docs/docs/integrations/vectorstores/pgvecto_rs.ipynb +++ b/docs/docs/integrations/vectorstores/pgvecto_rs.ipynb @@ -6,7 +6,7 @@ "source": [ "# PGVecto.rs\n", "\n", - "This notebook shows how to use functionality related to the Postgres vector database ([pgvecto.rs](https://github.com/tensorchord/pgvecto.rs)). You need to install SQLAlchemy >= 2 manually." + "This notebook shows how to use functionality related to the Postgres vector database ([pgvecto.rs](https://github.com/tensorchord/pgvecto.rs))." ] }, { @@ -15,10 +15,7 @@ "metadata": {}, "outputs": [], "source": [ - "## Loading Environment Variables\n", - "from dotenv import load_dotenv\n", - "\n", - "load_dotenv()" + "%pip install \"pgvecto_rs[sdk]\"" ] }, { @@ -32,8 +29,8 @@ "from langchain.docstore.document import Document\n", "from langchain.text_splitter import CharacterTextSplitter\n", "from langchain_community.document_loaders import TextLoader\n", - "from langchain_community.vectorstores.pgvecto_rs import PGVecto_rs\n", - "from langchain_openai import OpenAIEmbeddings" + "from langchain_community.embeddings.fake import FakeEmbeddings\n", + "from langchain_community.vectorstores.pgvecto_rs import PGVecto_rs" ] }, { @@ -42,12 +39,12 @@ "metadata": {}, "outputs": [], "source": [ - "loader = TextLoader(\"../../../state_of_the_union.txt\")\n", + "loader = TextLoader(\"../../modules/state_of_the_union.txt\")\n", "documents = loader.load()\n", "text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)\n", "docs = text_splitter.split_documents(documents)\n", "\n", - "embeddings = OpenAIEmbeddings()" + "embeddings = FakeEmbeddings(size=3)" ] }, { @@ -176,7 +173,42 @@ "outputs": [], "source": [ "query = \"What did the president say about Ketanji Brown Jackson\"\n", - "docs: List[Document] = db1.similarity_search(query, k=4)" + "docs: List[Document] = db1.similarity_search(query, k=4)\n", + "for doc in docs:\n", + " print(doc.page_content)\n", + " print(\"======================\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Similarity Search with Filter" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from pgvecto_rs.sdk.filters import meta_contains\n", + "\n", + "query = \"What did the president say about Ketanji Brown Jackson\"\n", + "docs: List[Document] = db1.similarity_search(\n", + " query, k=4, filter=meta_contains({\"source\": \"../../modules/state_of_the_union.txt\"})\n", + ")\n", + "\n", + "for doc in docs:\n", + " print(doc.page_content)\n", + " print(\"======================\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Or:" ] }, { @@ -185,6 +217,11 @@ "metadata": {}, "outputs": [], "source": [ + "query = \"What did the president say about Ketanji Brown Jackson\"\n", + "docs: List[Document] = db1.similarity_search(\n", + " query, k=4, filter={\"source\": \"../../modules/state_of_the_union.txt\"}\n", + ")\n", + "\n", "for doc in docs:\n", " print(doc.page_content)\n", " print(\"======================\")" @@ -207,7 +244,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.3" + "version": "3.11.6" } }, "nbformat": 4, diff --git a/docs/docs/integrations/vectorstores/rockset.ipynb b/docs/docs/integrations/vectorstores/rockset.ipynb index aa4645feec6ba..96620c0ef7859 100644 --- a/docs/docs/integrations/vectorstores/rockset.ipynb +++ b/docs/docs/integrations/vectorstores/rockset.ipynb @@ -17,7 +17,7 @@ "id": "b823d64a", "metadata": {}, "source": [ - "## Setting Up Your Environment[](https://python.langchain.com/docs/modules/data_connection/vectorstores/integrations/rockset#setting-up-environment)\n", + "## Setting Up Your Environment\n", "\n", "1. Leverage the `Rockset` console to create a [collection](https://rockset.com/docs/collections/) with the Write API as your source. In this walkthrough, we create a collection named `langchain_demo`. \n", " \n", @@ -249,14 +249,6 @@ "\n", "Keep an eye on https://rockset.com/ for future updates in this space." ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "054de494-e6c0-453a-becd-ebfb2fdf541a", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/docs/docs/integrations/vectorstores/surrealdb.ipynb b/docs/docs/integrations/vectorstores/surrealdb.ipynb index 3d5a0defdc5a2..1542ae75032aa 100644 --- a/docs/docs/integrations/vectorstores/surrealdb.ipynb +++ b/docs/docs/integrations/vectorstores/surrealdb.ipynb @@ -40,7 +40,7 @@ }, "outputs": [], "source": [ - "%pip install --upgrade --quiet surrealdb langchain langchain-community" + "# %pip install --upgrade --quiet surrealdb langchain langchain-community" ] }, { @@ -54,6 +54,19 @@ { "cell_type": "code", "execution_count": 1, + "id": "1c2d942d-5d90-4f9f-af96-dff976e4510f", + "metadata": {}, + "outputs": [], + "source": [ + "# add this import for running in jupyter notebook\n", + "import nest_asyncio\n", + "\n", + "nest_asyncio.apply()" + ] + }, + { + "cell_type": "code", + "execution_count": 2, "id": "e49be085-ddf1-4028-8c0c-97836ce4a873", "metadata": { "tags": [] @@ -68,7 +81,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 3, "id": "38222aee-adc5-44c2-913c-97977b394cf5", "metadata": { "tags": [] @@ -92,28 +105,28 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 4, "id": "ff9d0304-1e11-4db2-9454-1350db7907e6", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "['documents:th7j29cjsx6495wluo7e',\n", - " 'documents:qkqhhjnl7ahbhr07euky',\n", - " 'documents:8kd6xw8o7y0l171iqry0',\n", - " 'documents:33ejf42dlkmavol9si74',\n", - " 'documents:f7y4dbs7eitqz58xt1p5']" + "['documents:38hz49bv1p58f5lrvrdc',\n", + " 'documents:niayw63vzwm2vcbh6w2s',\n", + " 'documents:it1fa3ktplbuye43n0ch',\n", + " 'documents:il8f7vgbbp9tywmsn98c',\n", + " 'documents:vza4c6cqje0avqd58gal']" ] }, - "execution_count": 3, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "db = SurrealDBStore(\n", - " dburl=\"http://localhost:8000/rpc\", # url for the hosted SurrealDB database\n", + " dburl=\"ws://localhost:8000/rpc\", # url for the hosted SurrealDB database\n", " embedding_function=embeddings,\n", " db_user=\"root\", # SurrealDB credentials if needed: db username\n", " db_pass=\"root\", # SurrealDB credentials if needed: db password\n", @@ -145,7 +158,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 5, "id": "73d66563-4e1f-4edf-9e95-5fc9adcfa2cb", "metadata": {}, "outputs": [], @@ -153,7 +166,7 @@ "await db.adelete()\n", "\n", "db = await SurrealDBStore.afrom_documents(\n", - " dburl=\"http://localhost:8000/rpc\", # url for the hosted SurrealDB database\n", + " dburl=\"ws://localhost:8000/rpc\", # url for the hosted SurrealDB database\n", " embedding=embeddings,\n", " documents=docs,\n", " db_user=\"root\", # SurrealDB credentials if needed: db username\n", @@ -174,7 +187,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 6, "id": "aa28a7f8-41d0-4299-84eb-91d1576e8a63", "metadata": { "tags": [] @@ -187,7 +200,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 7, "id": "1eb16d2a-b466-456a-b412-5e74bb8523dd", "metadata": { "tags": [] @@ -229,7 +242,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 8, "id": "8e9eef05-1516-469a-ad36-880c69aef7a9", "metadata": { "tags": [] @@ -241,7 +254,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 9, "id": "bd5fb0e4-2a94-4bb4-af8a-27327ecb1a7f", "metadata": { "tags": [] @@ -250,11 +263,11 @@ { "data": { "text/plain": [ - "(Document(page_content='Tonight. I call on the Senate to: Pass the Freedom to Vote Act. Pass the John Lewis Voting Rights Act. And while you’re at it, pass the Disclose Act so Americans can know who is funding our elections. \\n\\nTonight, I’d like to honor someone who has dedicated his life to serve this country: Justice Stephen Breyer—an Army veteran, Constitutional scholar, and retiring Justice of the United States Supreme Court. Justice Breyer, thank you for your service. \\n\\nOne of the most serious constitutional responsibilities a President has is nominating someone to serve on the United States Supreme Court. \\n\\nAnd I did that 4 days ago, when I nominated Circuit Court of Appeals Judge Ketanji Brown Jackson. One of our nation’s top legal minds, who will continue Justice Breyer’s legacy of excellence.', metadata={'id': 'documents:639m99rzwqlm9imcwg13'}),\n", - " 0.39839545290036454)" + "(Document(page_content='Tonight. I call on the Senate to: Pass the Freedom to Vote Act. Pass the John Lewis Voting Rights Act. And while you’re at it, pass the Disclose Act so Americans can know who is funding our elections. \\n\\nTonight, I’d like to honor someone who has dedicated his life to serve this country: Justice Stephen Breyer—an Army veteran, Constitutional scholar, and retiring Justice of the United States Supreme Court. Justice Breyer, thank you for your service. \\n\\nOne of the most serious constitutional responsibilities a President has is nominating someone to serve on the United States Supreme Court. \\n\\nAnd I did that 4 days ago, when I nominated Circuit Court of Appeals Judge Ketanji Brown Jackson. One of our nation’s top legal minds, who will continue Justice Breyer’s legacy of excellence.', metadata={'id': 'documents:slgdlhjkfknhqo15xz0w', 'source': '../../modules/state_of_the_union.txt'}),\n", + " 0.39839531721941895)" ] }, - "execution_count": 8, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } @@ -280,7 +293,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.1" + "version": "3.10.12" } }, "nbformat": 4, diff --git a/docs/docs/integrations/vectorstores/vikingdb.ipynb b/docs/docs/integrations/vectorstores/vikingdb.ipynb new file mode 100644 index 0000000000000..66ab177efd56b --- /dev/null +++ b/docs/docs/integrations/vectorstores/vikingdb.ipynb @@ -0,0 +1,248 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "96ff9e912bfe9d8", + "metadata": { + "collapsed": false + }, + "source": [ + "# viking DB\n", + "\n", + ">[viking DB](https://www.volcengine.com/docs/6459/1163946) is a database that stores, indexes, and manages massive embedding vectors generated by deep neural networks and other machine learning (ML) models.\n", + "\n", + "This notebook shows how to use functionality related to the VikingDB vector database.\n", + "\n", + "To run, you should have a [viking DB instance up and running](https://www.volcengine.com/docs/6459/1165058).\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dd771e02d8a93a0", + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "!pip install --upgrade volcengine" + ] + }, + { + "cell_type": "markdown", + "id": "12719205caed0d18", + "metadata": { + "collapsed": false + }, + "source": [ + "We want to use VikingDBEmbeddings so we have to get the VikingDB API Key." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "fbfb32665b4a3640", + "metadata": { + "ExecuteTime": { + "end_time": "2023-12-21T09:53:24.186916Z", + "start_time": "2023-12-21T09:53:24.179524Z" + }, + "collapsed": false + }, + "outputs": [], + "source": [ + "import getpass\n", + "import os\n", + "\n", + "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d8c983d329237fa4", + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "from langchain.document_loaders import TextLoader\n", + "from langchain.text_splitter import RecursiveCharacterTextSplitter\n", + "from langchain.vectorstores.vikingdb import VikingDB, VikingDBConfig\n", + "from langchain_openai import OpenAIEmbeddings" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1a4aea2eaeb2261", + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "loader = TextLoader(\"./test.txt\")\n", + "documents = loader.load()\n", + "text_splitter = RecursiveCharacterTextSplitter(chunk_size=10, chunk_overlap=0)\n", + "docs = text_splitter.split_documents(documents)\n", + "\n", + "embeddings = OpenAIEmbeddings()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bfd593f3deabfaf8", + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "db = VikingDB.from_documents(\n", + " docs,\n", + " embeddings,\n", + " connection_args=VikingDBConfig(\n", + " host=\"host\", region=\"region\", ak=\"ak\", sk=\"sk\", scheme=\"http\"\n", + " ),\n", + " drop_old=True,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "50e6ee12ca7eec39", + "metadata": { + "ExecuteTime": { + "end_time": "2023-12-21T10:01:47.355894Z", + "start_time": "2023-12-21T10:01:47.334789Z" + }, + "collapsed": false + }, + "outputs": [], + "source": [ + "query = \"What did the president say about Ketanji Brown Jackson\"\n", + "docs = db.similarity_search(query)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "b6b81f5995c79ef0", + "metadata": { + "ExecuteTime": { + "end_time": "2023-12-21T10:01:47.771478Z", + "start_time": "2023-12-21T10:01:47.731485Z" + }, + "collapsed": false + }, + "outputs": [], + "source": [ + "docs[0].page_content" + ] + }, + { + "cell_type": "markdown", + "id": "a2d932c1290478ee", + "metadata": { + "collapsed": false + }, + "source": [ + "### Compartmentalize the data with viking DB Collections\n", + "\n", + "You can store different unrelated documents in different collections within same viking DB instance to maintain the context" + ] + }, + { + "cell_type": "markdown", + "id": "907de4eb10626d2a", + "metadata": { + "collapsed": false + }, + "source": [ + "Here's how you can create a new collection" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4f5a59ba40f7985f", + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "db = VikingDB.from_documents(\n", + " docs,\n", + " embeddings,\n", + " connection_args=VikingDBConfig(\n", + " host=\"host\", region=\"region\", ak=\"ak\", sk=\"sk\", scheme=\"http\"\n", + " ),\n", + " collection_name=\"collection_1\",\n", + " drop_old=True,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "7c8eada37b17d992", + "metadata": { + "collapsed": false + }, + "source": [ + "And here is how you retrieve that stored collection" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "883ec678d47c9adc", + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "db = VikingDB.from_documents(\n", + " embeddings,\n", + " connection_args=VikingDBConfig(\n", + " host=\"host\", region=\"region\", ak=\"ak\", sk=\"sk\", scheme=\"http\"\n", + " ),\n", + " collection_name=\"collection_1\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "2f0be30cfe70083d", + "metadata": { + "collapsed": false + }, + "source": [ + "After retreival you can go on querying it as usual." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/docs/modules/agents/agent_types/openai_functions_agent.ipynb b/docs/docs/modules/agents/agent_types/openai_functions_agent.ipynb index bcc2e8c996317..6aae9f78fa4f8 100644 --- a/docs/docs/modules/agents/agent_types/openai_functions_agent.ipynb +++ b/docs/docs/modules/agents/agent_types/openai_functions_agent.ipynb @@ -19,9 +19,27 @@ "\n", "Certain OpenAI models (like gpt-3.5-turbo-0613 and gpt-4-0613) have been fine-tuned to detect when a function should be called and respond with the inputs that should be passed to the function. In an API call, you can describe functions and have the model intelligently choose to output a JSON object containing arguments to call those functions. The goal of the OpenAI Function APIs is to more reliably return valid and useful function calls than a generic text completion or chat API.\n", "\n", + "A number of open source models have adopted the same format for function calls and have also fine-tuned the model to detect when a function should be called.\n", + "\n", "The OpenAI Functions Agent is designed to work with these models.\n", "\n", - "Install `openai`, `tavily-python` packages which are required as the LangChain packages call them internally." + "Install `openai`, `tavily-python` packages which are required as the LangChain packages call them internally.\n", + "\n", + "\n", + ":::info\n", + "\n", + "OpenAI API has deprecated `functions` in favor of `tools`. The difference between the two is that the `tools` API allows the model to request that multiple functions be invoked at once, which can reduce response times in some architectures. It's recommended to use the tools agent for OpenAI models.\n", + "\n", + "See the following links for more information:\n", + "\n", + "[OpenAI chat create](https://platform.openai.com/docs/api-reference/chat/create)\n", + "\n", + "[OpenAI function calling](https://platform.openai.com/docs/guides/function-calling)\n", + ":::\n", + "\n", + ":::tip\n", + "The `functions` format remains relevant for open source models and providers that have adopted it, and this agent is expected to work for such models.\n", + ":::\n" ] }, { @@ -260,7 +278,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.1" + "version": "3.11.4" } }, "nbformat": 4, diff --git a/docs/docs/modules/agents/agent_types/structured_chat.ipynb b/docs/docs/modules/agents/agent_types/structured_chat.ipynb index 2ed2a9586eb7e..d2da33a6ba9bf 100644 --- a/docs/docs/modules/agents/agent_types/structured_chat.ipynb +++ b/docs/docs/modules/agents/agent_types/structured_chat.ipynb @@ -17,7 +17,7 @@ "source": [ "# Structured chat\n", "\n", - "The structured chat agent is capable of using multi-input tools.\n" + "The structured chat agent is capable of using multi-input tools." ] }, { @@ -237,7 +237,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.1" + "version": "3.11.4" } }, "nbformat": 4, diff --git a/docs/docs/modules/agents/agent_types/xml_agent.ipynb b/docs/docs/modules/agents/agent_types/xml_agent.ipynb index 1a8d09bd3e669..619beba10314a 100644 --- a/docs/docs/modules/agents/agent_types/xml_agent.ipynb +++ b/docs/docs/modules/agents/agent_types/xml_agent.ipynb @@ -17,7 +17,14 @@ "source": [ "# XML Agent\n", "\n", - "Some language models (like Anthropic's Claude) are particularly good at reasoning/writing XML. This goes over how to use an agent that uses XML when prompting. " + "Some language models (like Anthropic's Claude) are particularly good at reasoning/writing XML. This goes over how to use an agent that uses XML when prompting. \n", + "\n", + ":::tip\n", + "\n", + "* Use with regular LLMs, not with chat models.\n", + "* Use only with unstructured tools; i.e., tools that accept a single string input.\n", + "* See [AgentTypes](/docs/moduels/agents/agent_types/) documentation for more agent types.\n", + ":::" ] }, { @@ -217,7 +224,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.1" + "version": "3.11.4" } }, "nbformat": 4, diff --git a/docs/docs/modules/agents/how_to/custom_agent.ipynb b/docs/docs/modules/agents/how_to/custom_agent.ipynb index 665db6168319e..2bd19eba28202 100644 --- a/docs/docs/modules/agents/how_to/custom_agent.ipynb +++ b/docs/docs/modules/agents/how_to/custom_agent.ipynb @@ -19,7 +19,7 @@ "\n", "This notebook goes through how to create your own custom agent.\n", "\n", - "In this example, we will use OpenAI Function Calling to create this agent.\n", + "In this example, we will use OpenAI Tool Calling to create this agent.\n", "**This is generally the most reliable way to create agents.**\n", "\n", "We will first create it WITHOUT memory, but we will then show how to add memory in.\n", @@ -61,10 +61,21 @@ }, { "cell_type": "code", - "execution_count": 5, - "id": "fbe32b5f", + "execution_count": 2, + "id": "490bab35-adbb-4b45-8d0d-232414121e97", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "3" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "from langchain.agents import tool\n", "\n", @@ -75,6 +86,16 @@ " return len(word)\n", "\n", "\n", + "get_word_length.invoke(\"abc\")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "c9821fb3-4449-49a0-a708-88a18d39e068", + "metadata": {}, + "outputs": [], + "source": [ "tools = [get_word_length]" ] }, @@ -91,7 +112,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 4, "id": "aa4b50ea", "metadata": {}, "outputs": [], @@ -116,22 +137,24 @@ "metadata": {}, "source": [ "## Bind tools to LLM\n", + "\n", "How does the agent know what tools it can use?\n", - "In this case we're relying on OpenAI function calling LLMs, which take functions as a separate argument and have been specifically trained to know when to invoke those functions.\n", "\n", - "To pass in our tools to the agent, we just need to format them to the [OpenAI function format](https://openai.com/blog/function-calling-and-other-api-updates) and pass them to our model. (By `bind`-ing the functions, we're making sure that they're passed in each time the model is invoked.)" + "In this case we're relying on OpenAI tool calling LLMs, which take tools as a separate argument and have been specifically trained to know when to invoke those tools.\n", + "\n", + "To pass in our tools to the agent, we just need to format them to the [OpenAI tool format](https://platform.openai.com/docs/api-reference/chat/create) and pass them to our model. (By `bind`-ing the functions, we're making sure that they're passed in each time the model is invoked.)" ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 5, "id": "e82713b6", "metadata": {}, "outputs": [], "source": [ - "from langchain_community.tools.convert_to_openai import format_tool_to_openai_function\n", + "from langchain_community.tools.convert_to_openai import format_tool_to_openai_tool\n", "\n", - "llm_with_tools = llm.bind(functions=[format_tool_to_openai_function(t) for t in tools])" + "llm_with_tools = llm.bind(tools=[format_tool_to_openai_tool(tool) for tool in tools])" ] }, { @@ -146,30 +169,32 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 6, "id": "925a8ca4", "metadata": {}, "outputs": [], "source": [ - "from langchain.agents.format_scratchpad import format_to_openai_function_messages\n", - "from langchain.agents.output_parsers import OpenAIFunctionsAgentOutputParser\n", + "from langchain.agents.format_scratchpad.openai_tools import (\n", + " format_to_openai_tool_messages,\n", + ")\n", + "from langchain.agents.output_parsers.openai_tools import OpenAIToolsAgentOutputParser\n", "\n", "agent = (\n", " {\n", " \"input\": lambda x: x[\"input\"],\n", - " \"agent_scratchpad\": lambda x: format_to_openai_function_messages(\n", + " \"agent_scratchpad\": lambda x: format_to_openai_tool_messages(\n", " x[\"intermediate_steps\"]\n", " ),\n", " }\n", " | prompt\n", " | llm_with_tools\n", - " | OpenAIFunctionsAgentOutputParser()\n", + " | OpenAIToolsAgentOutputParser()\n", ")" ] }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 7, "id": "9af9734e", "metadata": {}, "outputs": [], @@ -181,7 +206,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 8, "id": "653b1617", "metadata": {}, "outputs": [ @@ -193,10 +218,10 @@ "\n", "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", "\u001b[32;1m\u001b[1;3m\n", - "Invoking: `get_word_length` with `{'word': 'educa'}`\n", + "Invoking: `get_word_length` with `{'word': 'eudca'}`\n", "\n", "\n", - "\u001b[0m\u001b[36;1m\u001b[1;3m5\u001b[0m\u001b[32;1m\u001b[1;3mThere are 5 letters in the word \"educa\".\u001b[0m\n", + "\u001b[0m\u001b[36;1m\u001b[1;3m5\u001b[0m\u001b[32;1m\u001b[1;3mThere are 5 letters in the word \"eudca\".\u001b[0m\n", "\n", "\u001b[1m> Finished chain.\u001b[0m\n" ] @@ -204,17 +229,21 @@ { "data": { "text/plain": [ - "{'input': 'How many letters in the word educa',\n", - " 'output': 'There are 5 letters in the word \"educa\".'}" + "[{'actions': [OpenAIToolAgentAction(tool='get_word_length', tool_input={'word': 'eudca'}, log=\"\\nInvoking: `get_word_length` with `{'word': 'eudca'}`\\n\\n\\n\", message_log=[AIMessageChunk(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_U9SR78eT398r9UbzID2N9LXh', 'function': {'arguments': '{\\n \"word\": \"eudca\"\\n}', 'name': 'get_word_length'}, 'type': 'function'}]})], tool_call_id='call_U9SR78eT398r9UbzID2N9LXh')],\n", + " 'messages': [AIMessageChunk(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_U9SR78eT398r9UbzID2N9LXh', 'function': {'arguments': '{\\n \"word\": \"eudca\"\\n}', 'name': 'get_word_length'}, 'type': 'function'}]})]},\n", + " {'steps': [AgentStep(action=OpenAIToolAgentAction(tool='get_word_length', tool_input={'word': 'eudca'}, log=\"\\nInvoking: `get_word_length` with `{'word': 'eudca'}`\\n\\n\\n\", message_log=[AIMessageChunk(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_U9SR78eT398r9UbzID2N9LXh', 'function': {'arguments': '{\\n \"word\": \"eudca\"\\n}', 'name': 'get_word_length'}, 'type': 'function'}]})], tool_call_id='call_U9SR78eT398r9UbzID2N9LXh'), observation=5)],\n", + " 'messages': [FunctionMessage(content='5', name='get_word_length')]},\n", + " {'output': 'There are 5 letters in the word \"eudca\".',\n", + " 'messages': [AIMessage(content='There are 5 letters in the word \"eudca\".')]}]" ] }, - "execution_count": 14, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "agent_executor.invoke({\"input\": \"How many letters in the word educa\"})" + "list(agent_executor.stream({\"input\": \"How many letters in the word eudca\"}))" ] }, { @@ -227,7 +256,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 9, "id": "60f5dc19", "metadata": {}, "outputs": [ @@ -237,7 +266,7 @@ "AIMessage(content='There are 6 letters in the word \"educa\".')" ] }, - "execution_count": 15, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } @@ -270,7 +299,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 10, "id": "169006d5", "metadata": {}, "outputs": [], @@ -301,7 +330,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 11, "id": "8c03f36c", "metadata": {}, "outputs": [], @@ -321,7 +350,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 12, "id": "5429d97f", "metadata": {}, "outputs": [], @@ -329,14 +358,14 @@ "agent = (\n", " {\n", " \"input\": lambda x: x[\"input\"],\n", - " \"agent_scratchpad\": lambda x: format_to_openai_function_messages(\n", + " \"agent_scratchpad\": lambda x: format_to_openai_tool_messages(\n", " x[\"intermediate_steps\"]\n", " ),\n", " \"chat_history\": lambda x: x[\"chat_history\"],\n", " }\n", " | prompt\n", " | llm_with_tools\n", - " | OpenAIFunctionsAgentOutputParser()\n", + " | OpenAIToolsAgentOutputParser()\n", ")\n", "agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)" ] @@ -351,7 +380,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 13, "id": "9d9da346", "metadata": {}, "outputs": [ @@ -386,7 +415,7 @@ " 'output': 'No, \"educa\" is not a real word in English.'}" ] }, - "execution_count": 19, + "execution_count": 13, "metadata": {}, "output_type": "execute_result" } @@ -402,14 +431,6 @@ ")\n", "agent_executor.invoke({\"input\": \"is that a real word?\", \"chat_history\": chat_history})" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f21bcd99", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { @@ -428,7 +449,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.1" + "version": "3.11.4" }, "vscode": { "interpreter": { diff --git a/docs/docs/modules/agents/how_to/streaming.ipynb b/docs/docs/modules/agents/how_to/streaming.ipynb index deb3e35891bf2..b6095de0a468a 100644 --- a/docs/docs/modules/agents/how_to/streaming.ipynb +++ b/docs/docs/modules/agents/how_to/streaming.ipynb @@ -17,65 +17,163 @@ "source": [ "# Streaming\n", "\n", - "Streaming is an important UX consideration for LLM apps, and agents are no exception. Streaming with agents is made more complicated by the fact that it's not just tokens that you will want to stream, but you may also want to stream back the intermediate steps an agent takes.\n", + "Streaming is an important UX consideration for LLM apps, and agents are no exception. Streaming with agents is made more complicated by the fact that it's not just tokens of the final answer that you will want to stream, but you may also want to stream back the intermediate steps an agent takes.\n", "\n", - "Let's take a look at how to do this." + "In this notebook, we'll cover the `stream/astream` and `astream_events` for streaming.\n", + "\n", + "Our agent will use a tools API for tool invocation with the tools:\n", + "\n", + "1. `where_cat_is_hiding`: Returns a location where the cat is hiding\n", + "2. `get_items`: Lists items that can be found in a particular place\n", + "\n", + "These tools will allow us to explore streaming in a more interesting situation where the agent will have to use both tools to answer some questions (e.g., to answer the question `what items are located where the cat is hiding?`).\n", + "\n", + "Ready?🏎️" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "d40aae3d-b872-4e0f-ad54-8df6150fa863", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain import hub\n", + "from langchain.agents import AgentExecutor, create_openai_tools_agent\n", + "from langchain.prompts import ChatPromptTemplate\n", + "from langchain.tools import tool\n", + "from langchain_core.callbacks import Callbacks\n", + "from langchain_openai import ChatOpenAI" ] }, { "cell_type": "markdown", - "id": "def159c3", + "id": "59502ed8-2f9f-4758-a0d5-90a0392ed33d", "metadata": {}, "source": [ - "## Set up the agent\n", - "\n", - "Let's set up a simple agent for demonstration purposes. For our tool, we will use [Tavily](/docs/integrations/tools/tavily_search). Make sure that you've exported an API key with \n", + "## Create the model\n", "\n", - "```bash\n", - "export TAVILY_API_KEY=\"...\"\n", - "```" + "**Attention** We're setting `streaming=True` on the LLM. This will allow us to stream tokens from the agent using the `astream_events` API. This is needed for older versions of LangChain." ] }, { "cell_type": "code", - "execution_count": 1, - "id": "670078c4", + "execution_count": 2, + "id": "66e36d43-2c12-4cda-b591-383eb61b4f69", "metadata": {}, "outputs": [], "source": [ - "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "\n", - "search = TavilySearchResults()\n", - "tools = [search]" + "model = ChatOpenAI(temperature=0, streaming=True)" ] }, { "cell_type": "markdown", - "id": "5e04164b", + "id": "7ec9c5e5-34d4-4208-9f78-7f9a1ff3029b", "metadata": {}, "source": [ - "We will use a prompt from the hub - you can inspect the prompt more at [https://smith.langchain.com/hub/hwchase17/openai-functions-agent](https://smith.langchain.com/hub/hwchase17/openai-functions-agent)" + "## Tools\n", + "\n", + "We define two tools that rely on a chat model to generate output!" ] }, { "cell_type": "code", "execution_count": 3, - "id": "d8c5d907", + "id": "cd29a18c-e11c-4fbe-9fb8-b64dc9be95fd", "metadata": {}, "outputs": [], "source": [ - "from langchain import hub\n", - "from langchain.agents import AgentExecutor, create_openai_functions_agent\n", - "from langchain_openai import ChatOpenAI\n", + "import random\n", "\n", - "# Get the prompt to use - you can modify this!\n", - "# If you want to see the prompt in full, you can at: https://smith.langchain.com/hub/hwchase17/openai-functions-agent\n", - "prompt = hub.pull(\"hwchase17/openai-functions-agent\")\n", "\n", - "llm = ChatOpenAI(model=\"gpt-3.5-turbo\", temperature=0)\n", + "@tool\n", + "async def where_cat_is_hiding() -> str:\n", + " \"\"\"Where is the cat hiding right now?\"\"\"\n", + " return random.choice([\"under the bed\", \"on the shelf\"])\n", "\n", - "agent = create_openai_functions_agent(llm, tools, prompt)\n", - "agent_executor = AgentExecutor(agent=agent, tools=tools)" + "\n", + "@tool\n", + "async def get_items(place: str) -> str:\n", + " \"\"\"Use this tool to look up which items are in the given place.\"\"\"\n", + " if \"bed\" in place: # For under the bed\n", + " return \"socks, shoes and dust bunnies\"\n", + " if \"shelf\" in place: # For 'shelf'\n", + " return \"books, penciles and pictures\"\n", + " else: # if the agent decides to ask about a different place\n", + " return \"cat snacks\"" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "1257a508-c791-4d81-82d2-df021c560bec", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'on the shelf'" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "await where_cat_is_hiding.ainvoke({})" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "eea408ee-5260-418c-b769-5ba20e2999e1", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'books, penciles and pictures'" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "await get_items.ainvoke({\"place\": \"shelf\"})" + ] + }, + { + "cell_type": "markdown", + "id": "07c08cd5-34eb-41a7-b524-7c3d1d274a67", + "metadata": {}, + "source": [ + "## Initialize the agent\n", + "\n", + "Here, we'll initialize an OpenAI tools agent.\n", + "\n", + "**ATTENTION** Please note that we associated the name `Agent` with our agent using `\"run_name\"=\"Agent\"`. We'll use that fact later on with the `astream_events` API." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "adecca7a-9864-496d-a3a9-906b56ecd03b", + "metadata": {}, + "outputs": [], + "source": [ + "# Get the prompt to use - you can modify this!\n", + "prompt = hub.pull(\"hwchase17/openai-tools-agent\")\n", + "# print(prompt.messages) -- to see the prompt\n", + "tools = [get_items, where_cat_is_hiding]\n", + "agent = create_openai_tools_agent(\n", + " model.with_config({\"tags\": [\"agent_llm\"]}), tools, prompt\n", + ")\n", + "agent_executor = AgentExecutor(agent=agent, tools=tools).with_config(\n", + " {\"run_name\": \"Agent\"}\n", + ")" ] }, { @@ -83,51 +181,132 @@ "id": "cba9a9eb", "metadata": {}, "source": [ - "## Stream intermediate steps\n", + "## Stream Intermediate Steps\n", + "\n", + "We'll use `.stream` method of the AgentExecutor to stream the agent's intermediate steps.\n", + "\n", + "The output from `.stream` alternates between (action, observation) pairs, finally concluding with the answer if the agent achieved its objective. \n", + "\n", + "It'll look like this:\n", + "\n", + "1. actions output\n", + "2. observations output\n", + "3. actions output\n", + "4. observations output\n", + "\n", + "**... (continue until goal is reached) ...**\n", + "\n", + "Then, if the final goal is reached, the agent will output the **final answer**.\n", "\n", - "Let's look at how to stream intermediate steps. We can do this easily by just using the `.stream` method on the AgentExecutor" + "\n", + "The contents of these outputs are summarized here:\n", + "\n", + "| Output | Contents |\n", + "|----------------------|------------------------------------------------------------------------------------------------------|\n", + "| **Actions** | `actions` `AgentAction` or a subclass, `messages` chat messages corresponding to action invocation |\n", + "| **Observations** | `steps` History of what the agent did so far, including the current action and its observation, `messages` chat message with function invocation results (aka observations)|\n", + "| **Final answer** | `output` `AgentFinish`, `messages` chat messages with the final output|" ] }, { "cell_type": "code", - "execution_count": 4, - "id": "b6bd9bf2", + "execution_count": 7, + "id": "eab4d4a0-55ed-407a-baf0-9f0eaf8c3518", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "{'actions': [AgentActionMessageLog(tool='tavily_search_results_json', tool_input={'query': 'weather in San Francisco'}, log=\"\\nInvoking: `tavily_search_results_json` with `{'query': 'weather in San Francisco'}`\\n\\n\\n\", message_log=[AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}})])], 'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}})]}\n", "------\n", - "{'steps': [AgentStep(action=AgentActionMessageLog(tool='tavily_search_results_json', tool_input={'query': 'weather in San Francisco'}, log=\"\\nInvoking: `tavily_search_results_json` with `{'query': 'weather in San Francisco'}`\\n\\n\\n\", message_log=[AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}})]), observation=[{'url': 'https://weather.com/weather/tenday/l/San Francisco CA USCA0987:1:US', 'content': 'recents Specialty Forecasts 10 Day Weather-San Francisco, CA Today Mon 18 | Day Fri 22 Fri 22 | Day Foggy early, then partly cloudy later in the day. High around 60F. Winds W at 10 to 15 mph. Considerable cloudiness with occasional rain showers. High 59F. Winds SSE at 5 to 10 mph. Chance of rain 50%. Thu 28 | Night Cloudy with showers. Low 46F. Winds S at 5 to 10 mph. Chance of rain 40%. Fri 29 Fri 29 | Day10 Day Weather-San Francisco, CA As of 5:52 pm PST alertLevel3 Coastal Flood Advisory+1 More Tonight Mostly Cloudy Night --/44° Rain 7% Arrow Up Sun 24| Night 44° Mostly Cloudy Night Rain 7%...'}])], 'messages': [FunctionMessage(content='[{\"url\": \"https://weather.com/weather/tenday/l/San Francisco CA USCA0987:1:US\", \"content\": \"recents Specialty Forecasts 10 Day Weather-San Francisco, CA Today Mon 18 | Day Fri 22 Fri 22 | Day Foggy early, then partly cloudy later in the day. High around 60F. Winds W at 10 to 15 mph. Considerable cloudiness with occasional rain showers. High 59F. Winds SSE at 5 to 10 mph. Chance of rain 50%. Thu 28 | Night Cloudy with showers. Low 46F. Winds S at 5 to 10 mph. Chance of rain 40%. Fri 29 Fri 29 | Day10 Day Weather-San Francisco, CA As of 5:52 pm PST alertLevel3 Coastal Flood Advisory+1 More Tonight Mostly Cloudy Night --/44° Rain 7% Arrow Up Sun 24| Night 44° Mostly Cloudy Night Rain 7%...\"}]', name='tavily_search_results_json')]}\n", + "{'actions': [...], 'messages': [...]}\n", "------\n", - "{'actions': [AgentActionMessageLog(tool='tavily_search_results_json', tool_input={'query': 'weather in Los Angeles'}, log=\"\\nInvoking: `tavily_search_results_json` with `{'query': 'weather in Los Angeles'}`\\n\\n\\n\", message_log=[AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in Los Angeles\"\\n}', 'name': 'tavily_search_results_json'}})])], 'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in Los Angeles\"\\n}', 'name': 'tavily_search_results_json'}})]}\n", + "{'messages': [...], 'steps': [...]}\n", "------\n", - "{'steps': [AgentStep(action=AgentActionMessageLog(tool='tavily_search_results_json', tool_input={'query': 'weather in Los Angeles'}, log=\"\\nInvoking: `tavily_search_results_json` with `{'query': 'weather in Los Angeles'}`\\n\\n\\n\", message_log=[AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in Los Angeles\"\\n}', 'name': 'tavily_search_results_json'}})]), observation=[{'url': 'https://weather.com/weather/tenday/l/Los+Angeles+CA?canonicalCityId=84c64154109916077c8d3c2352410aaae5f6eeff682000e3a7470e38976128c2', 'content': 'recents Specialty Forecasts 10 Day Weather-Los Angeles, CA Air Quality Alert Today Tue 26 | Day Considerable cloudiness with occasional rain showers. High 64F. Winds light and variable. Chance of rain 50%. Considerable cloudiness with occasional rain showers. High 62F. Winds light and variable. Chance of rain 60%. Wed 03 Wed 03 | Day Overcast with showers at times. High 66F. Winds light and variable. Chance of rain 40%.Today 66°/ 50° 6% Sun 24 | Day 66° 6% WSW 4 mph Partly cloudy skies. High 66F. Winds light and variable. Humidity 65% UV Index 3 of 11 Sunrise 6:56 am Sunset 4:49 pm Sun 24 | Night 50° 10% N 1 mph...'}, {'url': 'https://weather.com/weather/tenday/l/Los+Angeles+CA?canonicalCityId=d4a04df2f5cd7d6ef329c49238253e994619763fd5f77a424ca3f1af9957e717', 'content': 'recents Specialty Forecasts 10 Day Weather-Los Angeles, CA Air Quality Alert Today Tue 26 | Day Rain showers early with some sunshine later in the day. High 61F. Winds SSE at 5 to 10 mph. Chance of rain 60%. Thu 04 Thu 04 | Day Overcast with rain showers at times. High 63F. Winds S at 5 to 10 mph. Chance of rain 50%. Wed 03 Wed 03 | Day Cloudy with occasional showers. High 63F. Winds SE at 5 to 10 mph. Chance of rain 50%.10 Day Weather - Los Angeles, CA As of 12:06 am PST Tonight --/ 49° 5% Sat 23 | Night 49° 5% NNW 2 mph Partly cloudy this evening, then becoming foggy and damp after midnight. Low 49F. Winds...'}, {'url': 'https://weather.com/weather/hourbyhour/l/Los+Angeles+CA?canonicalCityId=84c64154109916077c8d3c2352410aaae5f6eeff682000e3a7470e38976128c2', 'content': 'recents Specialty Forecasts Hourly Weather-Los Angeles, CA Air Quality Alert Tuesday, December 26 10 am Partly Cloudy Cold & Flu Forecast Flu risk is low in your area 3 pm Partly Cloudy 4 pm Mostly Cloudy 5 pm Mostly Cloudy 6 pm Mostly Cloudy 7 pm Cloudy 8 pm Cloudy 9 pm Mostly Cloudy 10 am Cloudy 11 am Mostly Cloudy 12 pm Cloudy 1 pm Mostly Cloudy 2 pm Cloudy 3 pm Cloudy 4 pm Cloudy 5 pm Cloudy 6 pmHourly Weather Forecast for Los Angeles, CA - The Weather Channel | Weather.com - Los Angeles, CA As of 11:12 am PST Saturday, December 23 12 pm 64° 4% Mostly Cloudy Feels Like 64°...'}])], 'messages': [FunctionMessage(content='[{\"url\": \"https://weather.com/weather/tenday/l/Los+Angeles+CA?canonicalCityId=84c64154109916077c8d3c2352410aaae5f6eeff682000e3a7470e38976128c2\", \"content\": \"recents Specialty Forecasts 10 Day Weather-Los Angeles, CA Air Quality Alert Today Tue 26 | Day Considerable cloudiness with occasional rain showers. High 64F. Winds light and variable. Chance of rain 50%. Considerable cloudiness with occasional rain showers. High 62F. Winds light and variable. Chance of rain 60%. Wed 03 Wed 03 | Day Overcast with showers at times. High 66F. Winds light and variable. Chance of rain 40%.Today 66°/ 50° 6% Sun 24 | Day 66° 6% WSW 4 mph Partly cloudy skies. High 66F. Winds light and variable. Humidity 65% UV Index 3 of 11 Sunrise 6:56 am Sunset 4:49 pm Sun 24 | Night 50° 10% N 1 mph...\"}, {\"url\": \"https://weather.com/weather/tenday/l/Los+Angeles+CA?canonicalCityId=d4a04df2f5cd7d6ef329c49238253e994619763fd5f77a424ca3f1af9957e717\", \"content\": \"recents Specialty Forecasts 10 Day Weather-Los Angeles, CA Air Quality Alert Today Tue 26 | Day Rain showers early with some sunshine later in the day. High 61F. Winds SSE at 5 to 10 mph. Chance of rain 60%. Thu 04 Thu 04 | Day Overcast with rain showers at times. High 63F. Winds S at 5 to 10 mph. Chance of rain 50%. Wed 03 Wed 03 | Day Cloudy with occasional showers. High 63F. Winds SE at 5 to 10 mph. Chance of rain 50%.10 Day Weather - Los Angeles, CA As of 12:06 am PST Tonight --/ 49° 5% Sat 23 | Night 49° 5% NNW 2 mph Partly cloudy this evening, then becoming foggy and damp after midnight. Low 49F. Winds...\"}, {\"url\": \"https://weather.com/weather/hourbyhour/l/Los+Angeles+CA?canonicalCityId=84c64154109916077c8d3c2352410aaae5f6eeff682000e3a7470e38976128c2\", \"content\": \"recents Specialty Forecasts Hourly Weather-Los Angeles, CA Air Quality Alert Tuesday, December 26 10 am Partly Cloudy Cold & Flu Forecast Flu risk is low in your area 3 pm Partly Cloudy 4 pm Mostly Cloudy 5 pm Mostly Cloudy 6 pm Mostly Cloudy 7 pm Cloudy 8 pm Cloudy 9 pm Mostly Cloudy 10 am Cloudy 11 am Mostly Cloudy 12 pm Cloudy 1 pm Mostly Cloudy 2 pm Cloudy 3 pm Cloudy 4 pm Cloudy 5 pm Cloudy 6 pmHourly Weather Forecast for Los Angeles, CA - The Weather Channel | Weather.com - Los Angeles, CA As of 11:12 am PST Saturday, December 23 12 pm 64° 4% Mostly Cloudy Feels Like 64°...\"}]', name='tavily_search_results_json')]}\n", + "{'actions': [...], 'messages': [...]}\n", "------\n", - "{'output': \"The weather in San Francisco is currently foggy early, then partly cloudy later in the day with a high around 60°F. There is a chance of rain showers with a high of 59°F tomorrow. The temperature will drop to a low of 46°F on Thursday night with cloudy conditions and showers.\\n\\nIn Los Angeles, there is considerable cloudiness with occasional rain showers today. The high temperature is expected to be 64°F with light and variable winds. Tomorrow, there will be considerable cloudiness with occasional rain showers and a high of 62°F. The weather will be overcast with showers at times on Wednesday with a high of 66°F.\\n\\nPlease note that weather conditions can change rapidly, so it's always a good idea to check a reliable weather source for the most up-to-date information.\", 'messages': [AIMessage(content=\"The weather in San Francisco is currently foggy early, then partly cloudy later in the day with a high around 60°F. There is a chance of rain showers with a high of 59°F tomorrow. The temperature will drop to a low of 46°F on Thursday night with cloudy conditions and showers.\\n\\nIn Los Angeles, there is considerable cloudiness with occasional rain showers today. The high temperature is expected to be 64°F with light and variable winds. Tomorrow, there will be considerable cloudiness with occasional rain showers and a high of 62°F. The weather will be overcast with showers at times on Wednesday with a high of 66°F.\\n\\nPlease note that weather conditions can change rapidly, so it's always a good idea to check a reliable weather source for the most up-to-date information.\")]}\n", - "------\n" + "{'messages': [...], 'steps': [...]}\n", + "------\n", + "{'messages': [...],\n", + " 'output': 'The items located where the cat is hiding on the shelf are books, '\n", + " 'pencils, and pictures.'}\n" ] } ], "source": [ - "for chunk in agent_executor.stream({\"input\": \"what is the weather in SF and then LA\"}):\n", - " print(chunk)\n", - " print(\"------\")" + "# Note: We use `pprint` to print only to depth 1, it makes it easier to see the output from a high level, before digging in.\n", + "import pprint\n", + "\n", + "chunks = []\n", + "\n", + "async for chunk in agent_executor.astream(\n", + " {\"input\": \"what's items are located where the cat is hiding?\"}\n", + "):\n", + " chunks.append(chunk)\n", + " print(\"------\")\n", + " pprint.pprint(chunk, depth=1)" ] }, { "cell_type": "markdown", - "id": "433c78f0", + "id": "76a930c7-7c6f-4602-b265-d38018f067be", "metadata": {}, "source": [ - "You can see that we get back a bunch of different information. There are two ways to work with this information:\n", + "### Using Messages\n", "\n", - "1. By using the AgentAction/observation/AgentFinish object\n", - "2. By using the `messages` object\n", - "\n", - "You may prefer to use the `messages` object if you are working with a chatbot - because these are chat messages and can be rendered directly. If you don't care about that, the AgentAction/observation/AgentFinish is probably the easier one to inspect." + "You can access the underlying `messages` from the outputs. Using messages can be nice when working with chat applications - because everything is a message!" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "4d5a3112-b2d4-488a-ac76-aa40dcec9cfe", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[OpenAIToolAgentAction(tool='where_cat_is_hiding', tool_input={}, log='\\nInvoking: `where_cat_is_hiding` with `{}`\\n\\n\\n', message_log=[AIMessageChunk(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_pKy4OLcBx6pR6k3GHBOlH68r', 'function': {'arguments': '{}', 'name': 'where_cat_is_hiding'}, 'type': 'function'}]})], tool_call_id='call_pKy4OLcBx6pR6k3GHBOlH68r')]" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "chunks[0][\"actions\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "2f5eead3-f6f0-40b7-82c7-3b485c634e94", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[AIMessageChunk(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_pKy4OLcBx6pR6k3GHBOlH68r', 'function': {'arguments': '{}', 'name': 'where_cat_is_hiding'}, 'type': 'function'}]})]\n", + "[FunctionMessage(content='on the shelf', name='where_cat_is_hiding')]\n", + "[AIMessageChunk(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_qZTz1mRfCCXT18SUy0E07eS4', 'function': {'arguments': '{\\n \"place\": \"shelf\"\\n}', 'name': 'get_items'}, 'type': 'function'}]})]\n", + "[FunctionMessage(content='books, penciles and pictures', name='get_items')]\n", + "[AIMessage(content='The items located where the cat is hiding on the shelf are books, pencils, and pictures.')]\n" + ] + } + ], + "source": [ + "for chunk in chunks:\n", + " print(chunk[\"messages\"])" + ] + }, + { + "cell_type": "markdown", + "id": "1397f859-8595-488e-9857-c4e090a136d3", + "metadata": {}, + "source": [ + "In addition, they contain full logging information (`actions` and `steps`) which may be easier to process for rendering purposes." ] }, { @@ -135,14 +314,16 @@ "id": "edd291a7", "metadata": {}, "source": [ - "### Using AgentAction/observation/AgentFinish\n", + "### Using AgentAction/Observation\n", "\n", - "You can access these raw objects as part of the streamed payload. This gives you more low level information, but can be harder to parse." + "The outputs also contain richer structured information inside of `actions` and `steps`, which could be useful in some situations, but can also be harder to parse.\n", + "\n", + "**Attention** `AgentFinish` is not available as part of the `streaming` method. If this is something you'd like to be added, please start a discussion on github and explain why its needed." ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 10, "id": "603bff1d", "metadata": {}, "outputs": [ @@ -150,109 +331,287 @@ "name": "stdout", "output_type": "stream", "text": [ - "Calling Tool ```tavily_search_results_json``` with input ```{'query': 'weather in San Francisco'}```\n", - "------\n", - "Got result: ```[{'url': 'https://weather.com/weather/tenday/l/San Francisco CA USCA0987:1:US', 'content': 'recents Specialty Forecasts 10 Day Weather-San Francisco, CA Today Mon 18 | Day Fri 22 Fri 22 | Day Foggy early, then partly cloudy later in the day. High around 60F. Winds W at 10 to 15 mph. Considerable cloudiness with occasional rain showers. High 59F. Winds SSE at 5 to 10 mph. Chance of rain 50%. Thu 28 | Night Cloudy with showers. Low 46F. Winds S at 5 to 10 mph. Chance of rain 40%. Fri 29 Fri 29 | Day10 Day Weather-San Francisco, CA As of 5:52 pm PST alertLevel3 Coastal Flood Advisory+1 More Tonight Mostly Cloudy Night --/44° Rain 7% Arrow Up Sun 24| Night 44° Mostly Cloudy Night Rain 7%...'}]```\n", - "------\n", - "Calling Tool ```tavily_search_results_json``` with input ```{'query': 'weather in Los Angeles'}```\n", - "------\n", - "Got result: ```[{'url': 'https://hoodline.com/2023/12/los-angeles-hit-with-no-burn-order-during-cloudy-holiday-forecast-aqmd-urges-compliance-for-public-health/', 'content': 'skies and a chance of rain. According to the National Weather Service, today’s weather in Los Angeles is mostly sunny visiting the AQMD site or using its mobile app. While Los Angeles navigates through a cloudy and cooler weather Weather & Environment in ... Los Angeles Hit with No-Burn Order During Cloudy Holiday Forecast and cooler weather pattern, with temperatures fluctuating around the high 60 degrees and chances of rain by FridayPublished on December 26, 2023. Los Angeles residents face a restricted holiday season as the South Coast Air Quality Management District (AQMD) extends a mandatory no-burn order amid multiple ...'}, {'url': 'https://weather.com/weather/tenday/l/Los+Angeles+CA?canonicalCityId=84c64154109916077c8d3c2352410aaae5f6eeff682000e3a7470e38976128c2', 'content': 'recents Specialty Forecasts 10 Day Weather-Los Angeles, CA Tonight Sat 23 | Night Considerable cloudiness with occasional rain showers. Low 48F. Winds light and variable. Chance of rain 60%. Thu 04 Mon 01 | Day Considerable clouds early. Some decrease in clouds later in the day. High 66F. Winds light and variable. Thu 04 | Night Showers early becoming less numerous late. Low 48F. Winds light and variable. Chance of rain 40%. Fri 05Today 66°/ 50° 6% Sun 24 | Day 66° 6% WSW 4 mph Partly cloudy skies. High 66F. Winds light and variable. Humidity 65% UV Index 3 of 11 Sunrise 6:56 am Sunset 4:49 pm Sun 24 | Night 50° 10% N 1 mph...'}, {'url': 'https://weather.com/weather/tenday/l/Los+Angeles+CA?canonicalCityId=d4a04df2f5cd7d6ef329c49238253e994619763fd5f77a424ca3f1af9957e717', 'content': 'recents Specialty Forecasts 10 Day Weather-Los Angeles, CA Tonight Sat 23 | Night Considerable cloudiness with occasional rain showers. Low 48F. Winds light and variable. Chance of rain 60%. Thu 04 Mon 01 | Day Considerable clouds early. Some decrease in clouds later in the day. High 66F. Winds light and variable. Thu 04 | Night Showers early becoming less numerous late. Low 48F. Winds light and variable. Chance of rain 40%. Fri 0510 Day Weather - Los Angeles, CA As of 12:06 am PST Tonight --/ 49° 5% Sat 23 | Night 49° 5% NNW 2 mph Partly cloudy this evening, then becoming foggy and damp after midnight. Low 49F. Winds...'}]```\n", - "------\n", - "The weather in San Francisco is currently foggy early, then partly cloudy later in the day with a high around 60°F. There is a chance of rain showers with a high of 59°F tomorrow. The temperature will drop to a low of 46°F on Thursday night with cloudy skies and showers.\n", - "\n", - "In Los Angeles, there is considerable cloudiness with occasional rain showers. The temperature will drop to a low of 48°F tonight with light and variable winds. Tomorrow, there will be considerable clouds early with some decrease in clouds later in the day and a high of 66°F. Showers are expected in the evening with a low of 48°F.\n", - "------\n" + "Calling Tool: `where_cat_is_hiding` with input `{}`\n", + "---\n", + "Tool Result: `on the shelf`\n", + "---\n", + "Calling Tool: `get_items` with input `{'place': 'shelf'}`\n", + "---\n", + "Tool Result: `books, penciles and pictures`\n", + "---\n", + "Final Output: The items located where the cat is hiding on the shelf are books, pencils, and pictures.\n", + "---\n" ] } ], "source": [ - "for chunk in agent_executor.stream({\"input\": \"what is the weather in SF and then LA\"}):\n", + "async for chunk in agent_executor.astream(\n", + " {\"input\": \"what's items are located where the cat is hiding?\"}\n", + "):\n", " # Agent Action\n", " if \"actions\" in chunk:\n", " for action in chunk[\"actions\"]:\n", - " print(\n", - " f\"Calling Tool ```{action.tool}``` with input ```{action.tool_input}```\"\n", - " )\n", + " print(f\"Calling Tool: `{action.tool}` with input `{action.tool_input}`\")\n", " # Observation\n", " elif \"steps\" in chunk:\n", " for step in chunk[\"steps\"]:\n", - " print(f\"Got result: ```{step.observation}```\")\n", + " print(f\"Tool Result: `{step.observation}`\")\n", " # Final result\n", " elif \"output\" in chunk:\n", - " print(chunk[\"output\"])\n", + " print(f'Final Output: {chunk[\"output\"]}')\n", " else:\n", - " raise ValueError\n", - " print(\"------\")" + " raise ValueError()\n", + " print(\"---\")" ] }, { + "attachments": {}, "cell_type": "markdown", - "id": "72df7b43", + "id": "5058f098-d8b5-4500-bd99-b972af3ecc09", "metadata": {}, "source": [ - "### Using messages\n", + "## Custom Streaming With Events\n", "\n", - "Using messages can be nice when working with chat applications - because everything is a message!" + "Use the `astream_events` API in case the default behavior of *stream* does not work for your application (e.g., if you need to stream individual tokens from the agent or surface steps occuring **within** tools).\n", + "\n", + "⚠️ This is a **beta** API, meaning that some details might change slightly in the future based on usage.\n", + "⚠️ To make sure all callbacks work properly, use `async` code throughout. Try avoiding mixing in sync versions of code (e.g., sync versions of tools).\n", + "\n", + "Let's use this API to stream the following events:\n", + "\n", + "1. Agent Start with inputs\n", + "1. Tool Start with inputs\n", + "1. Tool End with outputs\n", + "1. Stream the agent final anwer token by token\n", + "1. Agent End with outputs" ] }, { "cell_type": "code", - "execution_count": 7, - "id": "ca79c8d9", + "execution_count": 11, + "id": "46c59cac-25fa-4f42-8cf2-9bcaed6d92c4", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "[AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}})]\n", - "------\n", - "[FunctionMessage(content='[{\"url\": \"https://www.cbsnews.com/sanfrancisco/news/next-round-of-rain-set-to-arrive-in-bay-area-wednesday-morning/\", \"content\": \"weather persists through Thursday morning. The second system is projected to get to the Bay Area early Friday, Watch CBS News Next round of rain set to arrive in Bay Area Wednesday morning December 26, 2023 / 8:17 AM PST to the Bay Area on Wednesday with the arrival of the first of two storm systems. Overnight lows should be mostly in the 40s in the region, with some areas around the bay dropping into the 50s.Watch CBS News Weather Next round of rain set to arrive in Bay Area Wednesday morning December 26, 2023 / 8:17 AM PST / CBS/Bay City News Service While the outlook on Tuesday is cloudy and...\"}, {\"url\": \"https://weather.com/weather/tenday/l/San Francisco CA USCA0987:1:US\", \"content\": \"recents Specialty Forecasts 10 Day Weather-San Francisco, CA Today Mon 18 | Day Fri 22 Fri 22 | Day Foggy early, then partly cloudy later in the day. High around 60F. Winds W at 10 to 15 mph. Considerable cloudiness with occasional rain showers. High 59F. Winds SSE at 5 to 10 mph. Chance of rain 50%. Thu 28 | Night Cloudy with showers. Low 46F. Winds S at 5 to 10 mph. Chance of rain 40%. Fri 29 Fri 29 | Day10 Day Weather-San Francisco, CA As of 5:52 pm PST alertLevel3 Coastal Flood Advisory+1 More Tonight Mostly Cloudy Night --/44° Rain 7% Arrow Up Sun 24| Night 44° Mostly Cloudy Night Rain 7%...\"}]', name='tavily_search_results_json')]\n", - "------\n", - "[AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in Los Angeles\"\\n}', 'name': 'tavily_search_results_json'}})]\n", - "------\n", - "[FunctionMessage(content='[{\"url\": \"https://weather.com/weather/tenday/l/Los+Angeles+CA?canonicalCityId=d4a04df2f5cd7d6ef329c49238253e994619763fd5f77a424ca3f1af9957e717\", \"content\": \"recents Specialty Forecasts 10 Day Weather-Los Angeles, CA Tonight Sat 23 | Night Considerable cloudiness with occasional rain showers. Low 48F. Winds light and variable. Chance of rain 60%. Thu 04 Mon 01 | Day Considerable clouds early. Some decrease in clouds later in the day. High 66F. Winds light and variable. Thu 04 | Night Showers early becoming less numerous late. Low 48F. Winds light and variable. Chance of rain 40%. Fri 0510 Day Weather - Los Angeles, CA As of 12:06 am PST Tonight --/ 49° 5% Sat 23 | Night 49° 5% NNW 2 mph Partly cloudy this evening, then becoming foggy and damp after midnight. Low 49F. Winds...\"}, {\"url\": \"https://weather.com/weather/tenday/l/Los+Angeles+CA?canonicalCityId=84c64154109916077c8d3c2352410aaae5f6eeff682000e3a7470e38976128c2\", \"content\": \"recents Specialty Forecasts 10 Day Weather-Los Angeles, CA Tonight Sat 23 | Night Considerable cloudiness with occasional rain showers. Low 48F. Winds light and variable. Chance of rain 60%. Thu 04 Mon 01 | Day Considerable clouds early. Some decrease in clouds later in the day. High 66F. Winds light and variable. Thu 04 | Night Showers early becoming less numerous late. Low 48F. Winds light and variable. Chance of rain 40%. Fri 05Today 66°/ 50° 6% Sun 24 | Day 66° 6% WSW 4 mph Partly cloudy skies. High 66F. Winds light and variable. Humidity 65% UV Index 3 of 11 Sunrise 6:56 am Sunset 4:49 pm Sun 24 | Night 50° 10% N 1 mph...\"}, {\"url\": \"https://abc7.com/weather/\", \"content\": \"WATCH LIVE AccuWeather in the region are forecast to be high.More in the region are forecast to be high.More NOW IN EFFECT FROM 4 AM THURSDAY TO 10 PM PST SATURDAY...MoreToday\\'s Weather Los Angeles, CA Current Today Tonight MOSTLY CLOUDY 65 ° Feels Like 65° Sunrise 6:55 AM Humidity 65% Sunset 4:48 PM Windspeed ESE 3 mph Moonrise 2:08 PM Pressure 30.0 in...\"}]', name='tavily_search_results_json')]\n", - "------\n", - "[AIMessage(content='The weather in San Francisco is expected to have rain on Wednesday and Thursday. The temperature will range from the 40s to the 50s. You can find more information [here](https://www.cbsnews.com/sanfrancisco/news/next-round-of-rain-set-to-arrive-in-bay-area-wednesday-morning/) and [here](https://weather.com/weather/tenday/l/San Francisco CA USCA0987:1:US).\\n\\nThe weather in Los Angeles is expected to have occasional rain showers with temperatures ranging from the 40s to the 60s. You can find more information [here](https://weather.com/weather/tenday/l/Los+Angeles+CA?canonicalCityId=d4a04df2f5cd7d6ef329c49238253e994619763fd5f77a424ca3f1af9957e717) and [here](https://weather.com/weather/tenday/l/Los+Angeles+CA?canonicalCityId=84c64154109916077c8d3c2352410aaae5f6eeff682000e3a7470e38976128c2).')]\n", - "------\n" + "Starting agent: Agent with input: {'input': 'where is the cat hiding? what items are in that location?'}\n", + "--\n", + "Starting tool: where_cat_is_hiding with inputs: {}\n", + "Done tool: where_cat_is_hiding\n", + "Tool output was: on the shelf\n", + "--\n", + "--\n", + "Starting tool: get_items with inputs: {'place': 'shelf'}\n", + "Done tool: get_items\n", + "Tool output was: books, penciles and pictures\n", + "--\n", + "The| cat| is| currently| hiding| on| the| shelf|.| In| that| location|,| you| can| find| books|,| pencils|,| and| pictures|.|\n", + "--\n", + "Done agent: Agent with output: The cat is currently hiding on the shelf. In that location, you can find books, pencils, and pictures.\n" ] } ], "source": [ - "for chunk in agent_executor.stream({\"input\": \"what is the weather in SF and then LA\"}):\n", - " print(chunk[\"messages\"])\n", - " print(\"------\")" + "async for event in agent_executor.astream_events(\n", + " {\"input\": \"where is the cat hiding? what items are in that location?\"},\n", + " version=\"v1\",\n", + "):\n", + " kind = event[\"event\"]\n", + " if kind == \"on_chain_start\":\n", + " if (\n", + " event[\"name\"] == \"Agent\"\n", + " ): # Was assigned when creating the agent with `.with_config({\"run_name\": \"Agent\"})`\n", + " print(\n", + " f\"Starting agent: {event['name']} with input: {event['data'].get('input')}\"\n", + " )\n", + " elif kind == \"on_chain_end\":\n", + " if (\n", + " event[\"name\"] == \"Agent\"\n", + " ): # Was assigned when creating the agent with `.with_config({\"run_name\": \"Agent\"})`\n", + " print()\n", + " print(\"--\")\n", + " print(\n", + " f\"Done agent: {event['name']} with output: {event['data'].get('output')['output']}\"\n", + " )\n", + " if kind == \"on_chat_model_stream\":\n", + " content = event[\"data\"][\"chunk\"].content\n", + " if content:\n", + " # Empty content in the context of OpenAI means\n", + " # that the model is asking for a tool to be invoked.\n", + " # So we only print non-empty content\n", + " print(content, end=\"|\")\n", + " elif kind == \"on_tool_start\":\n", + " print(\"--\")\n", + " print(\n", + " f\"Starting tool: {event['name']} with inputs: {event['data'].get('input')}\"\n", + " )\n", + " elif kind == \"on_tool_end\":\n", + " print(f\"Done tool: {event['name']}\")\n", + " print(f\"Tool output was: {event['data'].get('output')}\")\n", + " print(\"--\")" ] }, { "cell_type": "markdown", - "id": "0dc01b0f", + "id": "09711ba8-f60e-4a5d-9ace-1bdc613a7c44", "metadata": {}, "source": [ - "## Stream tokens\n", + "### Stream Events from within Tools\n", "\n", - "In addition to streaming the final result, you can also stream tokens. This will require slightly more complicated parsing of the logs\n", + "If your tool leverages LangChain runnable objects (e.g., LCEL chains, LLMs, retrievers etc.) and you want to stream events from those objects as well, you'll need to make sure that callbacks are propagated correctly.\n", "\n", - "You will also need to make sure you set the LLM to be streaming" + "To see how to pass callbacks, let's re-implement the `get_items` tool to make it use an LLM and pass callbacks to that LLM. Feel free to adapt this to your use case." ] }, { "cell_type": "code", - "execution_count": 8, - "id": "3e92d09d", + "execution_count": 12, + "id": "fdd005f4-31d3-450f-b16b-b614c26a72f3", "metadata": {}, "outputs": [], "source": [ - "llm = ChatOpenAI(model=\"gpt-3.5-turbo\", temperature=0, streaming=True)\n", + "@tool\n", + "async def get_items(place: str, callbacks: Callbacks) -> str: # <--- Accept callbacks\n", + " \"\"\"Use this tool to look up which items are in the given place.\"\"\"\n", + " template = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\n", + " \"human\",\n", + " \"Can you tell me what kind of items i might find in the following place: '{place}'. \"\n", + " \"List at least 3 such items separating them by a comma. And include a brief description of each item..\",\n", + " )\n", + " ]\n", + " )\n", + " chain = template | model.with_config(\n", + " {\n", + " \"run_name\": \"Get Items LLM\",\n", + " \"tags\": [\"tool_llm\"],\n", + " \"callbacks\": callbacks, # <-- Propagate callbacks\n", + " }\n", + " )\n", + " chunks = [chunk async for chunk in chain.astream({\"place\": place})]\n", + " return \"\".join(chunk.content for chunk in chunks)" + ] + }, + { + "cell_type": "markdown", + "id": "66828308-538f-4a06-8ed6-bf398d7a3d56", + "metadata": {}, + "source": [ + "^ Take a look at how the tool propagates callbacks. \n", "\n", - "agent = create_openai_functions_agent(llm, tools, prompt)\n", - "agent_executor = AgentExecutor(agent=agent, tools=tools)" + "Next, let's initialize our agent, and take a look at the new output." ] }, { "cell_type": "code", - "execution_count": 9, - "id": "753ff598", + "execution_count": 13, + "id": "095df835-ab27-4791-80e9-07cdba180822", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Starting agent: Agent with input: {'input': 'where is the cat hiding? what items are in that location?'}\n", + "--\n", + "Starting tool: where_cat_is_hiding with inputs: {}\n", + "Done tool: where_cat_is_hiding\n", + "Tool output was: on the shelf\n", + "--\n", + "--\n", + "Starting tool: get_items with inputs: {'place': 'shelf'}\n", + "In| a| shelf|,| you| might| find|:\n", + "\n", + "|1|.| Books|:| A| shelf| is| commonly| used| to| store| books|.| It| may| contain| various| genres| such| as| novels|,| textbooks|,| or| reference| books|.| Books| provide| knowledge|,| entertainment|,| and| can| transport| you| to| different| worlds| through| storytelling|.\n", + "\n", + "|2|.| Decor|ative| items|:| Sh|elves| often| display| decorative| items| like| figur|ines|,| v|ases|,| or| photo| frames|.| These| items| add| a| personal| touch| to| the| space| and| can| reflect| the| owner|'s| interests| or| memories|.\n", + "\n", + "|3|.| Storage| boxes|:| Sh|elves| can| also| hold| storage| boxes| or| baskets|.| These| containers| help| organize| and| decl|utter| the| space| by| storing| miscellaneous| items| like| documents|,| accessories|,| or| small| household| items|.| They| provide| a| neat| and| tidy| appearance| to| the| shelf|.|Done tool: get_items\n", + "Tool output was: In a shelf, you might find:\n", + "\n", + "1. Books: A shelf is commonly used to store books. It may contain various genres such as novels, textbooks, or reference books. Books provide knowledge, entertainment, and can transport you to different worlds through storytelling.\n", + "\n", + "2. Decorative items: Shelves often display decorative items like figurines, vases, or photo frames. These items add a personal touch to the space and can reflect the owner's interests or memories.\n", + "\n", + "3. Storage boxes: Shelves can also hold storage boxes or baskets. These containers help organize and declutter the space by storing miscellaneous items like documents, accessories, or small household items. They provide a neat and tidy appearance to the shelf.\n", + "--\n", + "The| cat| is| hiding| on| the| shelf|.| In| that| location|,| you| might| find| books|,| decorative| items|,| and| storage| boxes|.|\n", + "--\n", + "Done agent: Agent with output: The cat is hiding on the shelf. In that location, you might find books, decorative items, and storage boxes.\n" + ] + } + ], + "source": [ + "# Get the prompt to use - you can modify this!\n", + "prompt = hub.pull(\"hwchase17/openai-tools-agent\")\n", + "# print(prompt.messages) -- to see the prompt\n", + "tools = [get_items, where_cat_is_hiding]\n", + "agent = create_openai_tools_agent(\n", + " model.with_config({\"tags\": [\"agent_llm\"]}), tools, prompt\n", + ")\n", + "agent_executor = AgentExecutor(agent=agent, tools=tools).with_config(\n", + " {\"run_name\": \"Agent\"}\n", + ")\n", + "\n", + "async for event in agent_executor.astream_events(\n", + " {\"input\": \"where is the cat hiding? what items are in that location?\"},\n", + " version=\"v1\",\n", + "):\n", + " kind = event[\"event\"]\n", + " if kind == \"on_chain_start\":\n", + " if (\n", + " event[\"name\"] == \"Agent\"\n", + " ): # Was assigned when creating the agent with `.with_config({\"run_name\": \"Agent\"})`\n", + " print(\n", + " f\"Starting agent: {event['name']} with input: {event['data'].get('input')}\"\n", + " )\n", + " elif kind == \"on_chain_end\":\n", + " if (\n", + " event[\"name\"] == \"Agent\"\n", + " ): # Was assigned when creating the agent with `.with_config({\"run_name\": \"Agent\"})`\n", + " print()\n", + " print(\"--\")\n", + " print(\n", + " f\"Done agent: {event['name']} with output: {event['data'].get('output')['output']}\"\n", + " )\n", + " if kind == \"on_chat_model_stream\":\n", + " content = event[\"data\"][\"chunk\"].content\n", + " if content:\n", + " # Empty content in the context of OpenAI means\n", + " # that the model is asking for a tool to be invoked.\n", + " # So we only print non-empty content\n", + " print(content, end=\"|\")\n", + " elif kind == \"on_tool_start\":\n", + " print(\"--\")\n", + " print(\n", + " f\"Starting tool: {event['name']} with inputs: {event['data'].get('input')}\"\n", + " )\n", + " elif kind == \"on_tool_end\":\n", + " print(f\"Done tool: {event['name']}\")\n", + " print(f\"Tool output was: {event['data'].get('output')}\")\n", + " print(\"--\")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "24386754-5cd6-4322-82f7-affb93322bad", + "metadata": {}, + "source": [ + "### Other aproaches\n", + "\n", + "#### Using astream_log\n", + "\n", + "**Note** You can also use the [astream_log](https://python.langchain.com/docs/expression_language/interface#async-stream-intermediate-steps) API. This API produces a granular log of all events that occur during execution. The log format is based on the [JSONPatch](https://jsonpatch.com/) standard. It's granular, but requires effort to parse. For this reason, we created the `astream_events` API instead." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "01ad657f-7759-4fb3-a7ca-e2d7e7f8b28f", "metadata": {}, "outputs": [ { @@ -262,522 +621,100 @@ "RunLogPatch({'op': 'replace',\n", " 'path': '',\n", " 'value': {'final_output': None,\n", - " 'id': '32650ba8-8a53-4b76-8846-dbb6c3a65727',\n", + " 'id': 'c261bc30-60d1-4420-9c66-c6c0797f2c2d',\n", " 'logs': {},\n", - " 'streamed_output': []}})\n", + " 'name': 'Agent',\n", + " 'streamed_output': [],\n", + " 'type': 'chain'}})\n", "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI',\n", + " 'path': '/logs/RunnableSequence',\n", " 'value': {'end_time': None,\n", " 'final_output': None,\n", - " 'id': 'ce3a507d-210d-40aa-8576-dd0aa97e6498',\n", + " 'id': '183cb6f8-ed29-4967-b1ea-024050ce66c7',\n", " 'metadata': {},\n", - " 'name': 'ChatOpenAI',\n", - " 'start_time': '2023-12-26T17:55:56.653',\n", + " 'name': 'RunnableSequence',\n", + " 'start_time': '2024-01-22T20:38:43.650+00:00',\n", " 'streamed_output': [],\n", " 'streamed_output_str': [],\n", - " 'tags': ['seq:step:3'],\n", - " 'type': 'llm'}})\n", - "RunLogPatch({'op': 'add', 'path': '/logs/ChatOpenAI/streamed_output_str/-', 'value': ''},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI/streamed_output/-',\n", - " 'value': AIMessageChunk(content='', additional_kwargs={'function_call': {'arguments': '', 'name': 'tavily_search_results_json'}})})\n", - "RunLogPatch({'op': 'add', 'path': '/logs/ChatOpenAI/streamed_output_str/-', 'value': ''},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI/streamed_output/-',\n", - " 'value': AIMessageChunk(content='', additional_kwargs={'function_call': {'arguments': '{\\n', 'name': ''}})})\n", - "RunLogPatch({'op': 'add', 'path': '/logs/ChatOpenAI/streamed_output_str/-', 'value': ''},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI/streamed_output/-',\n", - " 'value': AIMessageChunk(content='', additional_kwargs={'function_call': {'arguments': ' ', 'name': ''}})})\n", - "RunLogPatch({'op': 'add', 'path': '/logs/ChatOpenAI/streamed_output_str/-', 'value': ''},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI/streamed_output/-',\n", - " 'value': AIMessageChunk(content='', additional_kwargs={'function_call': {'arguments': ' \"', 'name': ''}})})\n", - "RunLogPatch({'op': 'add', 'path': '/logs/ChatOpenAI/streamed_output_str/-', 'value': ''},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI/streamed_output/-',\n", - " 'value': AIMessageChunk(content='', additional_kwargs={'function_call': {'arguments': 'query', 'name': ''}})})\n", - "RunLogPatch({'op': 'add', 'path': '/logs/ChatOpenAI/streamed_output_str/-', 'value': ''},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI/streamed_output/-',\n", - " 'value': AIMessageChunk(content='', additional_kwargs={'function_call': {'arguments': '\":', 'name': ''}})})\n", - "RunLogPatch({'op': 'add', 'path': '/logs/ChatOpenAI/streamed_output_str/-', 'value': ''},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI/streamed_output/-',\n", - " 'value': AIMessageChunk(content='', additional_kwargs={'function_call': {'arguments': ' \"', 'name': ''}})})\n", - "RunLogPatch({'op': 'add', 'path': '/logs/ChatOpenAI/streamed_output_str/-', 'value': ''},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI/streamed_output/-',\n", - " 'value': AIMessageChunk(content='', additional_kwargs={'function_call': {'arguments': 'weather', 'name': ''}})})\n", - "RunLogPatch({'op': 'add', 'path': '/logs/ChatOpenAI/streamed_output_str/-', 'value': ''},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI/streamed_output/-',\n", - " 'value': AIMessageChunk(content='', additional_kwargs={'function_call': {'arguments': ' in', 'name': ''}})})\n", - "RunLogPatch({'op': 'add', 'path': '/logs/ChatOpenAI/streamed_output_str/-', 'value': ''},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI/streamed_output/-',\n", - " 'value': AIMessageChunk(content='', additional_kwargs={'function_call': {'arguments': ' San', 'name': ''}})})\n", - "RunLogPatch({'op': 'add', 'path': '/logs/ChatOpenAI/streamed_output_str/-', 'value': ''},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI/streamed_output/-',\n", - " 'value': AIMessageChunk(content='', additional_kwargs={'function_call': {'arguments': ' Francisco', 'name': ''}})})\n", - "RunLogPatch({'op': 'add', 'path': '/logs/ChatOpenAI/streamed_output_str/-', 'value': ''},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI/streamed_output/-',\n", - " 'value': AIMessageChunk(content='', additional_kwargs={'function_call': {'arguments': '\"\\n', 'name': ''}})})\n", - "RunLogPatch({'op': 'add', 'path': '/logs/ChatOpenAI/streamed_output_str/-', 'value': ''},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI/streamed_output/-',\n", - " 'value': AIMessageChunk(content='', additional_kwargs={'function_call': {'arguments': '}', 'name': ''}})})\n", - "RunLogPatch({'op': 'add', 'path': '/logs/ChatOpenAI/streamed_output_str/-', 'value': ''},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI/streamed_output/-',\n", - " 'value': AIMessageChunk(content='')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI/final_output',\n", - " 'value': {'generations': [[{'generation_info': {'finish_reason': 'function_call'},\n", - " 'message': AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}),\n", - " 'text': '',\n", - " 'type': 'ChatGeneration'}]],\n", - " 'llm_output': None,\n", - " 'run': None}},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI/end_time',\n", - " 'value': '2023-12-26T17:55:57.337'})\n", + " 'tags': [],\n", + " 'type': 'chain'}})\n", "RunLogPatch({'op': 'add',\n", - " 'path': '/streamed_output/-',\n", - " 'value': {'actions': [AgentActionMessageLog(tool='tavily_search_results_json', tool_input={'query': 'weather in San Francisco'}, log=\"\\nInvoking: `tavily_search_results_json` with `{'query': 'weather in San Francisco'}`\\n\\n\\n\", message_log=[AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}})])],\n", - " 'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}})]}},\n", - " {'op': 'replace',\n", - " 'path': '/final_output',\n", - " 'value': {'actions': [AgentActionMessageLog(tool='tavily_search_results_json', tool_input={'query': 'weather in San Francisco'}, log=\"\\nInvoking: `tavily_search_results_json` with `{'query': 'weather in San Francisco'}`\\n\\n\\n\", message_log=[AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}})])],\n", - " 'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}})]}})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/streamed_output/-',\n", - " 'value': {'messages': [FunctionMessage(content='[{\"url\": \"https://www.cbsnews.com/sanfrancisco/news/next-round-of-rain-set-to-arrive-in-bay-area-wednesday-morning/\", \"content\": \"weather persists through Thursday morning. The second system is projected to get to the Bay Area early Friday, Watch CBS News Next round of rain set to arrive in Bay Area Wednesday morning December 26, 2023 / 8:17 AM PST to the Bay Area on Wednesday with the arrival of the first of two storm systems. Overnight lows should be mostly in the 40s in the region, with some areas around the bay dropping into the 50s.Watch CBS News Weather Next round of rain set to arrive in Bay Area Wednesday morning December 26, 2023 / 8:17 AM PST / CBS/Bay City News Service While the outlook on Tuesday is cloudy and...\"}, {\"url\": \"https://weather.com/weather/tenday/l/San Francisco CA USCA0987:1:US\", \"content\": \"recents Specialty Forecasts 10 Day Weather-San Francisco, CA Today Mon 18 | Day Fri 22 Fri 22 | Day Foggy early, then partly cloudy later in the day. High around 60F. Winds W at 10 to 15 mph. Considerable cloudiness with occasional rain showers. High 59F. Winds SSE at 5 to 10 mph. Chance of rain 50%. Thu 28 | Night Cloudy with showers. Low 46F. Winds S at 5 to 10 mph. Chance of rain 40%. Fri 29 Fri 29 | Day10 Day Weather-San Francisco, CA As of 5:52 pm PST alertLevel3 Coastal Flood Advisory+1 More Tonight Mostly Cloudy Night --/44° Rain 7% Arrow Up Sun 24| Night 44° Mostly Cloudy Night Rain 7%...\"}]', name='tavily_search_results_json')],\n", - " 'steps': [AgentStep(action=AgentActionMessageLog(tool='tavily_search_results_json', tool_input={'query': 'weather in San Francisco'}, log=\"\\nInvoking: `tavily_search_results_json` with `{'query': 'weather in San Francisco'}`\\n\\n\\n\", message_log=[AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}})]), observation=[{'url': 'https://www.cbsnews.com/sanfrancisco/news/next-round-of-rain-set-to-arrive-in-bay-area-wednesday-morning/', 'content': 'weather persists through Thursday morning. The second system is projected to get to the Bay Area early Friday, Watch CBS News Next round of rain set to arrive in Bay Area Wednesday morning December 26, 2023 / 8:17 AM PST to the Bay Area on Wednesday with the arrival of the first of two storm systems. Overnight lows should be mostly in the 40s in the region, with some areas around the bay dropping into the 50s.Watch CBS News Weather Next round of rain set to arrive in Bay Area Wednesday morning December 26, 2023 / 8:17 AM PST / CBS/Bay City News Service While the outlook on Tuesday is cloudy and...'}, {'url': 'https://weather.com/weather/tenday/l/San Francisco CA USCA0987:1:US', 'content': 'recents Specialty Forecasts 10 Day Weather-San Francisco, CA Today Mon 18 | Day Fri 22 Fri 22 | Day Foggy early, then partly cloudy later in the day. High around 60F. Winds W at 10 to 15 mph. Considerable cloudiness with occasional rain showers. High 59F. Winds SSE at 5 to 10 mph. Chance of rain 50%. Thu 28 | Night Cloudy with showers. Low 46F. Winds S at 5 to 10 mph. Chance of rain 40%. Fri 29 Fri 29 | Day10 Day Weather-San Francisco, CA As of 5:52 pm PST alertLevel3 Coastal Flood Advisory+1 More Tonight Mostly Cloudy Night --/44° Rain 7% Arrow Up Sun 24| Night 44° Mostly Cloudy Night Rain 7%...'}])]}},\n", - " {'op': 'add',\n", - " 'path': '/final_output/steps',\n", - " 'value': [AgentStep(action=AgentActionMessageLog(tool='tavily_search_results_json', tool_input={'query': 'weather in San Francisco'}, log=\"\\nInvoking: `tavily_search_results_json` with `{'query': 'weather in San Francisco'}`\\n\\n\\n\", message_log=[AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}})]), observation=[{'url': 'https://www.cbsnews.com/sanfrancisco/news/next-round-of-rain-set-to-arrive-in-bay-area-wednesday-morning/', 'content': 'weather persists through Thursday morning. The second system is projected to get to the Bay Area early Friday, Watch CBS News Next round of rain set to arrive in Bay Area Wednesday morning December 26, 2023 / 8:17 AM PST to the Bay Area on Wednesday with the arrival of the first of two storm systems. Overnight lows should be mostly in the 40s in the region, with some areas around the bay dropping into the 50s.Watch CBS News Weather Next round of rain set to arrive in Bay Area Wednesday morning December 26, 2023 / 8:17 AM PST / CBS/Bay City News Service While the outlook on Tuesday is cloudy and...'}, {'url': 'https://weather.com/weather/tenday/l/San Francisco CA USCA0987:1:US', 'content': 'recents Specialty Forecasts 10 Day Weather-San Francisco, CA Today Mon 18 | Day Fri 22 Fri 22 | Day Foggy early, then partly cloudy later in the day. High around 60F. Winds W at 10 to 15 mph. Considerable cloudiness with occasional rain showers. High 59F. Winds SSE at 5 to 10 mph. Chance of rain 50%. Thu 28 | Night Cloudy with showers. Low 46F. Winds S at 5 to 10 mph. Chance of rain 40%. Fri 29 Fri 29 | Day10 Day Weather-San Francisco, CA As of 5:52 pm PST alertLevel3 Coastal Flood Advisory+1 More Tonight Mostly Cloudy Night --/44° Rain 7% Arrow Up Sun 24| Night 44° Mostly Cloudy Night Rain 7%...'}])]},\n", - " {'op': 'add',\n", - " 'path': '/final_output/messages/1',\n", - " 'value': FunctionMessage(content='[{\"url\": \"https://www.cbsnews.com/sanfrancisco/news/next-round-of-rain-set-to-arrive-in-bay-area-wednesday-morning/\", \"content\": \"weather persists through Thursday morning. The second system is projected to get to the Bay Area early Friday, Watch CBS News Next round of rain set to arrive in Bay Area Wednesday morning December 26, 2023 / 8:17 AM PST to the Bay Area on Wednesday with the arrival of the first of two storm systems. Overnight lows should be mostly in the 40s in the region, with some areas around the bay dropping into the 50s.Watch CBS News Weather Next round of rain set to arrive in Bay Area Wednesday morning December 26, 2023 / 8:17 AM PST / CBS/Bay City News Service While the outlook on Tuesday is cloudy and...\"}, {\"url\": \"https://weather.com/weather/tenday/l/San Francisco CA USCA0987:1:US\", \"content\": \"recents Specialty Forecasts 10 Day Weather-San Francisco, CA Today Mon 18 | Day Fri 22 Fri 22 | Day Foggy early, then partly cloudy later in the day. High around 60F. Winds W at 10 to 15 mph. Considerable cloudiness with occasional rain showers. High 59F. Winds SSE at 5 to 10 mph. Chance of rain 50%. Thu 28 | Night Cloudy with showers. Low 46F. Winds S at 5 to 10 mph. Chance of rain 40%. Fri 29 Fri 29 | Day10 Day Weather-San Francisco, CA As of 5:52 pm PST alertLevel3 Coastal Flood Advisory+1 More Tonight Mostly Cloudy Night --/44° Rain 7% Arrow Up Sun 24| Night 44° Mostly Cloudy Night Rain 7%...\"}]', name='tavily_search_results_json')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2',\n", + " 'path': '/logs/RunnableAssign',\n", " 'value': {'end_time': None,\n", " 'final_output': None,\n", - " 'id': '503b959e-b6c2-427b-b2e8-7d40497a2458',\n", + " 'id': '7fe1bb27-3daf-492e-bc7e-28602398f008',\n", " 'metadata': {},\n", - " 'name': 'ChatOpenAI',\n", - " 'start_time': '2023-12-26T17:56:00.983',\n", + " 'name': 'RunnableAssign',\n", + " 'start_time': '2024-01-22T20:38:43.652+00:00',\n", " 'streamed_output': [],\n", " 'streamed_output_str': [],\n", - " 'tags': ['seq:step:3'],\n", - " 'type': 'llm'}})\n", - "RunLogPatch({'op': 'add', 'path': '/logs/ChatOpenAI:2/streamed_output_str/-', 'value': ''},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content='')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': 'The'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content='The')})\n", + " 'tags': ['seq:step:1'],\n", + " 'type': 'chain'}})\n", "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': ' weather'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content=' weather')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': ' in'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content=' in')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': ' San'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content=' San')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': ' Francisco'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content=' Francisco')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': ' is'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content=' is')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': ' currently'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content=' currently')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': ' cloudy'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content=' cloudy')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': ' with'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content=' with')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': ' occasional'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content=' occasional')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': ' rain'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content=' rain')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': ' showers'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content=' showers')})\n", + " 'path': '/logs/RunnableAssign/streamed_output/-',\n", + " 'value': {'input': 'where is the cat hiding? what items are in that '\n", + " 'location?',\n", + " 'intermediate_steps': []}})\n", "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': '.'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content='.')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': ' The'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content=' The')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': ' temperature'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content=' temperature')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': ' is'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content=' is')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': ' around'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content=' around')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': ' '},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content=' ')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': '59'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content='59')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': '°F'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content='°F')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': ' ('},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content=' (')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': '15'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content='15')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': '°C'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content='°C')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': ')'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content=')')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': ' with'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content=' with')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': ' winds'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content=' winds')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': ' from'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content=' from')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': ' the'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content=' the')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': ' southeast'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content=' southeast')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': ' at'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content=' at')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': ' '},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content=' ')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': '5'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content='5')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': ' to'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content=' to')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': ' '},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content=' ')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': '10'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content='10')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': ' mph'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content=' mph')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': '.'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content='.')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': ' The'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content=' The')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': ' overnight'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content=' overnight')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': ' low'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content=' low')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': ' is'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content=' is')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': ' expected'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content=' expected')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': ' to'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content=' to')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': ' be'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content=' be')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': ' around'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content=' around')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': ' '},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content=' ')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': '46'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content='46')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': '°F'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content='°F')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': ' ('},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content=' (')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': '8'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content='8')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': '°C'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content='°C')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': ')'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content=')')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': ' with'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content=' with')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': ' a'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content=' a')})\n", - "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': ' chance'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content=' chance')})\n", + " 'path': '/logs/RunnableParallel',\n", + " 'value': {'end_time': None,\n", + " 'final_output': None,\n", + " 'id': 'b034e867-e6bb-4296-bfe6-752c44fba6ce',\n", + " 'metadata': {},\n", + " 'name': 'RunnableParallel',\n", + " 'start_time': '2024-01-22T20:38:43.652+00:00',\n", + " 'streamed_output': [],\n", + " 'streamed_output_str': [],\n", + " 'tags': [],\n", + " 'type': 'chain'}})\n", "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': ' of'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content=' of')})\n", + " 'path': '/logs/RunnableLambda',\n", + " 'value': {'end_time': None,\n", + " 'final_output': None,\n", + " 'id': '65ceef3e-7a80-4015-8b5b-d949326872e9',\n", + " 'metadata': {},\n", + " 'name': 'RunnableLambda',\n", + " 'start_time': '2024-01-22T20:38:43.653+00:00',\n", + " 'streamed_output': [],\n", + " 'streamed_output_str': [],\n", + " 'tags': ['map:key:agent_scratchpad'],\n", + " 'type': 'chain'}})\n", + "RunLogPatch({'op': 'add', 'path': '/logs/RunnableLambda/streamed_output/-', 'value': []})\n", "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': ' showers'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content=' showers')})\n", + " 'path': '/logs/RunnableParallel/streamed_output/-',\n", + " 'value': {'agent_scratchpad': []}})\n", "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n", - " 'value': '.'},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content='.')})\n", - "RunLogPatch({'op': 'add', 'path': '/logs/ChatOpenAI:2/streamed_output_str/-', 'value': ''},\n", - " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/streamed_output/-',\n", - " 'value': AIMessageChunk(content='')})\n", + " 'path': '/logs/RunnableAssign/streamed_output/-',\n", + " 'value': {'agent_scratchpad': []}})\n", "RunLogPatch({'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/final_output',\n", - " 'value': {'generations': [[{'generation_info': {'finish_reason': 'stop'},\n", - " 'message': AIMessage(content='The weather in San Francisco is currently cloudy with occasional rain showers. The temperature is around 59°F (15°C) with winds from the southeast at 5 to 10 mph. The overnight low is expected to be around 46°F (8°C) with a chance of showers.'),\n", - " 'text': 'The weather in San Francisco is '\n", - " 'currently cloudy with occasional rain '\n", - " 'showers. The temperature is around 59°F '\n", - " '(15°C) with winds from the southeast at '\n", - " '5 to 10 mph. The overnight low is '\n", - " 'expected to be around 46°F (8°C) with a '\n", - " 'chance of showers.',\n", - " 'type': 'ChatGeneration'}]],\n", - " 'llm_output': None,\n", - " 'run': None}},\n", + " 'path': '/logs/RunnableLambda/final_output',\n", + " 'value': {'output': []}},\n", " {'op': 'add',\n", - " 'path': '/logs/ChatOpenAI:2/end_time',\n", - " 'value': '2023-12-26T17:56:02.356'})\n", + " 'path': '/logs/RunnableLambda/end_time',\n", + " 'value': '2024-01-22T20:38:43.654+00:00'})\n", "RunLogPatch({'op': 'add',\n", - " 'path': '/streamed_output/-',\n", - " 'value': {'messages': [AIMessage(content='The weather in San Francisco is currently cloudy with occasional rain showers. The temperature is around 59°F (15°C) with winds from the southeast at 5 to 10 mph. The overnight low is expected to be around 46°F (8°C) with a chance of showers.')],\n", - " 'output': 'The weather in San Francisco is currently cloudy with '\n", - " 'occasional rain showers. The temperature is around 59°F '\n", - " '(15°C) with winds from the southeast at 5 to 10 mph. '\n", - " 'The overnight low is expected to be around 46°F (8°C) '\n", - " 'with a chance of showers.'}},\n", + " 'path': '/logs/RunnableParallel/final_output',\n", + " 'value': {'agent_scratchpad': []}},\n", " {'op': 'add',\n", - " 'path': '/final_output/output',\n", - " 'value': 'The weather in San Francisco is currently cloudy with occasional '\n", - " 'rain showers. The temperature is around 59°F (15°C) with winds '\n", - " 'from the southeast at 5 to 10 mph. The overnight low is expected '\n", - " 'to be around 46°F (8°C) with a chance of showers.'},\n", - " {'op': 'add',\n", - " 'path': '/final_output/messages/2',\n", - " 'value': AIMessage(content='The weather in San Francisco is currently cloudy with occasional rain showers. The temperature is around 59°F (15°C) with winds from the southeast at 5 to 10 mph. The overnight low is expected to be around 46°F (8°C) with a chance of showers.')})\n" + " 'path': '/logs/RunnableParallel/end_time',\n", + " 'value': '2024-01-22T20:38:43.655+00:00'})\n" ] } ], "source": [ + "i = 0\n", "async for chunk in agent_executor.astream_log(\n", - " {\"input\": \"what is the weather in sf\", \"chat_history\": []},\n", - " include_names=[\"ChatOpenAI\"],\n", + " {\"input\": \"where is the cat hiding? what items are in that location?\"},\n", "):\n", - " print(chunk)" + " print(chunk)\n", + " i += 1\n", + " if i > 10:\n", + " break" ] }, { "cell_type": "markdown", - "id": "51a51076", + "id": "5763c64b-7fff-4167-9eb3-172209cef958", "metadata": {}, "source": [ "This may require some logic to get in a workable format" @@ -785,8 +722,8 @@ }, { "cell_type": "code", - "execution_count": 10, - "id": "7cdae318", + "execution_count": 15, + "id": "f7120cbd-6bea-4706-821a-ff3b6722bf1d", "metadata": {}, "outputs": [ { @@ -796,272 +733,104 @@ "\n", "None\n", "----\n", - "/logs/ChatOpenAI\n", - "{'id': '3f6d3587-600f-419b-8225-8908a347b7d2', 'name': 'ChatOpenAI', 'type': 'llm', 'tags': ['seq:step:3'], 'metadata': {}, 'start_time': '2023-12-26T17:56:19.884', 'streamed_output': [], 'streamed_output_str': [], 'final_output': None, 'end_time': None}\n", + "/logs/RunnableSequence\n", + "{'id': '22bbd5db-9578-4e3f-a6ec-9b61f08cb8a9', 'name': 'RunnableSequence', 'type': 'chain', 'tags': [], 'metadata': {}, 'start_time': '2024-01-22T20:38:43.668+00:00', 'streamed_output': [], 'streamed_output_str': [], 'final_output': None, 'end_time': None}\n", "----\n", - "/logs/ChatOpenAI/streamed_output/-\n", - "content='' additional_kwargs={'function_call': {'arguments': '', 'name': 'tavily_search_results_json'}}\n", + "/logs/RunnableAssign\n", + "{'id': 'e0c00ae2-aaa2-4a09-bc93-cb34bf3f6554', 'name': 'RunnableAssign', 'type': 'chain', 'tags': ['seq:step:1'], 'metadata': {}, 'start_time': '2024-01-22T20:38:43.672+00:00', 'streamed_output': [], 'streamed_output_str': [], 'final_output': None, 'end_time': None}\n", "----\n", - "/logs/ChatOpenAI/streamed_output/-\n", - "content='' additional_kwargs={'function_call': {'arguments': '{\\n', 'name': 'tavily_search_results_json'}}\n", + "/logs/RunnableAssign/streamed_output/-\n", + "{'input': 'where is the cat hiding? what items are in that location?', 'intermediate_steps': []}\n", "----\n", - "/logs/ChatOpenAI/streamed_output/-\n", - "content='' additional_kwargs={'function_call': {'arguments': '{\\n ', 'name': 'tavily_search_results_json'}}\n", + "/logs/RunnableParallel\n", + "{'id': '26ff576d-ff9d-4dea-98b2-943312a37f4d', 'name': 'RunnableParallel', 'type': 'chain', 'tags': [], 'metadata': {}, 'start_time': '2024-01-22T20:38:43.674+00:00', 'streamed_output': [], 'streamed_output_str': [], 'final_output': None, 'end_time': None}\n", "----\n", - "/logs/ChatOpenAI/streamed_output/-\n", - "content='' additional_kwargs={'function_call': {'arguments': '{\\n \"', 'name': 'tavily_search_results_json'}}\n", + "/logs/RunnableLambda\n", + "{'id': '9f343c6a-23f7-4a28-832f-d4fe3e95d1dc', 'name': 'RunnableLambda', 'type': 'chain', 'tags': ['map:key:agent_scratchpad'], 'metadata': {}, 'start_time': '2024-01-22T20:38:43.685+00:00', 'streamed_output': [], 'streamed_output_str': [], 'final_output': None, 'end_time': None}\n", "----\n", - "/logs/ChatOpenAI/streamed_output/-\n", - "content='' additional_kwargs={'function_call': {'arguments': '{\\n \"query', 'name': 'tavily_search_results_json'}}\n", + "/logs/RunnableLambda/streamed_output/-\n", + "[]\n", "----\n", - "/logs/ChatOpenAI/streamed_output/-\n", - "content='' additional_kwargs={'function_call': {'arguments': '{\\n \"query\":', 'name': 'tavily_search_results_json'}}\n", + "/logs/RunnableParallel/streamed_output/-\n", + "{'agent_scratchpad': []}\n", "----\n", - "/logs/ChatOpenAI/streamed_output/-\n", - "content='' additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"', 'name': 'tavily_search_results_json'}}\n", + "/logs/RunnableAssign/streamed_output/-\n", + "{'input': 'where is the cat hiding? what items are in that location?', 'intermediate_steps': [], 'agent_scratchpad': []}\n", "----\n", - "/logs/ChatOpenAI/streamed_output/-\n", - "content='' additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather', 'name': 'tavily_search_results_json'}}\n", + "/logs/RunnableLambda/end_time\n", + "2024-01-22T20:38:43.687+00:00\n", "----\n", - "/logs/ChatOpenAI/streamed_output/-\n", - "content='' additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in', 'name': 'tavily_search_results_json'}}\n", + "/logs/RunnableParallel/end_time\n", + "2024-01-22T20:38:43.688+00:00\n", "----\n", - "/logs/ChatOpenAI/streamed_output/-\n", - "content='' additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San', 'name': 'tavily_search_results_json'}}\n", + "/logs/RunnableAssign/end_time\n", + "2024-01-22T20:38:43.688+00:00\n", "----\n", - "/logs/ChatOpenAI/streamed_output/-\n", - "content='' additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco', 'name': 'tavily_search_results_json'}}\n", + "/logs/ChatPromptTemplate\n", + "{'id': '7e3a84d5-46b8-4782-8eed-d1fe92be6a30', 'name': 'ChatPromptTemplate', 'type': 'prompt', 'tags': ['seq:step:2'], 'metadata': {}, 'start_time': '2024-01-22T20:38:43.689+00:00', 'streamed_output': [], 'streamed_output_str': [], 'final_output': None, 'end_time': None}\n", + "----\n", + "/logs/ChatPromptTemplate/end_time\n", + "2024-01-22T20:38:43.689+00:00\n", + "----\n", + "/logs/ChatOpenAI\n", + "{'id': '6446f7ec-b3e4-4637-89d8-b4b34b46ea14', 'name': 'ChatOpenAI', 'type': 'llm', 'tags': ['seq:step:3', 'agent_llm'], 'metadata': {}, 'start_time': '2024-01-22T20:38:43.690+00:00', 'streamed_output': [], 'streamed_output_str': [], 'final_output': None, 'end_time': None}\n", "----\n", "/logs/ChatOpenAI/streamed_output/-\n", - "content='' additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n', 'name': 'tavily_search_results_json'}}\n", + "content='' additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_gKFg6FX8ZQ88wFUs94yx86PF', 'function': {'arguments': '', 'name': 'where_cat_is_hiding'}, 'type': 'function'}]}\n", "----\n", "/logs/ChatOpenAI/streamed_output/-\n", - "content='' additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}\n", + "content='' additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_gKFg6FX8ZQ88wFUs94yx86PF', 'function': {'arguments': '{}', 'name': 'where_cat_is_hiding'}, 'type': 'function'}]}\n", "----\n", "/logs/ChatOpenAI/streamed_output/-\n", - "content='' additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}\n", + "content='' additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_gKFg6FX8ZQ88wFUs94yx86PF', 'function': {'arguments': '{}', 'name': 'where_cat_is_hiding'}, 'type': 'function'}]}\n", "----\n", "/logs/ChatOpenAI/end_time\n", - "2023-12-26T17:56:20.849\n", + "2024-01-22T20:38:44.203+00:00\n", "----\n", - "/final_output\n", - "None\n", + "/logs/OpenAIToolsAgentOutputParser\n", + "{'id': '65912835-8dcd-4be2-ad05-9f239a7ef704', 'name': 'OpenAIToolsAgentOutputParser', 'type': 'parser', 'tags': ['seq:step:4'], 'metadata': {}, 'start_time': '2024-01-22T20:38:44.204+00:00', 'streamed_output': [], 'streamed_output_str': [], 'final_output': None, 'end_time': None}\n", "----\n", - "/final_output/messages/1\n", - "content='[{\"url\": \"https://weather.com/weather/tenday/l/San Francisco CA USCA0987:1:US\", \"content\": \"recents Specialty Forecasts 10 Day Weather-San Francisco, CA Today Mon 18 | Day Fri 22 Fri 22 | Day Foggy early, then partly cloudy later in the day. High around 60F. Winds W at 10 to 15 mph. Considerable cloudiness with occasional rain showers. High 59F. Winds SSE at 5 to 10 mph. Chance of rain 50%. Thu 28 | Night Cloudy with showers. Low 46F. Winds S at 5 to 10 mph. Chance of rain 40%. Fri 29 Fri 29 | Day10 Day Weather-San Francisco, CA As of 5:52 pm PST alertLevel3 Coastal Flood Advisory+1 More Tonight Mostly Cloudy Night --/44° Rain 7% Arrow Up Sun 24| Night 44° Mostly Cloudy Night Rain 7%...\"}]' name='tavily_search_results_json'\n", + "/logs/OpenAIToolsAgentOutputParser/end_time\n", + "2024-01-22T20:38:44.205+00:00\n", "----\n", - "/logs/ChatOpenAI:2\n", - "{'id': 'fc7ab413-6f59-4a9e-bae1-3140abdaff55', 'name': 'ChatOpenAI', 'type': 'llm', 'tags': ['seq:step:3'], 'metadata': {}, 'start_time': '2023-12-26T17:56:24.546', 'streamed_output': [], 'streamed_output_str': [], 'final_output': None, 'end_time': None}\n", + "/logs/RunnableSequence/streamed_output/-\n", + "[OpenAIToolAgentAction(tool='where_cat_is_hiding', tool_input={}, log='\\nInvoking: `where_cat_is_hiding` with `{}`\\n\\n\\n', message_log=[AIMessageChunk(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_gKFg6FX8ZQ88wFUs94yx86PF', 'function': {'arguments': '{}', 'name': 'where_cat_is_hiding'}, 'type': 'function'}]})], tool_call_id='call_gKFg6FX8ZQ88wFUs94yx86PF')]\n", "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content=''\n", + "/logs/RunnableSequence/end_time\n", + "2024-01-22T20:38:44.206+00:00\n", "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently fog'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around '\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around 60'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around 60°F'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around 60°F.'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around 60°F. Later'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around 60°F. Later in'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around 60°F. Later in the'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around 60°F. Later in the day'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around 60°F. Later in the day,'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around 60°F. Later in the day, it'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around 60°F. Later in the day, it will'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around 60°F. Later in the day, it will become'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around 60°F. Later in the day, it will become partly'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around 60°F. Later in the day, it will become partly cloudy'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around 60°F. Later in the day, it will become partly cloudy.'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around 60°F. Later in the day, it will become partly cloudy. The'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around 60°F. Later in the day, it will become partly cloudy. The wind'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around 60°F. Later in the day, it will become partly cloudy. The wind is'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around 60°F. Later in the day, it will become partly cloudy. The wind is coming'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around 60°F. Later in the day, it will become partly cloudy. The wind is coming from'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around 60°F. Later in the day, it will become partly cloudy. The wind is coming from the'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around 60°F. Later in the day, it will become partly cloudy. The wind is coming from the west'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around 60°F. Later in the day, it will become partly cloudy. The wind is coming from the west at'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around 60°F. Later in the day, it will become partly cloudy. The wind is coming from the west at '\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around 60°F. Later in the day, it will become partly cloudy. The wind is coming from the west at 10'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around 60°F. Later in the day, it will become partly cloudy. The wind is coming from the west at 10 to'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around 60°F. Later in the day, it will become partly cloudy. The wind is coming from the west at 10 to '\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around 60°F. Later in the day, it will become partly cloudy. The wind is coming from the west at 10 to 15'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around 60°F. Later in the day, it will become partly cloudy. The wind is coming from the west at 10 to 15 mph'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around 60°F. Later in the day, it will become partly cloudy. The wind is coming from the west at 10 to 15 mph.'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around 60°F. Later in the day, it will become partly cloudy. The wind is coming from the west at 10 to 15 mph. Tomorrow'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around 60°F. Later in the day, it will become partly cloudy. The wind is coming from the west at 10 to 15 mph. Tomorrow,'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around 60°F. Later in the day, it will become partly cloudy. The wind is coming from the west at 10 to 15 mph. Tomorrow, there'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around 60°F. Later in the day, it will become partly cloudy. The wind is coming from the west at 10 to 15 mph. Tomorrow, there will'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around 60°F. Later in the day, it will become partly cloudy. The wind is coming from the west at 10 to 15 mph. Tomorrow, there will be'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around 60°F. Later in the day, it will become partly cloudy. The wind is coming from the west at 10 to 15 mph. Tomorrow, there will be considerable'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around 60°F. Later in the day, it will become partly cloudy. The wind is coming from the west at 10 to 15 mph. Tomorrow, there will be considerable cloud'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around 60°F. Later in the day, it will become partly cloudy. The wind is coming from the west at 10 to 15 mph. Tomorrow, there will be considerable cloudiness'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around 60°F. Later in the day, it will become partly cloudy. The wind is coming from the west at 10 to 15 mph. Tomorrow, there will be considerable cloudiness with'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around 60°F. Later in the day, it will become partly cloudy. The wind is coming from the west at 10 to 15 mph. Tomorrow, there will be considerable cloudiness with occasional'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around 60°F. Later in the day, it will become partly cloudy. The wind is coming from the west at 10 to 15 mph. Tomorrow, there will be considerable cloudiness with occasional rain'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around 60°F. Later in the day, it will become partly cloudy. The wind is coming from the west at 10 to 15 mph. Tomorrow, there will be considerable cloudiness with occasional rain showers'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around 60°F. Later in the day, it will become partly cloudy. The wind is coming from the west at 10 to 15 mph. Tomorrow, there will be considerable cloudiness with occasional rain showers and'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around 60°F. Later in the day, it will become partly cloudy. The wind is coming from the west at 10 to 15 mph. Tomorrow, there will be considerable cloudiness with occasional rain showers and a'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around 60°F. Later in the day, it will become partly cloudy. The wind is coming from the west at 10 to 15 mph. Tomorrow, there will be considerable cloudiness with occasional rain showers and a high'\n", - "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around 60°F. Later in the day, it will become partly cloudy. The wind is coming from the west at 10 to 15 mph. Tomorrow, there will be considerable cloudiness with occasional rain showers and a high of'\n", + "/final_output\n", + "None\n", "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around 60°F. Later in the day, it will become partly cloudy. The wind is coming from the west at 10 to 15 mph. Tomorrow, there will be considerable cloudiness with occasional rain showers and a high of '\n", + "/logs/where_cat_is_hiding\n", + "{'id': '21fde139-0dfa-42bb-ad90-b5b1e984aaba', 'name': 'where_cat_is_hiding', 'type': 'tool', 'tags': [], 'metadata': {}, 'start_time': '2024-01-22T20:38:44.208+00:00', 'streamed_output': [], 'streamed_output_str': [], 'final_output': None, 'end_time': None}\n", "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around 60°F. Later in the day, it will become partly cloudy. The wind is coming from the west at 10 to 15 mph. Tomorrow, there will be considerable cloudiness with occasional rain showers and a high of 59'\n", + "/logs/where_cat_is_hiding/end_time\n", + "2024-01-22T20:38:44.208+00:00\n", "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around 60°F. Later in the day, it will become partly cloudy. The wind is coming from the west at 10 to 15 mph. Tomorrow, there will be considerable cloudiness with occasional rain showers and a high of 59°F'\n", + "/final_output/messages/1\n", + "content='under the bed' name='where_cat_is_hiding'\n", "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around 60°F. Later in the day, it will become partly cloudy. The wind is coming from the west at 10 to 15 mph. Tomorrow, there will be considerable cloudiness with occasional rain showers and a high of 59°F.'\n", + "/logs/RunnableSequence:2\n", + "{'id': '37d52845-b689-4c18-9c10-ffdd0c4054b0', 'name': 'RunnableSequence', 'type': 'chain', 'tags': [], 'metadata': {}, 'start_time': '2024-01-22T20:38:44.210+00:00', 'streamed_output': [], 'streamed_output_str': [], 'final_output': None, 'end_time': None}\n", "----\n", - "/logs/ChatOpenAI:2/streamed_output/-\n", - "content='The weather in San Francisco is currently foggy with a high around 60°F. Later in the day, it will become partly cloudy. The wind is coming from the west at 10 to 15 mph. Tomorrow, there will be considerable cloudiness with occasional rain showers and a high of 59°F.'\n", + "/logs/RunnableAssign:2\n", + "{'id': '30024dea-064f-4b04-b130-671f47ac59bc', 'name': 'RunnableAssign', 'type': 'chain', 'tags': ['seq:step:1'], 'metadata': {}, 'start_time': '2024-01-22T20:38:44.213+00:00', 'streamed_output': [], 'streamed_output_str': [], 'final_output': None, 'end_time': None}\n", "----\n", - "/logs/ChatOpenAI:2/end_time\n", - "2023-12-26T17:56:25.673\n", + "/logs/RunnableAssign:2/streamed_output/-\n", + "{'input': 'where is the cat hiding? what items are in that location?', 'intermediate_steps': [(OpenAIToolAgentAction(tool='where_cat_is_hiding', tool_input={}, log='\\nInvoking: `where_cat_is_hiding` with `{}`\\n\\n\\n', message_log=[AIMessageChunk(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_gKFg6FX8ZQ88wFUs94yx86PF', 'function': {'arguments': '{}', 'name': 'where_cat_is_hiding'}, 'type': 'function'}]})], tool_call_id='call_gKFg6FX8ZQ88wFUs94yx86PF'), 'under the bed')]}\n", "----\n", - "/final_output/messages/2\n", - "content='The weather in San Francisco is currently foggy with a high around 60°F. Later in the day, it will become partly cloudy. The wind is coming from the west at 10 to 15 mph. Tomorrow, there will be considerable cloudiness with occasional rain showers and a high of 59°F.'\n", + "/logs/RunnableParallel:2\n", + "{'id': '98906cd7-93c2-47e8-a7d7-2e8d4ab09ed0', 'name': 'RunnableParallel', 'type': 'chain', 'tags': [], 'metadata': {}, 'start_time': '2024-01-22T20:38:44.215+00:00', 'streamed_output': [], 'streamed_output_str': [], 'final_output': None, 'end_time': None}\n", "----\n" ] } ], "source": [ + "i = 0\n", "path_status = {}\n", "async for chunk in agent_executor.astream_log(\n", - " {\"input\": \"what is the weather in sf\", \"chat_history\": []},\n", - " include_names=[\"ChatOpenAI\"],\n", + " {\"input\": \"where is the cat hiding? what items are in that location?\"},\n", "):\n", " for op in chunk.ops:\n", " if op[\"op\"] == \"add\":\n", @@ -1071,16 +840,291 @@ " path_status[op[\"path\"]] += op[\"value\"]\n", " print(op[\"path\"])\n", " print(path_status.get(op[\"path\"]))\n", - " print(\"----\")" + " print(\"----\")\n", + " i += 1\n", + " if i > 30:\n", + " break" + ] + }, + { + "cell_type": "markdown", + "id": "d85bf6ed-8d89-46fb-bbd8-6c84de7ae18f", + "metadata": {}, + "source": [ + "#### Using callbacks (Legacy)\n", + "\n", + "Another approach to streaming is using callbacks. This may be useful if you're still on an older version of LangChain and cannot upgrade.\n", + "\n", + "Generall, this is **NOT** a recommended approach because:\n", + "\n", + "1. for most applications, you'll need to create two workers, write the callbacks to a queue and have another worker reading from the queue (i.e., there's hidden complexity to make this work).\n", + "2. **end** events may be missing some metadata (e.g., like run name). So if you need the additional metadata, you should inherit from `BaseTracer` instead of `AsyncCallbackHandler` to pick up the relevant information from the runs (aka traces), or else implement the aggregation logic yourself based on the `run_id`.\n", + "3. There is inconsistent behavior with the callbacks (e.g., how inputs and outputs are encoded) depending on the callback type that you'll need to workaround.\n", + "\n", + "For illustration purposes, we implement a callback below that shows how to get *token by token* streaming. Feel free to implement other callbacks based on your application needs.\n", + "\n", + "But `astream_events` does all of this you under the hood, so you don't have to!" ] }, { "cell_type": "code", - "execution_count": null, - "id": "4fdfc76d", + "execution_count": 16, + "id": "2c577a4a-b754-4c32-a951-8003b876ea9a", "metadata": {}, - "outputs": [], - "source": [] + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "on chain start: \n", + "{'input': 'where is the cat hiding and what items can be found there?'}\n", + "on chain start: \n", + "{'input': ''}\n", + "on chain start: \n", + "{'input': ''}\n", + "on chain start: \n", + "{'input': ''}\n", + "on chain start: \n", + "{'input': ''}\n", + "On chain end\n", + "[]\n", + "On chain end\n", + "{'agent_scratchpad': []}\n", + "On chain end\n", + "{'input': 'where is the cat hiding and what items can be found there?', 'intermediate_steps': [], 'agent_scratchpad': []}\n", + "on chain start: \n", + "{'input': 'where is the cat hiding and what items can be found there?', 'intermediate_steps': [], 'agent_scratchpad': []}\n", + "On chain end\n", + "{'lc': 1, 'type': 'constructor', 'id': ['langchain', 'prompts', 'chat', 'ChatPromptValue'], 'kwargs': {'messages': [{'lc': 1, 'type': 'constructor', 'id': ['langchain', 'schema', 'messages', 'SystemMessage'], 'kwargs': {'content': 'You are a helpful assistant', 'additional_kwargs': {}}}, {'lc': 1, 'type': 'constructor', 'id': ['langchain', 'schema', 'messages', 'HumanMessage'], 'kwargs': {'content': 'where is the cat hiding and what items can be found there?', 'additional_kwargs': {}}}]}}\n", + "agent_llm: \n", + "\n", + "on chain start: \n", + "content='' additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_pboyZTT0587rJtujUluO2OOc', 'function': {'arguments': '{}', 'name': 'where_cat_is_hiding'}, 'type': 'function'}]}\n", + "On chain end\n", + "[{'lc': 1, 'type': 'constructor', 'id': ['langchain', 'schema', 'agent', 'OpenAIToolAgentAction'], 'kwargs': {'tool': 'where_cat_is_hiding', 'tool_input': {}, 'log': '\\nInvoking: `where_cat_is_hiding` with `{}`\\n\\n\\n', 'message_log': [{'lc': 1, 'type': 'constructor', 'id': ['langchain', 'schema', 'messages', 'AIMessageChunk'], 'kwargs': {'example': False, 'content': '', 'additional_kwargs': {'tool_calls': [{'index': 0, 'id': 'call_pboyZTT0587rJtujUluO2OOc', 'function': {'arguments': '{}', 'name': 'where_cat_is_hiding'}, 'type': 'function'}]}}}], 'tool_call_id': 'call_pboyZTT0587rJtujUluO2OOc'}}]\n", + "On chain end\n", + "[OpenAIToolAgentAction(tool='where_cat_is_hiding', tool_input={}, log='\\nInvoking: `where_cat_is_hiding` with `{}`\\n\\n\\n', message_log=[AIMessageChunk(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_pboyZTT0587rJtujUluO2OOc', 'function': {'arguments': '{}', 'name': 'where_cat_is_hiding'}, 'type': 'function'}]})], tool_call_id='call_pboyZTT0587rJtujUluO2OOc')]\n", + "Tool start\n", + "{'name': 'where_cat_is_hiding', 'description': 'where_cat_is_hiding() -> str - Where is the cat hiding right now?'}\n", + "Tool end\n", + "on the shelf\n", + "on chain start: \n", + "{'input': ''}\n", + "on chain start: \n", + "{'input': ''}\n", + "on chain start: \n", + "{'input': ''}\n", + "on chain start: \n", + "{'input': ''}\n", + "On chain end\n", + "[AIMessageChunk(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_pboyZTT0587rJtujUluO2OOc', 'function': {'arguments': '{}', 'name': 'where_cat_is_hiding'}, 'type': 'function'}]}), ToolMessage(content='on the shelf', additional_kwargs={'name': 'where_cat_is_hiding'}, tool_call_id='call_pboyZTT0587rJtujUluO2OOc')]\n", + "On chain end\n", + "{'agent_scratchpad': [AIMessageChunk(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_pboyZTT0587rJtujUluO2OOc', 'function': {'arguments': '{}', 'name': 'where_cat_is_hiding'}, 'type': 'function'}]}), ToolMessage(content='on the shelf', additional_kwargs={'name': 'where_cat_is_hiding'}, tool_call_id='call_pboyZTT0587rJtujUluO2OOc')]}\n", + "On chain end\n", + "{'input': 'where is the cat hiding and what items can be found there?', 'intermediate_steps': [(OpenAIToolAgentAction(tool='where_cat_is_hiding', tool_input={}, log='\\nInvoking: `where_cat_is_hiding` with `{}`\\n\\n\\n', message_log=[AIMessageChunk(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_pboyZTT0587rJtujUluO2OOc', 'function': {'arguments': '{}', 'name': 'where_cat_is_hiding'}, 'type': 'function'}]})], tool_call_id='call_pboyZTT0587rJtujUluO2OOc'), 'on the shelf')], 'agent_scratchpad': [AIMessageChunk(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_pboyZTT0587rJtujUluO2OOc', 'function': {'arguments': '{}', 'name': 'where_cat_is_hiding'}, 'type': 'function'}]}), ToolMessage(content='on the shelf', additional_kwargs={'name': 'where_cat_is_hiding'}, tool_call_id='call_pboyZTT0587rJtujUluO2OOc')]}\n", + "on chain start: \n", + "{'input': 'where is the cat hiding and what items can be found there?', 'intermediate_steps': [(OpenAIToolAgentAction(tool='where_cat_is_hiding', tool_input={}, log='\\nInvoking: `where_cat_is_hiding` with `{}`\\n\\n\\n', message_log=[AIMessageChunk(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_pboyZTT0587rJtujUluO2OOc', 'function': {'arguments': '{}', 'name': 'where_cat_is_hiding'}, 'type': 'function'}]})], tool_call_id='call_pboyZTT0587rJtujUluO2OOc'), 'on the shelf')], 'agent_scratchpad': [AIMessageChunk(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_pboyZTT0587rJtujUluO2OOc', 'function': {'arguments': '{}', 'name': 'where_cat_is_hiding'}, 'type': 'function'}]}), ToolMessage(content='on the shelf', additional_kwargs={'name': 'where_cat_is_hiding'}, tool_call_id='call_pboyZTT0587rJtujUluO2OOc')]}\n", + "On chain end\n", + "{'lc': 1, 'type': 'constructor', 'id': ['langchain', 'prompts', 'chat', 'ChatPromptValue'], 'kwargs': {'messages': [{'lc': 1, 'type': 'constructor', 'id': ['langchain', 'schema', 'messages', 'SystemMessage'], 'kwargs': {'content': 'You are a helpful assistant', 'additional_kwargs': {}}}, {'lc': 1, 'type': 'constructor', 'id': ['langchain', 'schema', 'messages', 'HumanMessage'], 'kwargs': {'content': 'where is the cat hiding and what items can be found there?', 'additional_kwargs': {}}}, {'lc': 1, 'type': 'constructor', 'id': ['langchain', 'schema', 'messages', 'AIMessageChunk'], 'kwargs': {'example': False, 'content': '', 'additional_kwargs': {'tool_calls': [{'index': 0, 'id': 'call_pboyZTT0587rJtujUluO2OOc', 'function': {'arguments': '{}', 'name': 'where_cat_is_hiding'}, 'type': 'function'}]}}}, {'lc': 1, 'type': 'constructor', 'id': ['langchain', 'schema', 'messages', 'ToolMessage'], 'kwargs': {'tool_call_id': 'call_pboyZTT0587rJtujUluO2OOc', 'content': 'on the shelf', 'additional_kwargs': {'name': 'where_cat_is_hiding'}}}]}}\n", + "agent_llm: \n", + "\n", + "on chain start: \n", + "content='' additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_vIVtgUb9Gvmc3zAGIrshnmbh', 'function': {'arguments': '{\\n \"place\": \"shelf\"\\n}', 'name': 'get_items'}, 'type': 'function'}]}\n", + "On chain end\n", + "[{'lc': 1, 'type': 'constructor', 'id': ['langchain', 'schema', 'agent', 'OpenAIToolAgentAction'], 'kwargs': {'tool': 'get_items', 'tool_input': {'place': 'shelf'}, 'log': \"\\nInvoking: `get_items` with `{'place': 'shelf'}`\\n\\n\\n\", 'message_log': [{'lc': 1, 'type': 'constructor', 'id': ['langchain', 'schema', 'messages', 'AIMessageChunk'], 'kwargs': {'example': False, 'content': '', 'additional_kwargs': {'tool_calls': [{'index': 0, 'id': 'call_vIVtgUb9Gvmc3zAGIrshnmbh', 'function': {'arguments': '{\\n \"place\": \"shelf\"\\n}', 'name': 'get_items'}, 'type': 'function'}]}}}], 'tool_call_id': 'call_vIVtgUb9Gvmc3zAGIrshnmbh'}}]\n", + "On chain end\n", + "[OpenAIToolAgentAction(tool='get_items', tool_input={'place': 'shelf'}, log=\"\\nInvoking: `get_items` with `{'place': 'shelf'}`\\n\\n\\n\", message_log=[AIMessageChunk(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_vIVtgUb9Gvmc3zAGIrshnmbh', 'function': {'arguments': '{\\n \"place\": \"shelf\"\\n}', 'name': 'get_items'}, 'type': 'function'}]})], tool_call_id='call_vIVtgUb9Gvmc3zAGIrshnmbh')]\n", + "Tool start\n", + "{'name': 'get_items', 'description': 'get_items(place: str, callbacks: Union[List[langchain_core.callbacks.base.BaseCallbackHandler], langchain_core.callbacks.base.BaseCallbackManager, NoneType]) -> str - Use this tool to look up which items are in the given place.'}\n", + "tool_llm: In| a| shelf|,| you| might| find|:\n", + "\n", + "|1|.| Books|:| A| shelf| is| commonly| used| to| store| books|.| Books| can| be| of| various| genres|,| such| as| novels|,| textbooks|,| or| reference| books|.| They| provide| knowledge|,| entertainment|,| and| can| transport| you| to| different| worlds| through| storytelling|.\n", + "\n", + "|2|.| Decor|ative| items|:| Sh|elves| often| serve| as| a| display| area| for| decorative| items| like| figur|ines|,| v|ases|,| or| sculptures|.| These| items| add| aesthetic| value| to| the| space| and| reflect| the| owner|'s| personal| taste| and| style|.\n", + "\n", + "|3|.| Storage| boxes|:| Sh|elves| can| also| be| used| to| store| various| items| in| organized| boxes|.| These| boxes| can| hold| anything| from| office| supplies|,| craft| materials|,| or| sentimental| items|.| They| help| keep| the| space| tidy| and| provide| easy| access| to| stored| belongings|.|\n", + "\n", + "Tool end\n", + "In a shelf, you might find:\n", + "\n", + "1. Books: A shelf is commonly used to store books. Books can be of various genres, such as novels, textbooks, or reference books. They provide knowledge, entertainment, and can transport you to different worlds through storytelling.\n", + "\n", + "2. Decorative items: Shelves often serve as a display area for decorative items like figurines, vases, or sculptures. These items add aesthetic value to the space and reflect the owner's personal taste and style.\n", + "\n", + "3. Storage boxes: Shelves can also be used to store various items in organized boxes. These boxes can hold anything from office supplies, craft materials, or sentimental items. They help keep the space tidy and provide easy access to stored belongings.\n", + "on chain start: \n", + "{'input': ''}\n", + "on chain start: \n", + "{'input': ''}\n", + "on chain start: \n", + "{'input': ''}\n", + "on chain start: \n", + "{'input': ''}\n", + "On chain end\n", + "[AIMessageChunk(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_pboyZTT0587rJtujUluO2OOc', 'function': {'arguments': '{}', 'name': 'where_cat_is_hiding'}, 'type': 'function'}]}), ToolMessage(content='on the shelf', additional_kwargs={'name': 'where_cat_is_hiding'}, tool_call_id='call_pboyZTT0587rJtujUluO2OOc'), AIMessageChunk(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_vIVtgUb9Gvmc3zAGIrshnmbh', 'function': {'arguments': '{\\n \"place\": \"shelf\"\\n}', 'name': 'get_items'}, 'type': 'function'}]}), ToolMessage(content=\"In a shelf, you might find:\\n\\n1. Books: A shelf is commonly used to store books. Books can be of various genres, such as novels, textbooks, or reference books. They provide knowledge, entertainment, and can transport you to different worlds through storytelling.\\n\\n2. Decorative items: Shelves often serve as a display area for decorative items like figurines, vases, or sculptures. These items add aesthetic value to the space and reflect the owner's personal taste and style.\\n\\n3. Storage boxes: Shelves can also be used to store various items in organized boxes. These boxes can hold anything from office supplies, craft materials, or sentimental items. They help keep the space tidy and provide easy access to stored belongings.\", additional_kwargs={'name': 'get_items'}, tool_call_id='call_vIVtgUb9Gvmc3zAGIrshnmbh')]\n", + "On chain end\n", + "{'agent_scratchpad': [AIMessageChunk(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_pboyZTT0587rJtujUluO2OOc', 'function': {'arguments': '{}', 'name': 'where_cat_is_hiding'}, 'type': 'function'}]}), ToolMessage(content='on the shelf', additional_kwargs={'name': 'where_cat_is_hiding'}, tool_call_id='call_pboyZTT0587rJtujUluO2OOc'), AIMessageChunk(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_vIVtgUb9Gvmc3zAGIrshnmbh', 'function': {'arguments': '{\\n \"place\": \"shelf\"\\n}', 'name': 'get_items'}, 'type': 'function'}]}), ToolMessage(content=\"In a shelf, you might find:\\n\\n1. Books: A shelf is commonly used to store books. Books can be of various genres, such as novels, textbooks, or reference books. They provide knowledge, entertainment, and can transport you to different worlds through storytelling.\\n\\n2. Decorative items: Shelves often serve as a display area for decorative items like figurines, vases, or sculptures. These items add aesthetic value to the space and reflect the owner's personal taste and style.\\n\\n3. Storage boxes: Shelves can also be used to store various items in organized boxes. These boxes can hold anything from office supplies, craft materials, or sentimental items. They help keep the space tidy and provide easy access to stored belongings.\", additional_kwargs={'name': 'get_items'}, tool_call_id='call_vIVtgUb9Gvmc3zAGIrshnmbh')]}\n", + "On chain end\n", + "{'input': 'where is the cat hiding and what items can be found there?', 'intermediate_steps': [(OpenAIToolAgentAction(tool='where_cat_is_hiding', tool_input={}, log='\\nInvoking: `where_cat_is_hiding` with `{}`\\n\\n\\n', message_log=[AIMessageChunk(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_pboyZTT0587rJtujUluO2OOc', 'function': {'arguments': '{}', 'name': 'where_cat_is_hiding'}, 'type': 'function'}]})], tool_call_id='call_pboyZTT0587rJtujUluO2OOc'), 'on the shelf'), (OpenAIToolAgentAction(tool='get_items', tool_input={'place': 'shelf'}, log=\"\\nInvoking: `get_items` with `{'place': 'shelf'}`\\n\\n\\n\", message_log=[AIMessageChunk(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_vIVtgUb9Gvmc3zAGIrshnmbh', 'function': {'arguments': '{\\n \"place\": \"shelf\"\\n}', 'name': 'get_items'}, 'type': 'function'}]})], tool_call_id='call_vIVtgUb9Gvmc3zAGIrshnmbh'), \"In a shelf, you might find:\\n\\n1. Books: A shelf is commonly used to store books. Books can be of various genres, such as novels, textbooks, or reference books. They provide knowledge, entertainment, and can transport you to different worlds through storytelling.\\n\\n2. Decorative items: Shelves often serve as a display area for decorative items like figurines, vases, or sculptures. These items add aesthetic value to the space and reflect the owner's personal taste and style.\\n\\n3. Storage boxes: Shelves can also be used to store various items in organized boxes. These boxes can hold anything from office supplies, craft materials, or sentimental items. They help keep the space tidy and provide easy access to stored belongings.\")], 'agent_scratchpad': [AIMessageChunk(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_pboyZTT0587rJtujUluO2OOc', 'function': {'arguments': '{}', 'name': 'where_cat_is_hiding'}, 'type': 'function'}]}), ToolMessage(content='on the shelf', additional_kwargs={'name': 'where_cat_is_hiding'}, tool_call_id='call_pboyZTT0587rJtujUluO2OOc'), AIMessageChunk(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_vIVtgUb9Gvmc3zAGIrshnmbh', 'function': {'arguments': '{\\n \"place\": \"shelf\"\\n}', 'name': 'get_items'}, 'type': 'function'}]}), ToolMessage(content=\"In a shelf, you might find:\\n\\n1. Books: A shelf is commonly used to store books. Books can be of various genres, such as novels, textbooks, or reference books. They provide knowledge, entertainment, and can transport you to different worlds through storytelling.\\n\\n2. Decorative items: Shelves often serve as a display area for decorative items like figurines, vases, or sculptures. These items add aesthetic value to the space and reflect the owner's personal taste and style.\\n\\n3. Storage boxes: Shelves can also be used to store various items in organized boxes. These boxes can hold anything from office supplies, craft materials, or sentimental items. They help keep the space tidy and provide easy access to stored belongings.\", additional_kwargs={'name': 'get_items'}, tool_call_id='call_vIVtgUb9Gvmc3zAGIrshnmbh')]}\n", + "on chain start: \n", + "{'input': 'where is the cat hiding and what items can be found there?', 'intermediate_steps': [(OpenAIToolAgentAction(tool='where_cat_is_hiding', tool_input={}, log='\\nInvoking: `where_cat_is_hiding` with `{}`\\n\\n\\n', message_log=[AIMessageChunk(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_pboyZTT0587rJtujUluO2OOc', 'function': {'arguments': '{}', 'name': 'where_cat_is_hiding'}, 'type': 'function'}]})], tool_call_id='call_pboyZTT0587rJtujUluO2OOc'), 'on the shelf'), (OpenAIToolAgentAction(tool='get_items', tool_input={'place': 'shelf'}, log=\"\\nInvoking: `get_items` with `{'place': 'shelf'}`\\n\\n\\n\", message_log=[AIMessageChunk(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_vIVtgUb9Gvmc3zAGIrshnmbh', 'function': {'arguments': '{\\n \"place\": \"shelf\"\\n}', 'name': 'get_items'}, 'type': 'function'}]})], tool_call_id='call_vIVtgUb9Gvmc3zAGIrshnmbh'), \"In a shelf, you might find:\\n\\n1. Books: A shelf is commonly used to store books. Books can be of various genres, such as novels, textbooks, or reference books. They provide knowledge, entertainment, and can transport you to different worlds through storytelling.\\n\\n2. Decorative items: Shelves often serve as a display area for decorative items like figurines, vases, or sculptures. These items add aesthetic value to the space and reflect the owner's personal taste and style.\\n\\n3. Storage boxes: Shelves can also be used to store various items in organized boxes. These boxes can hold anything from office supplies, craft materials, or sentimental items. They help keep the space tidy and provide easy access to stored belongings.\")], 'agent_scratchpad': [AIMessageChunk(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_pboyZTT0587rJtujUluO2OOc', 'function': {'arguments': '{}', 'name': 'where_cat_is_hiding'}, 'type': 'function'}]}), ToolMessage(content='on the shelf', additional_kwargs={'name': 'where_cat_is_hiding'}, tool_call_id='call_pboyZTT0587rJtujUluO2OOc'), AIMessageChunk(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_vIVtgUb9Gvmc3zAGIrshnmbh', 'function': {'arguments': '{\\n \"place\": \"shelf\"\\n}', 'name': 'get_items'}, 'type': 'function'}]}), ToolMessage(content=\"In a shelf, you might find:\\n\\n1. Books: A shelf is commonly used to store books. Books can be of various genres, such as novels, textbooks, or reference books. They provide knowledge, entertainment, and can transport you to different worlds through storytelling.\\n\\n2. Decorative items: Shelves often serve as a display area for decorative items like figurines, vases, or sculptures. These items add aesthetic value to the space and reflect the owner's personal taste and style.\\n\\n3. Storage boxes: Shelves can also be used to store various items in organized boxes. These boxes can hold anything from office supplies, craft materials, or sentimental items. They help keep the space tidy and provide easy access to stored belongings.\", additional_kwargs={'name': 'get_items'}, tool_call_id='call_vIVtgUb9Gvmc3zAGIrshnmbh')]}\n", + "On chain end\n", + "{'lc': 1, 'type': 'constructor', 'id': ['langchain', 'prompts', 'chat', 'ChatPromptValue'], 'kwargs': {'messages': [{'lc': 1, 'type': 'constructor', 'id': ['langchain', 'schema', 'messages', 'SystemMessage'], 'kwargs': {'content': 'You are a helpful assistant', 'additional_kwargs': {}}}, {'lc': 1, 'type': 'constructor', 'id': ['langchain', 'schema', 'messages', 'HumanMessage'], 'kwargs': {'content': 'where is the cat hiding and what items can be found there?', 'additional_kwargs': {}}}, {'lc': 1, 'type': 'constructor', 'id': ['langchain', 'schema', 'messages', 'AIMessageChunk'], 'kwargs': {'example': False, 'content': '', 'additional_kwargs': {'tool_calls': [{'index': 0, 'id': 'call_pboyZTT0587rJtujUluO2OOc', 'function': {'arguments': '{}', 'name': 'where_cat_is_hiding'}, 'type': 'function'}]}}}, {'lc': 1, 'type': 'constructor', 'id': ['langchain', 'schema', 'messages', 'ToolMessage'], 'kwargs': {'tool_call_id': 'call_pboyZTT0587rJtujUluO2OOc', 'content': 'on the shelf', 'additional_kwargs': {'name': 'where_cat_is_hiding'}}}, {'lc': 1, 'type': 'constructor', 'id': ['langchain', 'schema', 'messages', 'AIMessageChunk'], 'kwargs': {'example': False, 'content': '', 'additional_kwargs': {'tool_calls': [{'index': 0, 'id': 'call_vIVtgUb9Gvmc3zAGIrshnmbh', 'function': {'arguments': '{\\n \"place\": \"shelf\"\\n}', 'name': 'get_items'}, 'type': 'function'}]}}}, {'lc': 1, 'type': 'constructor', 'id': ['langchain', 'schema', 'messages', 'ToolMessage'], 'kwargs': {'tool_call_id': 'call_vIVtgUb9Gvmc3zAGIrshnmbh', 'content': \"In a shelf, you might find:\\n\\n1. Books: A shelf is commonly used to store books. Books can be of various genres, such as novels, textbooks, or reference books. They provide knowledge, entertainment, and can transport you to different worlds through storytelling.\\n\\n2. Decorative items: Shelves often serve as a display area for decorative items like figurines, vases, or sculptures. These items add aesthetic value to the space and reflect the owner's personal taste and style.\\n\\n3. Storage boxes: Shelves can also be used to store various items in organized boxes. These boxes can hold anything from office supplies, craft materials, or sentimental items. They help keep the space tidy and provide easy access to stored belongings.\", 'additional_kwargs': {'name': 'get_items'}}}]}}\n", + "agent_llm: The| cat| is| hiding| on| the| shelf|.| In| the| shelf|,| you| might| find| books|,| decorative| items|,| and| storage| boxes|.|\n", + "\n", + "on chain start: \n", + "content='The cat is hiding on the shelf. In the shelf, you might find books, decorative items, and storage boxes.'\n", + "On chain end\n", + "{'lc': 1, 'type': 'constructor', 'id': ['langchain', 'schema', 'agent', 'AgentFinish'], 'kwargs': {'return_values': {'output': 'The cat is hiding on the shelf. In the shelf, you might find books, decorative items, and storage boxes.'}, 'log': 'The cat is hiding on the shelf. In the shelf, you might find books, decorative items, and storage boxes.'}}\n", + "On chain end\n", + "return_values={'output': 'The cat is hiding on the shelf. In the shelf, you might find books, decorative items, and storage boxes.'} log='The cat is hiding on the shelf. In the shelf, you might find books, decorative items, and storage boxes.'\n", + "On chain end\n", + "{'output': 'The cat is hiding on the shelf. In the shelf, you might find books, decorative items, and storage boxes.'}\n" + ] + } + ], + "source": [ + "from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, TypeVar, Union\n", + "from uuid import UUID\n", + "\n", + "from langchain_core.callbacks.base import AsyncCallbackHandler\n", + "from langchain_core.messages import BaseMessage\n", + "from langchain_core.outputs import ChatGenerationChunk, GenerationChunk, LLMResult\n", + "\n", + "# Here is a custom handler that will print the tokens to stdout.\n", + "# Instead of printing to stdout you can send the data elsewhere; e.g., to a streaming API response\n", + "\n", + "\n", + "class TokenByTokenHandler(AsyncCallbackHandler):\n", + " def __init__(self, tags_of_interest: List[str]) -> None:\n", + " \"\"\"A custom call back handler.\n", + "\n", + " Args:\n", + " tags_of_interest: Only LLM tokens from models with these tags will be\n", + " printed.\n", + " \"\"\"\n", + " self.tags_of_interest = tags_of_interest\n", + "\n", + " async def on_chain_start(\n", + " self,\n", + " serialized: Dict[str, Any],\n", + " inputs: Dict[str, Any],\n", + " *,\n", + " run_id: UUID,\n", + " parent_run_id: Optional[UUID] = None,\n", + " tags: Optional[List[str]] = None,\n", + " metadata: Optional[Dict[str, Any]] = None,\n", + " **kwargs: Any,\n", + " ) -> None:\n", + " \"\"\"Run when chain starts running.\"\"\"\n", + " print(\"on chain start: \")\n", + " print(inputs)\n", + "\n", + " async def on_chain_end(\n", + " self,\n", + " outputs: Dict[str, Any],\n", + " *,\n", + " run_id: UUID,\n", + " parent_run_id: Optional[UUID] = None,\n", + " tags: Optional[List[str]] = None,\n", + " **kwargs: Any,\n", + " ) -> None:\n", + " \"\"\"Run when chain ends running.\"\"\"\n", + " print(\"On chain end\")\n", + " print(outputs)\n", + "\n", + " async def on_chat_model_start(\n", + " self,\n", + " serialized: Dict[str, Any],\n", + " messages: List[List[BaseMessage]],\n", + " *,\n", + " run_id: UUID,\n", + " parent_run_id: Optional[UUID] = None,\n", + " tags: Optional[List[str]] = None,\n", + " metadata: Optional[Dict[str, Any]] = None,\n", + " **kwargs: Any,\n", + " ) -> Any:\n", + " \"\"\"Run when a chat model starts running.\"\"\"\n", + " overlap_tags = self.get_overlap_tags(tags)\n", + "\n", + " if overlap_tags:\n", + " print(\",\".join(overlap_tags), end=\": \", flush=True)\n", + "\n", + " def on_tool_start(\n", + " self,\n", + " serialized: Dict[str, Any],\n", + " input_str: str,\n", + " *,\n", + " run_id: UUID,\n", + " parent_run_id: Optional[UUID] = None,\n", + " tags: Optional[List[str]] = None,\n", + " metadata: Optional[Dict[str, Any]] = None,\n", + " inputs: Optional[Dict[str, Any]] = None,\n", + " **kwargs: Any,\n", + " ) -> Any:\n", + " \"\"\"Run when tool starts running.\"\"\"\n", + " print(\"Tool start\")\n", + " print(serialized)\n", + "\n", + " def on_tool_end(\n", + " self,\n", + " output: str,\n", + " *,\n", + " run_id: UUID,\n", + " parent_run_id: Optional[UUID] = None,\n", + " **kwargs: Any,\n", + " ) -> Any:\n", + " \"\"\"Run when tool ends running.\"\"\"\n", + " print(\"Tool end\")\n", + " print(output)\n", + "\n", + " async def on_llm_end(\n", + " self,\n", + " response: LLMResult,\n", + " *,\n", + " run_id: UUID,\n", + " parent_run_id: Optional[UUID] = None,\n", + " tags: Optional[List[str]] = None,\n", + " **kwargs: Any,\n", + " ) -> None:\n", + " \"\"\"Run when LLM ends running.\"\"\"\n", + " overlap_tags = self.get_overlap_tags(tags)\n", + "\n", + " if overlap_tags:\n", + " # Who can argue with beauty?\n", + " print()\n", + " print()\n", + "\n", + " def get_overlap_tags(self, tags: Optional[List[str]]) -> List[str]:\n", + " \"\"\"Check for overlap with filtered tags.\"\"\"\n", + " if not tags:\n", + " return []\n", + " return sorted(set(tags or []) & set(self.tags_of_interest or []))\n", + "\n", + " async def on_llm_new_token(\n", + " self,\n", + " token: str,\n", + " *,\n", + " chunk: Optional[Union[GenerationChunk, ChatGenerationChunk]] = None,\n", + " run_id: UUID,\n", + " parent_run_id: Optional[UUID] = None,\n", + " tags: Optional[List[str]] = None,\n", + " **kwargs: Any,\n", + " ) -> None:\n", + " \"\"\"Run on new LLM token. Only available when streaming is enabled.\"\"\"\n", + " overlap_tags = self.get_overlap_tags(tags)\n", + "\n", + " if token and overlap_tags:\n", + " print(token, end=\"|\", flush=True)\n", + "\n", + "\n", + "handler = TokenByTokenHandler(tags_of_interest=[\"tool_llm\", \"agent_llm\"])\n", + "\n", + "result = await agent_executor.ainvoke(\n", + " {\"input\": \"where is the cat hiding and what items can be found there?\"},\n", + " {\"callbacks\": [handler]},\n", + ")" + ] } ], "metadata": { @@ -1099,7 +1143,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.1" + "version": "3.11.4" } }, "nbformat": 4, diff --git a/docs/docs/modules/callbacks/custom_chain.mdx b/docs/docs/modules/callbacks/custom_chain.mdx deleted file mode 100644 index 6ec068eea23e9..0000000000000 --- a/docs/docs/modules/callbacks/custom_chain.mdx +++ /dev/null @@ -1,6 +0,0 @@ -# Callbacks for custom chains - - When you create a custom chain you can easily set it up to use the same callback system as all the built-in chains. -`_call`, `_generate`, `_run`, and equivalent async methods on Chains / LLMs / Chat Models / Agents / Tools now receive a 2nd argument called `run_manager` which is bound to that run, and contains the logging methods that can be used by that object (i.e. `on_llm_new_token`). This is useful when constructing a custom chain. See this guide for more information on how to [create custom chains and use callbacks inside them](/docs/modules/chains/how_to/custom_chain). - - diff --git a/docs/docs/modules/callbacks/index.mdx b/docs/docs/modules/callbacks/index.mdx index 9d0a1c78c745f..3a3ba08cfb8b0 100644 --- a/docs/docs/modules/callbacks/index.mdx +++ b/docs/docs/modules/callbacks/index.mdx @@ -95,42 +95,40 @@ prompt = PromptTemplate.from_template("1 + {number} = ") # Constructor callback: First, let's explicitly set the StdOutCallbackHandler when initializing our chain chain = LLMChain(llm=llm, prompt=prompt, callbacks=[handler]) -chain.run(number=2) +chain.invoke({"number":2}) # Use verbose flag: Then, let's use the `verbose` flag to achieve the same result chain = LLMChain(llm=llm, prompt=prompt, verbose=True) -chain.run(number=2) +chain.invoke({"number":2}) # Request callbacks: Finally, let's use the request `callbacks` to achieve the same result chain = LLMChain(llm=llm, prompt=prompt) -chain.run(number=2, callbacks=[handler]) +chain.invoke({"number":2}, {"callbacks":[handler]}) + ``` ``` - > Entering new LLMChain chain... - Prompt after formatting: - 1 + 2 = - - > Finished chain. - +> Entering new LLMChain chain... +Prompt after formatting: +1 + 2 = - > Entering new LLMChain chain... - Prompt after formatting: - 1 + 2 = +> Finished chain. - > Finished chain. +> Entering new LLMChain chain... +Prompt after formatting: +1 + 2 = - > Entering new LLMChain chain... - Prompt after formatting: - 1 + 2 = +> Finished chain. - > Finished chain. +> Entering new LLMChain chain... +Prompt after formatting: +1 + 2 = - '\n\n3' +> Finished chain. ``` diff --git a/docs/docs/modules/data_connection/document_transformers/split_by_token.ipynb b/docs/docs/modules/data_connection/document_transformers/split_by_token.ipynb index f0c36d0022e66..4236d0e38edbd 100644 --- a/docs/docs/modules/data_connection/document_transformers/split_by_token.ipynb +++ b/docs/docs/modules/data_connection/document_transformers/split_by_token.ipynb @@ -419,6 +419,105 @@ "print(texts[0])" ] }, + { + "cell_type": "markdown", + "id": "98a3f975", + "metadata": {}, + "source": [ + "## KoNLPY\n", + "> [KoNLPy: Korean NLP in Python](https://konlpy.org/en/latest/) is is a Python package for natural language processing (NLP) of the Korean language.\n", + "\n", + "Token splitting involves the segmentation of text into smaller, more manageable units called tokens. These tokens are often words, phrases, symbols, or other meaningful elements crucial for further processing and analysis. In languages like English, token splitting typically involves separating words by spaces and punctuation marks. The effectiveness of token splitting largely depends on the tokenizer's understanding of the language structure, ensuring the generation of meaningful tokens. Since tokenizers designed for the English language are not equipped to understand the unique semantic structures of other languages, such as Korean, they cannot be effectively used for Korean language processing.\n", + "\n", + "### Token splitting for Korean with KoNLPy's Kkma Analyzer\n", + "In case of Korean text, KoNLPY includes at morphological analyzer called `Kkma` (Korean Knowledge Morpheme Analyzer). `Kkma` provides detailed morphological analysis of Korean text. It breaks down sentences into words and words into their respective morphemes, identifying parts of speech for each token. It can segment a block of text into individual sentences, which is particularly useful for processing long texts.\n", + "\n", + "### Usage Considerations\n", + "While `Kkma` is renowned for its detailed analysis, it is important to note that this precision may impact processing speed. Thus, `Kkma` is best suited for applications where analytical depth is prioritized over rapid text processing." + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "88ec8f2f", + "metadata": {}, + "outputs": [], + "source": [ + "# pip install konlpy" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "ddfba6cf", + "metadata": {}, + "outputs": [], + "source": [ + "# This is a long Korean document that we want to split up into its component sentences.\n", + "with open(\"./your_korean_doc.txt\") as f:\n", + " korean_document = f.read()" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "225dfc5c", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain.text_splitter import KonlpyTextSplitter\n", + "\n", + "text_splitter = KonlpyTextSplitter()" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "id": "cf156711", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "춘향전 옛날에 남원에 이 도령이라는 벼슬아치 아들이 있었다.\n", + "\n", + "그의 외모는 빛나는 달처럼 잘생겼고, 그의 학식과 기예는 남보다 뛰어났다.\n", + "\n", + "한편, 이 마을에는 춘향이라는 절세 가인이 살고 있었다.\n", + "\n", + "춘 향의 아름다움은 꽃과 같아 마을 사람들 로부터 많은 사랑을 받았다.\n", + "\n", + "어느 봄날, 도령은 친구들과 놀러 나갔다가 춘 향을 만 나 첫 눈에 반하고 말았다.\n", + "\n", + "두 사람은 서로 사랑하게 되었고, 이내 비밀스러운 사랑의 맹세를 나누었다.\n", + "\n", + "하지만 좋은 날들은 오래가지 않았다.\n", + "\n", + "도령의 아버지가 다른 곳으로 전근을 가게 되어 도령도 떠나 야만 했다.\n", + "\n", + "이별의 아픔 속에서도, 두 사람은 재회를 기약하며 서로를 믿고 기다리기로 했다.\n", + "\n", + "그러나 새로 부임한 관아의 사또가 춘 향의 아름다움에 욕심을 내 어 그녀에게 강요를 시작했다.\n", + "\n", + "춘 향 은 도령에 대한 자신의 사랑을 지키기 위해, 사또의 요구를 단호히 거절했다.\n", + "\n", + "이에 분노한 사또는 춘 향을 감옥에 가두고 혹독한 형벌을 내렸다.\n", + "\n", + "이야기는 이 도령이 고위 관직에 오른 후, 춘 향을 구해 내는 것으로 끝난다.\n", + "\n", + "두 사람은 오랜 시련 끝에 다시 만나게 되고, 그들의 사랑은 온 세상에 전해 지며 후세에까지 이어진다.\n", + "\n", + "- 춘향전 (The Tale of Chunhyang)\n" + ] + } + ], + "source": [ + "texts = text_splitter.split_text(korean_document)\n", + "# The sentences are split with \"\\n\\n\" characters.\n", + "print(texts[0])" + ] + }, { "cell_type": "markdown", "id": "13dc0983", @@ -521,7 +620,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.1" + "version": "3.10.12" }, "vscode": { "interpreter": { diff --git a/docs/docs/modules/data_connection/retrievers/self_query.ipynb b/docs/docs/modules/data_connection/retrievers/self_query.ipynb index 22ab1e46fa67e..2b44db886f0ae 100644 --- a/docs/docs/modules/data_connection/retrievers/self_query.ipynb +++ b/docs/docs/modules/data_connection/retrievers/self_query.ipynb @@ -15,7 +15,7 @@ "\n", "A self-querying retriever is one that, as the name suggests, has the ability to query itself. Specifically, given any natural language query, the retriever uses a query-constructing LLM chain to write a structured query and then applies that structured query to its underlying VectorStore. This allows the retriever to not only use the user-input query for semantic similarity comparison with the contents of stored documents but to also extract filters from the user query on the metadata of stored documents and to execute those filters.\n", "\n", - "![](https://drive.google.com/uc?id=1OQUN-0MJcDUxmPXofgS7MqReEs720pqS)\n", + "![](../../../../static/img/self_querying.jpg)\n", "\n", "## Get started\n", "For demonstration purposes we'll use a `Chroma` vector store. We've created a small demo set of documents that contain summaries of movies.\n", @@ -561,7 +561,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.1" + "version": "3.9.1" } }, "nbformat": 4, diff --git a/docs/docs/modules/model_io/chat/index.mdx b/docs/docs/modules/model_io/chat/index.mdx index 99e72f841d5c2..9c4de13b27e49 100644 --- a/docs/docs/modules/model_io/chat/index.mdx +++ b/docs/docs/modules/model_io/chat/index.mdx @@ -1,5 +1,5 @@ --- -sidebar_position: 2 +sidebar_position: 3 --- # Chat Models diff --git a/docs/docs/modules/model_io/concepts.mdx b/docs/docs/modules/model_io/concepts.mdx index 2e688d8d12df8..b9542076df1cc 100644 --- a/docs/docs/modules/model_io/concepts.mdx +++ b/docs/docs/modules/model_io/concepts.mdx @@ -1,5 +1,5 @@ --- -sidebar_position: 0 +sidebar_position: 1 --- # Concepts diff --git a/docs/docs/modules/model_io/llms/index.mdx b/docs/docs/modules/model_io/llms/index.mdx index 396e7315f02d0..8e7a1e95dabb4 100644 --- a/docs/docs/modules/model_io/llms/index.mdx +++ b/docs/docs/modules/model_io/llms/index.mdx @@ -1,5 +1,5 @@ --- -sidebar_position: 1 +sidebar_position: 4 --- # LLMs diff --git a/docs/docs/modules/model_io/output_parsers/index.mdx b/docs/docs/modules/model_io/output_parsers/index.mdx index ac479f6918418..2f30437618344 100644 --- a/docs/docs/modules/model_io/output_parsers/index.mdx +++ b/docs/docs/modules/model_io/output_parsers/index.mdx @@ -1,5 +1,5 @@ --- -sidebar_position: 4 +sidebar_position: 5 hide_table_of_contents: true --- # Output Parsers diff --git a/docs/docs/modules/model_io/prompts/index.mdx b/docs/docs/modules/model_io/prompts/index.mdx index 53ae4f77546b1..52a5a34bd947a 100644 --- a/docs/docs/modules/model_io/prompts/index.mdx +++ b/docs/docs/modules/model_io/prompts/index.mdx @@ -1,5 +1,5 @@ --- -sidebar_position: 0 +sidebar_position: 2 --- # Prompts diff --git a/docs/docs/use_cases/qa_structured/_category_.yml b/docs/docs/use_cases/qa_structured/_category_.yml deleted file mode 100644 index 4cae9a0c8db1c..0000000000000 --- a/docs/docs/use_cases/qa_structured/_category_.yml +++ /dev/null @@ -1,2 +0,0 @@ -label: 'Q&A over structured data' -position: 0.1 diff --git a/docs/docs/use_cases/qa_structured/sql.ipynb b/docs/docs/use_cases/qa_structured/sql.ipynb deleted file mode 100644 index e19bbe4ccd3f1..0000000000000 --- a/docs/docs/use_cases/qa_structured/sql.ipynb +++ /dev/null @@ -1,1259 +0,0 @@ -{ - "cells": [ - { - "cell_type": "raw", - "metadata": {}, - "source": [ - "---\n", - "title: SQL\n", - "sidebar_position: 2\n", - "---\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/langchain-ai/langchain/blob/master/docs/docs/use_cases/qa_structured/sql.ipynb)\n", - "\n", - "## Use case\n", - "\n", - "Enterprise data is often stored in SQL databases.\n", - "\n", - "LLMs make it possible to interact with SQL databases using natural language.\n", - "\n", - "LangChain offers SQL Chains and Agents to build and run SQL queries based on natural language prompts. \n", - "\n", - "These are compatible with any SQL dialect supported by SQLAlchemy (e.g., MySQL, PostgreSQL, Oracle SQL, Databricks, SQLite).\n", - "\n", - "They enable use cases such as:\n", - "\n", - "- Generating queries that will be run based on natural language questions\n", - "- Creating chatbots that can answer questions based on database data\n", - "- Building custom dashboards based on insights a user wants to analyze\n", - "\n", - "## Overview\n", - "\n", - "LangChain provides tools to interact with SQL Databases:\n", - "\n", - "1. `Build SQL queries` based on natural language user questions\n", - "2. `Query a SQL database` using chains for query creation and execution\n", - "3. `Interact with a SQL database` using agents for robust and flexible querying \n", - "\n", - "![sql_usecase.png](../../../static/img/sql_usecase.png)\n", - "\n", - "## Quickstart\n", - "\n", - "First, get required packages and set environment variables:" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "%pip install --upgrade --quiet langchain langchain-experimental langchain-openai\n", - "\n", - "# Set env var OPENAI_API_KEY or load from a .env file\n", - "# import dotenv\n", - "\n", - "# dotenv.load_dotenv()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The below example will use a SQLite connection with Chinook database. \n", - " \n", - "Follow [installation steps](https://database.guide/2-sample-databases-sqlite/) to create `Chinook.db` in the same directory as this notebook:\n", - "\n", - "* Save [this file](https://raw.githubusercontent.com/lerocha/chinook-database/master/ChinookDatabase/DataSources/Chinook_Sqlite.sql) to the directory as `Chinook_Sqlite.sql`\n", - "* Run `sqlite3 Chinook.db`\n", - "* Run `.read Chinook_Sqlite.sql`\n", - "* Test `SELECT * FROM Artist LIMIT 10;`\n", - "\n", - "Now, `Chinhook.db` is in our directory.\n", - "\n", - "Let's create a `SQLDatabaseChain` to create and execute SQL queries." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "from langchain_community.utilities import SQLDatabase\n", - "from langchain_experimental.sql import SQLDatabaseChain\n", - "from langchain_openai import OpenAI\n", - "\n", - "db = SQLDatabase.from_uri(\"sqlite:///Chinook.db\")\n", - "llm = OpenAI(temperature=0, verbose=True)\n", - "db_chain = SQLDatabaseChain.from_llm(llm, db, verbose=True)" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "scrolled": true - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "\n", - "\u001b[1m> Entering new SQLDatabaseChain chain...\u001b[0m\n", - "How many employees are there?\n", - "SQLQuery:\u001b[32;1m\u001b[1;3mSELECT COUNT(*) FROM \"Employee\";\u001b[0m\n", - "SQLResult: \u001b[33;1m\u001b[1;3m[(8,)]\u001b[0m\n", - "Answer:\u001b[32;1m\u001b[1;3mThere are 8 employees.\u001b[0m\n", - "\u001b[1m> Finished chain.\u001b[0m\n" - ] - }, - { - "data": { - "text/plain": [ - "'There are 8 employees.'" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "db_chain.run(\"How many employees are there?\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Note that this both creates and executes the query. \n", - "\n", - "In the following sections, we will cover the 3 different use cases mentioned in the overview.\n", - "\n", - "### Go deeper\n", - "\n", - "You can load tabular data from other sources other than SQL Databases.\n", - "For example:\n", - "- [Loading a CSV file](/docs/integrations/document_loaders/csv)\n", - "- [Loading a Pandas DataFrame](/docs/integrations/document_loaders/pandas_dataframe)\n", - "Here you can [check full list of Document Loaders](/docs/integrations/document_loaders/)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Case 1: Text-to-SQL query\n" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [], - "source": [ - "from langchain.chains import create_sql_query_chain\n", - "from langchain_openai import ChatOpenAI" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's create the chain that will build the SQL Query:\n" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "SELECT COUNT(*) FROM Employee\n" - ] - } - ], - "source": [ - "chain = create_sql_query_chain(ChatOpenAI(temperature=0), db)\n", - "response = chain.invoke({\"question\": \"How many employees are there\"})\n", - "print(response)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "After building the SQL query based on a user question, we can execute the query:" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'[(8,)]'" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "db.run(response)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "As we can see, the SQL Query Builder chain **only created** the query, and we handled the **query execution separately**." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Go deeper\n", - "\n", - "**Looking under the hood**\n", - "\n", - "We can look at the [LangSmith trace](https://smith.langchain.com/public/c8fa52ea-be46-4829-bde2-52894970b830/r) to unpack this:\n", - "\n", - "[Some papers](https://arxiv.org/pdf/2204.00498.pdf) have reported good performance when prompting with:\n", - " \n", - "* A `CREATE TABLE` description for each table, which include column names, their types, etc\n", - "* Followed by three example rows in a `SELECT` statement\n", - "\n", - "`create_sql_query_chain` adopts this the best practice (see more in this [blog](https://blog.langchain.dev/llms-and-sql/)). \n", - "![sql_usecase.png](../../../static/img/create_sql_query_chain.png)\n", - "\n", - "**Improvements**\n", - "\n", - "The query builder can be improved in several ways, such as (but not limited to):\n", - "\n", - "- Customizing database description to your specific use case\n", - "- Hardcoding a few examples of questions and their corresponding SQL query in the prompt\n", - "- Using a vector database to include dynamic examples that are relevant to the specific user question\n", - "\n", - "All these examples involve customizing the chain's prompt. \n", - "\n", - "For example, we can include a few examples in our prompt like so:" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "from langchain.prompts import PromptTemplate\n", - "\n", - "TEMPLATE = \"\"\"Given an input question, first create a syntactically correct {dialect} query to run, then look at the results of the query and return the answer.\n", - "Use the following format:\n", - "\n", - "Question: \"Question here\"\n", - "SQLQuery: \"SQL Query to run\"\n", - "SQLResult: \"Result of the SQLQuery\"\n", - "Answer: \"Final answer here\"\n", - "\n", - "Only use the following tables:\n", - "\n", - "{table_info}.\n", - "\n", - "Some examples of SQL queries that correspond to questions are:\n", - "\n", - "{few_shot_examples}\n", - "\n", - "Question: {input}\"\"\"\n", - "\n", - "CUSTOM_PROMPT = PromptTemplate(\n", - " input_variables=[\"input\", \"few_shot_examples\", \"table_info\", \"dialect\"],\n", - " template=TEMPLATE,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can also access this [prompt](https://smith.langchain.com/hub/rlm/text-to-sql) in the LangChain prompt hub.\n", - "\n", - "This will work with your [LangSmith API key](https://docs.smith.langchain.com/)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from langchain import hub\n", - "\n", - "CUSTOM_PROMPT = hub.pull(\"rlm/text-to-sql\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Case 2: Text-to-SQL query and execution\n", - "\n", - "We can use `SQLDatabaseChain` from `langchain_experimental` to create and run SQL queries." - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [], - "source": [ - "from langchain_experimental.sql import SQLDatabaseChain\n", - "from langchain_openai import OpenAI\n", - "\n", - "llm = OpenAI(temperature=0, verbose=True)\n", - "db_chain = SQLDatabaseChain.from_llm(llm, db, verbose=True)" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "\n", - "\u001b[1m> Entering new SQLDatabaseChain chain...\u001b[0m\n", - "How many employees are there?\n", - "SQLQuery:\u001b[32;1m\u001b[1;3mSELECT COUNT(*) FROM \"Employee\";\u001b[0m\n", - "SQLResult: \u001b[33;1m\u001b[1;3m[(8,)]\u001b[0m\n", - "Answer:\u001b[32;1m\u001b[1;3mThere are 8 employees.\u001b[0m\n", - "\u001b[1m> Finished chain.\u001b[0m\n" - ] - }, - { - "data": { - "text/plain": [ - "'There are 8 employees.'" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "db_chain.run(\"How many employees are there?\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "As we can see, we get the same result as the previous case.\n", - "\n", - "Here, the chain **also handles the query execution** and provides a final answer based on the user question and the query result.\n", - "\n", - "**Be careful** while using this approach as it is susceptible to `SQL Injection`:\n", - "\n", - "* The chain is executing queries that are created by an LLM, and weren't validated\n", - "* e.g. records may be created, modified or deleted unintentionally_\n", - "\n", - "This is why we see the `SQLDatabaseChain` is inside `langchain_experimental`." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Go deeper\n", - "\n", - "**Looking under the hood**\n", - "\n", - "We can use the [LangSmith trace](https://smith.langchain.com/public/7f202a0c-1e35-42d6-a84a-6c2a58f697ef/r) to see what is happening under the hood:\n", - "\n", - "* As discussed above, first we create the query:\n", - "\n", - "```\n", - "text: ' SELECT COUNT(*) FROM \"Employee\";'\n", - "```\n", - "\n", - "* Then, it executes the query and passes the results to an LLM for synthesis.\n", - "\n", - "![sql_usecase.png](../../../static/img/sqldbchain_trace.png)\n", - "\n", - "\n", - "**Adding Sample Rows**\n", - "\n", - "Providing sample data can help the LLM construct correct queries when the data format is not obvious. \n", - "\n", - "For example, we can tell LLM that artists are saved with their full names by providing two rows from the Track table.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [], - "source": [ - "db = SQLDatabase.from_uri(\n", - " \"sqlite:///Chinook.db\",\n", - " include_tables=[\n", - " \"Track\"\n", - " ], # we include only one table to save tokens in the prompt :)\n", - " sample_rows_in_table_info=2,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The sample rows are added to the prompt after each corresponding table's column information.\n", - "\n", - "We can use `db.table_info` and check which sample rows are included:" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "CREATE TABLE \"Track\" (\n", - "\t\"TrackId\" INTEGER NOT NULL, \n", - "\t\"Name\" NVARCHAR(200) NOT NULL, \n", - "\t\"AlbumId\" INTEGER, \n", - "\t\"MediaTypeId\" INTEGER NOT NULL, \n", - "\t\"GenreId\" INTEGER, \n", - "\t\"Composer\" NVARCHAR(220), \n", - "\t\"Milliseconds\" INTEGER NOT NULL, \n", - "\t\"Bytes\" INTEGER, \n", - "\t\"UnitPrice\" NUMERIC(10, 2) NOT NULL, \n", - "\tPRIMARY KEY (\"TrackId\"), \n", - "\tFOREIGN KEY(\"MediaTypeId\") REFERENCES \"MediaType\" (\"MediaTypeId\"), \n", - "\tFOREIGN KEY(\"GenreId\") REFERENCES \"Genre\" (\"GenreId\"), \n", - "\tFOREIGN KEY(\"AlbumId\") REFERENCES \"Album\" (\"AlbumId\")\n", - ")\n", - "\n", - "/*\n", - "2 rows from Track table:\n", - "TrackId\tName\tAlbumId\tMediaTypeId\tGenreId\tComposer\tMilliseconds\tBytes\tUnitPrice\n", - "1\tFor Those About To Rock (We Salute You)\t1\t1\t1\tAngus Young, Malcolm Young, Brian Johnson\t343719\t11170334\t0.99\n", - "2\tBalls to the Wall\t2\t2\t1\tNone\t342562\t5510424\t0.99\n", - "*/\n" - ] - } - ], - "source": [ - "print(db.table_info)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Case 3: SQL agents\n", - "\n", - "LangChain has an SQL Agent which provides a more flexible way of interacting with SQL Databases than the `SQLDatabaseChain`.\n", - "\n", - "The main advantages of using the SQL Agent are:\n", - "\n", - "- It can answer questions based on the databases' schema as well as on the databases' content (like describing a specific table)\n", - "- It can recover from errors by running a generated query, catching the traceback and regenerating it correctly\n", - "\n", - "To initialize the agent, we use `create_sql_agent` function. \n", - "\n", - "This agent contains the `SQLDatabaseToolkit` which contains tools to: \n", - "\n", - "* Create and execute queries\n", - "* Check query syntax\n", - "* Retrieve table descriptions\n", - "* ... and more" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [], - "source": [ - "from langchain.agents import create_sql_agent\n", - "\n", - "# from langchain.agents import AgentExecutor\n", - "from langchain.agents.agent_types import AgentType\n", - "from langchain_community.agent_toolkits import SQLDatabaseToolkit\n", - "\n", - "db = SQLDatabase.from_uri(\"sqlite:///Chinook.db\")\n", - "\n", - "agent_executor = create_sql_agent(\n", - " llm=OpenAI(temperature=0),\n", - " toolkit=SQLDatabaseToolkit(db=db, llm=OpenAI(temperature=0)),\n", - " verbose=True,\n", - " agent_type=AgentType.ZERO_SHOT_REACT_DESCRIPTION,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Agent task example #1 - Running queries\n" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "\n", - "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", - "\u001b[32;1m\u001b[1;3mAction: sql_db_list_tables\n", - "Action Input: \u001b[0m\n", - "Observation: \u001b[38;5;200m\u001b[1;3mAlbum, Artist, Customer, Employee, Genre, Invoice, InvoiceLine, MediaType, Playlist, PlaylistTrack, Track\u001b[0m\n", - "Thought:\u001b[32;1m\u001b[1;3m I should query the schema of the Invoice and Customer tables.\n", - "Action: sql_db_schema\n", - "Action Input: Invoice, Customer\u001b[0m\n", - "Observation: \u001b[33;1m\u001b[1;3m\n", - "CREATE TABLE \"Customer\" (\n", - "\t\"CustomerId\" INTEGER NOT NULL, \n", - "\t\"FirstName\" NVARCHAR(40) NOT NULL, \n", - "\t\"LastName\" NVARCHAR(20) NOT NULL, \n", - "\t\"Company\" NVARCHAR(80), \n", - "\t\"Address\" NVARCHAR(70), \n", - "\t\"City\" NVARCHAR(40), \n", - "\t\"State\" NVARCHAR(40), \n", - "\t\"Country\" NVARCHAR(40), \n", - "\t\"PostalCode\" NVARCHAR(10), \n", - "\t\"Phone\" NVARCHAR(24), \n", - "\t\"Fax\" NVARCHAR(24), \n", - "\t\"Email\" NVARCHAR(60) NOT NULL, \n", - "\t\"SupportRepId\" INTEGER, \n", - "\tPRIMARY KEY (\"CustomerId\"), \n", - "\tFOREIGN KEY(\"SupportRepId\") REFERENCES \"Employee\" (\"EmployeeId\")\n", - ")\n", - "\n", - "/*\n", - "3 rows from Customer table:\n", - "CustomerId\tFirstName\tLastName\tCompany\tAddress\tCity\tState\tCountry\tPostalCode\tPhone\tFax\tEmail\tSupportRepId\n", - "1\tLuís\tGonçalves\tEmbraer - Empresa Brasileira de Aeronáutica S.A.\tAv. Brigadeiro Faria Lima, 2170\tSão José dos Campos\tSP\tBrazil\t12227-000\t+55 (12) 3923-5555\t+55 (12) 3923-5566\tluisg@embraer.com.br\t3\n", - "2\tLeonie\tKöhler\tNone\tTheodor-Heuss-Straße 34\tStuttgart\tNone\tGermany\t70174\t+49 0711 2842222\tNone\tleonekohler@surfeu.de\t5\n", - "3\tFrançois\tTremblay\tNone\t1498 rue Bélanger\tMontréal\tQC\tCanada\tH2G 1A7\t+1 (514) 721-4711\tNone\tftremblay@gmail.com\t3\n", - "*/\n", - "\n", - "\n", - "CREATE TABLE \"Invoice\" (\n", - "\t\"InvoiceId\" INTEGER NOT NULL, \n", - "\t\"CustomerId\" INTEGER NOT NULL, \n", - "\t\"InvoiceDate\" DATETIME NOT NULL, \n", - "\t\"BillingAddress\" NVARCHAR(70), \n", - "\t\"BillingCity\" NVARCHAR(40), \n", - "\t\"BillingState\" NVARCHAR(40), \n", - "\t\"BillingCountry\" NVARCHAR(40), \n", - "\t\"BillingPostalCode\" NVARCHAR(10), \n", - "\t\"Total\" NUMERIC(10, 2) NOT NULL, \n", - "\tPRIMARY KEY (\"InvoiceId\"), \n", - "\tFOREIGN KEY(\"CustomerId\") REFERENCES \"Customer\" (\"CustomerId\")\n", - ")\n", - "\n", - "/*\n", - "3 rows from Invoice table:\n", - "InvoiceId\tCustomerId\tInvoiceDate\tBillingAddress\tBillingCity\tBillingState\tBillingCountry\tBillingPostalCode\tTotal\n", - "1\t2\t2009-01-01 00:00:00\tTheodor-Heuss-Straße 34\tStuttgart\tNone\tGermany\t70174\t1.98\n", - "2\t4\t2009-01-02 00:00:00\tUllevålsveien 14\tOslo\tNone\tNorway\t0171\t3.96\n", - "3\t8\t2009-01-03 00:00:00\tGrétrystraat 63\tBrussels\tNone\tBelgium\t1000\t5.94\n", - "*/\u001b[0m\n", - "Thought:\u001b[32;1m\u001b[1;3m I should query the total sales per country.\n", - "Action: sql_db_query\n", - "Action Input: SELECT Country, SUM(Total) AS TotalSales FROM Invoice INNER JOIN Customer ON Invoice.CustomerId = Customer.CustomerId GROUP BY Country ORDER BY TotalSales DESC LIMIT 10\u001b[0m\n", - "Observation: \u001b[36;1m\u001b[1;3m[('USA', 523.0600000000003), ('Canada', 303.9599999999999), ('France', 195.09999999999994), ('Brazil', 190.09999999999997), ('Germany', 156.48), ('United Kingdom', 112.85999999999999), ('Czech Republic', 90.24000000000001), ('Portugal', 77.23999999999998), ('India', 75.25999999999999), ('Chile', 46.62)]\u001b[0m\n", - "Thought:\u001b[32;1m\u001b[1;3m I now know the final answer\n", - "Final Answer: The country with the highest total sales is the USA, with a total of $523.06.\u001b[0m\n", - "\n", - "\u001b[1m> Finished chain.\u001b[0m\n" - ] - }, - { - "data": { - "text/plain": [ - "'The country with the highest total sales is the USA, with a total of $523.06.'" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "agent_executor.run(\n", - " \"List the total sales per country. Which country's customers spent the most?\"\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Looking at the [LangSmith trace](https://smith.langchain.com/public/a86dbe17-5782-4020-bce6-2de85343168a/r), we can see:\n", - "\n", - "* The agent is using a ReAct style prompt\n", - "* First, it will look at the tables: `Action: sql_db_list_tables` using tool `sql_db_list_tables`\n", - "* Given the tables as an observation, it `thinks` and then determinates the next `action`:\n", - "\n", - "```\n", - "Observation: Album, Artist, Customer, Employee, Genre, Invoice, InvoiceLine, MediaType, Playlist, PlaylistTrack, Track\n", - "Thought: I should query the schema of the Invoice and Customer tables.\n", - "Action: sql_db_schema\n", - "Action Input: Invoice, Customer\n", - "```\n", - "\n", - "* It then formulates the query using the schema from tool `sql_db_schema`\n", - "\n", - "```\n", - "Thought: I should query the total sales per country.\n", - "Action: sql_db_query\n", - "Action Input: SELECT Country, SUM(Total) AS TotalSales FROM Invoice INNER JOIN Customer ON Invoice.CustomerId = Customer.CustomerId GROUP BY Country ORDER BY TotalSales DESC LIMIT 10\n", - "```\n", - "\n", - "* It finally executes the generated query using tool `sql_db_query`\n", - "\n", - "![sql_usecase.png](../../../static/img/SQLDatabaseToolkit.png)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Agent task example #2 - Describing a Table" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "\n", - "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", - "\u001b[32;1m\u001b[1;3mAction: sql_db_list_tables\n", - "Action Input: \u001b[0m\n", - "Observation: \u001b[38;5;200m\u001b[1;3mAlbum, Artist, Customer, Employee, Genre, Invoice, InvoiceLine, MediaType, Playlist, PlaylistTrack, Track\u001b[0m\n", - "Thought:\u001b[32;1m\u001b[1;3m The PlaylistTrack table is the most relevant to the question.\n", - "Action: sql_db_schema\n", - "Action Input: PlaylistTrack\u001b[0m\n", - "Observation: \u001b[33;1m\u001b[1;3m\n", - "CREATE TABLE \"PlaylistTrack\" (\n", - "\t\"PlaylistId\" INTEGER NOT NULL, \n", - "\t\"TrackId\" INTEGER NOT NULL, \n", - "\tPRIMARY KEY (\"PlaylistId\", \"TrackId\"), \n", - "\tFOREIGN KEY(\"TrackId\") REFERENCES \"Track\" (\"TrackId\"), \n", - "\tFOREIGN KEY(\"PlaylistId\") REFERENCES \"Playlist\" (\"PlaylistId\")\n", - ")\n", - "\n", - "/*\n", - "3 rows from PlaylistTrack table:\n", - "PlaylistId\tTrackId\n", - "1\t3402\n", - "1\t3389\n", - "1\t3390\n", - "*/\u001b[0m\n", - "Thought:\u001b[32;1m\u001b[1;3m I now know the final answer\n", - "Final Answer: The PlaylistTrack table contains two columns, PlaylistId and TrackId, which are both integers and form a primary key. It also has two foreign keys, one to the Track table and one to the Playlist table.\u001b[0m\n", - "\n", - "\u001b[1m> Finished chain.\u001b[0m\n" - ] - }, - { - "data": { - "text/plain": [ - "'The PlaylistTrack table contains two columns, PlaylistId and TrackId, which are both integers and form a primary key. It also has two foreign keys, one to the Track table and one to the Playlist table.'" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "agent_executor.run(\"Describe the playlisttrack table\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Extending the SQL Toolkit\n", - "\n", - "Although the out-of-the-box SQL Toolkit contains the necessary tools to start working on a database, it is often the case that some extra tools may be useful for extending the agent's capabilities. This is particularly useful when trying to use **domain specific knowledge** in the solution, in order to improve its overall performance.\n", - "\n", - "Some examples include:\n", - "\n", - "- Including dynamic few shot examples\n", - "- Finding misspellings in proper nouns to use as column filters\n", - "\n", - "We can create separate tools which tackle these specific use cases and include them as a complement to the standard SQL Toolkit. Let's see how to include these two custom tools.\n", - "\n", - "#### Including dynamic few-shot examples\n", - "\n", - "In order to include dynamic few-shot examples, we need a custom **Retriever Tool** that handles the vector database in order to retrieve the examples that are semantically similar to the user’s question.\n", - "\n", - "Let's start by creating a dictionary with some examples: " - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "few_shots = {\n", - " \"List all artists.\": \"SELECT * FROM artists;\",\n", - " \"Find all albums for the artist 'AC/DC'.\": \"SELECT * FROM albums WHERE ArtistId = (SELECT ArtistId FROM artists WHERE Name = 'AC/DC');\",\n", - " \"List all tracks in the 'Rock' genre.\": \"SELECT * FROM tracks WHERE GenreId = (SELECT GenreId FROM genres WHERE Name = 'Rock');\",\n", - " \"Find the total duration of all tracks.\": \"SELECT SUM(Milliseconds) FROM tracks;\",\n", - " \"List all customers from Canada.\": \"SELECT * FROM customers WHERE Country = 'Canada';\",\n", - " \"How many tracks are there in the album with ID 5?\": \"SELECT COUNT(*) FROM tracks WHERE AlbumId = 5;\",\n", - " \"Find the total number of invoices.\": \"SELECT COUNT(*) FROM invoices;\",\n", - " \"List all tracks that are longer than 5 minutes.\": \"SELECT * FROM tracks WHERE Milliseconds > 300000;\",\n", - " \"Who are the top 5 customers by total purchase?\": \"SELECT CustomerId, SUM(Total) AS TotalPurchase FROM invoices GROUP BY CustomerId ORDER BY TotalPurchase DESC LIMIT 5;\",\n", - " \"Which albums are from the year 2000?\": \"SELECT * FROM albums WHERE strftime('%Y', ReleaseDate) = '2000';\",\n", - " \"How many employees are there\": 'SELECT COUNT(*) FROM \"employee\"',\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can then create a retriever using the list of questions, assigning the target SQL query as metadata:" - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "metadata": {}, - "outputs": [], - "source": [ - "from langchain.schema import Document\n", - "from langchain_community.vectorstores import FAISS\n", - "from langchain_openai import OpenAIEmbeddings\n", - "\n", - "embeddings = OpenAIEmbeddings()\n", - "\n", - "few_shot_docs = [\n", - " Document(page_content=question, metadata={\"sql_query\": few_shots[question]})\n", - " for question in few_shots.keys()\n", - "]\n", - "vector_db = FAISS.from_documents(few_shot_docs, embeddings)\n", - "retriever = vector_db.as_retriever()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now we can create our own custom tool and append it as a new tool in the `create_sql_agent` function:" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [], - "source": [ - "from langchain_community.agent_toolkits import create_retriever_tool\n", - "\n", - "tool_description = \"\"\"\n", - "This tool will help you understand similar examples to adapt them to the user question.\n", - "Input to this tool should be the user question.\n", - "\"\"\"\n", - "\n", - "retriever_tool = create_retriever_tool(\n", - " retriever, name=\"sql_get_similar_examples\", description=tool_description\n", - ")\n", - "custom_tool_list = [retriever_tool]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now we can create the agent, adjusting the standard SQL Agent suffix to consider our use case. Although the most straightforward way to handle this would be to include it just in the tool description, this is often not enough and we need to specify it in the agent prompt using the `suffix` argument in the constructor." - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "metadata": {}, - "outputs": [], - "source": [ - "from langchain.agents import AgentType, create_sql_agent\n", - "from langchain_community.agent_toolkits import SQLDatabaseToolkit\n", - "from langchain_community.utilities import SQLDatabase\n", - "from langchain_openai import ChatOpenAI\n", - "\n", - "db = SQLDatabase.from_uri(\"sqlite:///Chinook.db\")\n", - "llm = ChatOpenAI(model_name=\"gpt-4\", temperature=0)\n", - "\n", - "toolkit = SQLDatabaseToolkit(db=db, llm=llm)\n", - "\n", - "custom_suffix = \"\"\"\n", - "I should first get the similar examples I know.\n", - "If the examples are enough to construct the query, I can build it.\n", - "Otherwise, I can then look at the tables in the database to see what I can query.\n", - "Then I should query the schema of the most relevant tables\n", - "\"\"\"\n", - "\n", - "agent = create_sql_agent(\n", - " llm=llm,\n", - " toolkit=toolkit,\n", - " verbose=True,\n", - " agent_type=AgentType.OPENAI_FUNCTIONS,\n", - " extra_tools=custom_tool_list,\n", - " suffix=custom_suffix,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's try it out:" - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "\n", - "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", - "\u001b[32;1m\u001b[1;3m\n", - "Invoking: `sql_get_similar_examples` with `How many employees do we have?`\n", - "\n", - "\n", - "\u001b[0m\u001b[33;1m\u001b[1;3m[Document(page_content='How many employees are there', metadata={'sql_query': 'SELECT COUNT(*) FROM \"employee\"'}), Document(page_content='Find the total number of invoices.', metadata={'sql_query': 'SELECT COUNT(*) FROM invoices;'})]\u001b[0m\u001b[32;1m\u001b[1;3m\n", - "Invoking: `sql_db_query_checker` with `SELECT COUNT(*) FROM employee`\n", - "responded: {content}\n", - "\n", - "\u001b[0m\u001b[36;1m\u001b[1;3mSELECT COUNT(*) FROM employee\u001b[0m\u001b[32;1m\u001b[1;3m\n", - "Invoking: `sql_db_query` with `SELECT COUNT(*) FROM employee`\n", - "\n", - "\n", - "\u001b[0m\u001b[36;1m\u001b[1;3m[(8,)]\u001b[0m\u001b[32;1m\u001b[1;3mWe have 8 employees.\u001b[0m\n", - "\n", - "\u001b[1m> Finished chain.\u001b[0m\n" - ] - }, - { - "data": { - "text/plain": [ - "'We have 8 employees.'" - ] - }, - "execution_count": 23, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "agent.run(\"How many employees do we have?\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "As we can see, the agent first used the `sql_get_similar_examples` tool in order to retrieve similar examples. As the question was very similar to other few shot examples, the agent **didn't need to use any other tool** from the standard Toolkit, thus **saving time and tokens**." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Finding and correcting misspellings for proper nouns\n", - "\n", - "In order to filter columns that contain proper nouns such as addresses, song names or artists, we first need to double-check the spelling in order to filter the data correctly. \n", - "\n", - "We can achieve this by creating a vector store using all the distinct proper nouns that exist in the database. We can then have the agent query that vector store each time the user includes a proper noun in their question, to find the correct spelling for that word. In this way, the agent can make sure it understands which entity the user is referring to before building the target query.\n", - "\n", - "Let's follow a similar approach to the few shots, but without metadata: just embedding the proper nouns and then querying to get the most similar one to the misspelled user question.\n", - "\n", - "First we need the unique values for each entity we want, for which we define a function that parses the result into a list of elements:" - ] - }, - { - "cell_type": "code", - "execution_count": 37, - "metadata": {}, - "outputs": [], - "source": [ - "import ast\n", - "import re\n", - "\n", - "\n", - "def run_query_save_results(db, query):\n", - " res = db.run(query)\n", - " res = [el for sub in ast.literal_eval(res) for el in sub if el]\n", - " res = [re.sub(r\"\\b\\d+\\b\", \"\", string).strip() for string in res]\n", - " return res\n", - "\n", - "\n", - "artists = run_query_save_results(db, \"SELECT Name FROM Artist\")\n", - "albums = run_query_save_results(db, \"SELECT Title FROM Album\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now we can proceed with creating the custom **retriever tool** and the final agent:" - ] - }, - { - "cell_type": "code", - "execution_count": 51, - "metadata": {}, - "outputs": [], - "source": [ - "from langchain_community.agent_toolkits import create_retriever_tool\n", - "from langchain_community.vectorstores import FAISS\n", - "from langchain_openai import OpenAIEmbeddings\n", - "\n", - "texts = artists + albums\n", - "\n", - "embeddings = OpenAIEmbeddings()\n", - "vector_db = FAISS.from_texts(texts, embeddings)\n", - "retriever = vector_db.as_retriever()\n", - "\n", - "retriever_tool = create_retriever_tool(\n", - " retriever,\n", - " name=\"name_search\",\n", - " description=\"use to learn how a piece of data is actually written, can be from names, surnames addresses etc\",\n", - ")\n", - "\n", - "custom_tool_list = [retriever_tool]" - ] - }, - { - "cell_type": "code", - "execution_count": 54, - "metadata": {}, - "outputs": [], - "source": [ - "from langchain.agents import AgentType, create_sql_agent\n", - "from langchain_community.agent_toolkits import SQLDatabaseToolkit\n", - "from langchain_community.utilities import SQLDatabase\n", - "from langchain_openai import ChatOpenAI\n", - "\n", - "# db = SQLDatabase.from_uri(\"sqlite:///Chinook.db\")\n", - "llm = ChatOpenAI(model_name=\"gpt-4\", temperature=0)\n", - "\n", - "toolkit = SQLDatabaseToolkit(db=db, llm=llm)\n", - "\n", - "custom_suffix = \"\"\"\n", - "If a user asks for me to filter based on proper nouns, I should first check the spelling using the name_search tool.\n", - "Otherwise, I can then look at the tables in the database to see what I can query.\n", - "Then I should query the schema of the most relevant tables\n", - "\"\"\"\n", - "\n", - "agent = create_sql_agent(\n", - " llm=llm,\n", - " toolkit=toolkit,\n", - " verbose=True,\n", - " agent_type=AgentType.OPENAI_FUNCTIONS,\n", - " extra_tools=custom_tool_list,\n", - " suffix=custom_suffix,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's try it out:" - ] - }, - { - "cell_type": "code", - "execution_count": 55, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "\n", - "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", - "\u001b[32;1m\u001b[1;3m\n", - "Invoking: `name_search` with `alis in pains`\n", - "\n", - "\n", - "\u001b[0m\u001b[33;1m\u001b[1;3m[Document(page_content='House of Pain', metadata={}), Document(page_content='Alice In Chains', metadata={}), Document(page_content='Aisha Duo', metadata={}), Document(page_content='House Of Pain', metadata={})]\u001b[0m\u001b[32;1m\u001b[1;3m\n", - "Invoking: `sql_db_list_tables` with ``\n", - "responded: {content}\n", - "\n", - "\u001b[0m\u001b[38;5;200m\u001b[1;3mAlbum, Artist, Customer, Employee, Genre, Invoice, InvoiceLine, MediaType, Playlist, PlaylistTrack, Track\u001b[0m\u001b[32;1m\u001b[1;3m\n", - "Invoking: `sql_db_schema` with `Album, Artist`\n", - "responded: {content}\n", - "\n", - "\u001b[0m\u001b[33;1m\u001b[1;3m\n", - "CREATE TABLE \"Album\" (\n", - "\t\"AlbumId\" INTEGER NOT NULL, \n", - "\t\"Title\" NVARCHAR(160) NOT NULL, \n", - "\t\"ArtistId\" INTEGER NOT NULL, \n", - "\tPRIMARY KEY (\"AlbumId\"), \n", - "\tFOREIGN KEY(\"ArtistId\") REFERENCES \"Artist\" (\"ArtistId\")\n", - ")\n", - "\n", - "/*\n", - "3 rows from Album table:\n", - "AlbumId\tTitle\tArtistId\n", - "1\tFor Those About To Rock We Salute You\t1\n", - "2\tBalls to the Wall\t2\n", - "3\tRestless and Wild\t2\n", - "*/\n", - "\n", - "\n", - "CREATE TABLE \"Artist\" (\n", - "\t\"ArtistId\" INTEGER NOT NULL, \n", - "\t\"Name\" NVARCHAR(120), \n", - "\tPRIMARY KEY (\"ArtistId\")\n", - ")\n", - "\n", - "/*\n", - "3 rows from Artist table:\n", - "ArtistId\tName\n", - "1\tAC/DC\n", - "2\tAccept\n", - "3\tAerosmith\n", - "*/\u001b[0m\u001b[32;1m\u001b[1;3m\n", - "Invoking: `sql_db_query_checker` with `SELECT COUNT(*) FROM Album JOIN Artist ON Album.ArtistId = Artist.ArtistId WHERE Artist.Name = 'Alice In Chains'`\n", - "responded: {content}\n", - "\n", - "\u001b[0m\u001b[36;1m\u001b[1;3mSELECT COUNT(*) FROM Album JOIN Artist ON Album.ArtistId = Artist.ArtistId WHERE Artist.Name = 'Alice In Chains'\u001b[0m\u001b[32;1m\u001b[1;3m\n", - "Invoking: `sql_db_query` with `SELECT COUNT(*) FROM Album JOIN Artist ON Album.ArtistId = Artist.ArtistId WHERE Artist.Name = 'Alice In Chains'`\n", - "\n", - "\n", - "\u001b[0m\u001b[36;1m\u001b[1;3m[(1,)]\u001b[0m\u001b[32;1m\u001b[1;3mAlice In Chains has 1 album in the database.\u001b[0m\n", - "\n", - "\u001b[1m> Finished chain.\u001b[0m\n" - ] - }, - { - "data": { - "text/plain": [ - "'Alice In Chains has 1 album in the database.'" - ] - }, - "execution_count": 55, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "agent.run(\"How many albums does alis in pains have?\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "As we can see, the agent used the `name_search` tool in order to check how to correctly query the database for this specific artist." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Go deeper\n", - "\n", - "To learn more about the SQL Agent and how it works we refer to the [SQL Agent Toolkit](/docs/integrations/toolkits/sql_database) documentation.\n", - "\n", - "You can also check Agents for other document types:\n", - "- [Pandas Agent](/docs/integrations/toolkits/pandas)\n", - "- [CSV Agent](/docs/integrations/toolkits/csv)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Elastic Search\n", - "\n", - "Going beyond the above use-case, there are integrations with other databases.\n", - "\n", - "For example, we can interact with Elasticsearch analytics database. \n", - "\n", - "This chain builds search queries via the Elasticsearch DSL API (filters and aggregations).\n", - "\n", - "The Elasticsearch client must have permissions for index listing, mapping description and search queries.\n", - "\n", - "See [here](https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html) for instructions on how to run Elasticsearch locally.\n", - "\n", - "Make sure to install the Elasticsearch Python client before:\n", - "\n", - "```sh\n", - "pip install elasticsearch\n", - "```" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [], - "source": [ - "from elasticsearch import Elasticsearch\n", - "from langchain.chains.elasticsearch_database import ElasticsearchDatabaseChain\n", - "from langchain_openai import ChatOpenAI" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Initialize Elasticsearch python client.\n", - "# See https://elasticsearch-py.readthedocs.io/en/v8.8.2/api.html#elasticsearch.Elasticsearch\n", - "ELASTIC_SEARCH_SERVER = \"https://elastic:pass@localhost:9200\"\n", - "db = Elasticsearch(ELASTIC_SEARCH_SERVER)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Uncomment the next cell to initially populate your db." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# customers = [\n", - "# {\"firstname\": \"Jennifer\", \"lastname\": \"Walters\"},\n", - "# {\"firstname\": \"Monica\",\"lastname\":\"Rambeau\"},\n", - "# {\"firstname\": \"Carol\",\"lastname\":\"Danvers\"},\n", - "# {\"firstname\": \"Wanda\",\"lastname\":\"Maximoff\"},\n", - "# {\"firstname\": \"Jennifer\",\"lastname\":\"Takeda\"},\n", - "# ]\n", - "# for i, customer in enumerate(customers):\n", - "# db.create(index=\"customers\", document=customer, id=i)\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "llm = ChatOpenAI(model_name=\"gpt-4\", temperature=0)\n", - "chain = ElasticsearchDatabaseChain.from_llm(llm=llm, database=db, verbose=True)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "question = \"What are the first names of all the customers?\"\n", - "chain.run(question)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can customize the prompt." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from langchain.prompts.prompt import PromptTemplate\n", - "\n", - "PROMPT_TEMPLATE = \"\"\"Given an input question, create a syntactically correct Elasticsearch query to run. Unless the user specifies in their question a specific number of examples they wish to obtain, always limit your query to at most {top_k} results. You can order the results by a relevant column to return the most interesting examples in the database.\n", - "\n", - "Unless told to do not query for all the columns from a specific index, only ask for a the few relevant columns given the question.\n", - "\n", - "Pay attention to use only the column names that you can see in the mapping description. Be careful to not query for columns that do not exist. Also, pay attention to which column is in which index. Return the query as valid json.\n", - "\n", - "Use the following format:\n", - "\n", - "Question: Question here\n", - "ESQuery: Elasticsearch Query formatted as json\n", - "\"\"\"\n", - "\n", - "PROMPT = PromptTemplate.from_template(\n", - " PROMPT_TEMPLATE,\n", - ")\n", - "chain = ElasticsearchDatabaseChain.from_llm(llm=llm, database=db, query_prompt=PROMPT)" - ] - } - ], - "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.9.1" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/docs/docs/use_cases/question_answering/chat_history.ipynb b/docs/docs/use_cases/question_answering/chat_history.ipynb index ffae5465de0e7..0478fb846cb74 100644 --- a/docs/docs/use_cases/question_answering/chat_history.ipynb +++ b/docs/docs/use_cases/question_answering/chat_history.ipynb @@ -359,11 +359,20 @@ "Here we've gone over how to add application logic for incorporating historical outputs, but we're still manually updating the chat history and inserting it into each input. In a real Q&A application we'll want some way of persisting chat history and some way of automatically inserting and updating it.\n", "\n", "For this we can use:\n", + "\n", "- [BaseChatMessageHistory](/docs/modules/memory/chat_messages/): Store chat history.\n", "- [RunnableWithMessageHistory](/docs/expression_language/how_to/message_history): Wrapper for an LCEL chain and a `BaseChatMessageHistory` that handles injecting chat history into inputs and updating it after each invocation.\n", "\n", "For a detailed walkthrough of how to use these classes together to create a stateful conversational chain, head to the [How to add message history (memory)](/docs/expression_language/how_to/message_history) LCEL page." ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1f67a60a-0a31-4315-9cce-19c78d658f6a", + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { diff --git a/docs/docs/use_cases/question_answering/index.ipynb b/docs/docs/use_cases/question_answering/index.ipynb index 078b004c01aca..af23b8e3aa0ef 100644 --- a/docs/docs/use_cases/question_answering/index.ipynb +++ b/docs/docs/use_cases/question_answering/index.ipynb @@ -38,7 +38,7 @@ "\n", "**Note**: Here we focus on Q&A for unstructured data. Two RAG use cases which we cover elsewhere are:\n", "\n", - "- [Q&A over structured data](/docs/use_cases/qa_structured/sql) (e.g., SQL)\n", + "- [Q&A over SQL data](/docs/use_cases/sql/)\n", "- [Q&A over code](/docs/use_cases/code_understanding) (e.g., Python)" ] }, @@ -103,7 +103,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.1" + "version": "3.9.1" } }, "nbformat": 4, diff --git a/docs/docs/use_cases/question_answering/per_user.ipynb b/docs/docs/use_cases/question_answering/per_user.ipynb index 3724d006751ce..d1459a73919ba 100644 --- a/docs/docs/use_cases/question_answering/per_user.ipynb +++ b/docs/docs/use_cases/question_answering/per_user.ipynb @@ -298,6 +298,18 @@ " config={\"configurable\": {\"search_kwargs\": {\"namespace\": \"ankush\"}}},\n", ")" ] + }, + { + "cell_type": "markdown", + "source": [ + "For more vectorstore implementations for multi-user, please refer to specific pages, such as [Milvus](/docs/integrations/vectorstores/milvus)." + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%% md\n" + } + } } ], "metadata": { @@ -321,4 +333,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/docs/docs/use_cases/sql/agents.ipynb b/docs/docs/use_cases/sql/agents.ipynb new file mode 100644 index 0000000000000..aa6db0dd3f920 --- /dev/null +++ b/docs/docs/use_cases/sql/agents.ipynb @@ -0,0 +1,815 @@ +{ + "cells": [ + { + "cell_type": "raw", + "metadata": {}, + "source": [ + "---\n", + "sidebar_position: 1\n", + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Agents\n", + "\n", + "LangChain has a SQL Agent which provides a more flexible way of interacting with SQL Databases than a chain. The main advantages of using the SQL Agent are:\n", + "\n", + "- It can answer questions based on the databases' schema as well as on the databases' content (like describing a specific table).\n", + "- It can recover from errors by running a generated query, catching the traceback and regenerating it correctly.\n", + "- It can query the database as many times as needed to answer the user question.\n", + "\n", + "To initialize the agent we'll use the [create_sql_agent](https://api.python.langchain.com/en/latest/agent_toolkits/langchain_community.agent_toolkits.sql.base.create_sql_agent.html) constructor. This agent uses the `SQLDatabaseToolkit` which contains tools to: \n", + "\n", + "* Create and execute queries\n", + "* Check query syntax\n", + "* Retrieve table descriptions\n", + "* ... and more\n", + "\n", + "## Setup\n", + "\n", + "First, get required packages and set environment variables:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet langchain langchain-community langchain-openai" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We default to OpenAI models in this guide, but you can swap them out for the model provider of your choice." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import getpass\n", + "import os\n", + "\n", + "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass()\n", + "\n", + "# Uncomment the below to use LangSmith. Not required.\n", + "# os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass()\n", + "# os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The below example will use a SQLite connection with Chinook database. Follow [these installation steps](https://database.guide/2-sample-databases-sqlite/) to create `Chinook.db` in the same directory as this notebook:\n", + "\n", + "* Save [this file](https://raw.githubusercontent.com/lerocha/chinook-database/master/ChinookDatabase/DataSources/Chinook_Sqlite.sql) as `Chinook_Sqlite.sql`\n", + "* Run `sqlite3 Chinook.db`\n", + "* Run `.read Chinook_Sqlite.sql`\n", + "* Test `SELECT * FROM Artist LIMIT 10;`\n", + "\n", + "Now, `Chinhook.db` is in our directory and we can interface with it using the SQLAlchemy-driven `SQLDatabase` class:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "sqlite\n", + "['Album', 'Artist', 'Customer', 'Employee', 'Genre', 'Invoice', 'InvoiceLine', 'MediaType', 'Playlist', 'PlaylistTrack', 'Track']\n" + ] + }, + { + "data": { + "text/plain": [ + "\"[(1, 'AC/DC'), (2, 'Accept'), (3, 'Aerosmith'), (4, 'Alanis Morissette'), (5, 'Alice In Chains'), (6, 'Antônio Carlos Jobim'), (7, 'Apocalyptica'), (8, 'Audioslave'), (9, 'BackBeat'), (10, 'Billy Cobham')]\"" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from langchain_community.utilities import SQLDatabase\n", + "\n", + "db = SQLDatabase.from_uri(\"sqlite:///Chinook.db\")\n", + "print(db.dialect)\n", + "print(db.get_usable_table_names())\n", + "db.run(\"SELECT * FROM Artist LIMIT 10;\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Agent\n", + "\n", + "We'll use an OpenAI chat model and an `\"openai-tools\"` agent, which will use OpenAI's function-calling API to drive the agent's tool selection and invocations." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_community.agent_toolkits import create_sql_agent\n", + "from langchain_openai import ChatOpenAI\n", + "\n", + "llm = ChatOpenAI(model=\"gpt-3.5-turbo\", temperature=0)\n", + "agent_executor = create_sql_agent(llm, db=db, agent_type=\"openai-tools\", verbose=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3m\n", + "Invoking: `sql_db_list_tables` with `{}`\n", + "\n", + "\n", + "\u001b[0m\u001b[38;5;200m\u001b[1;3mAlbum, Artist, Customer, Employee, Genre, Invoice, InvoiceLine, MediaType, Playlist, PlaylistTrack, Track\u001b[0m\u001b[32;1m\u001b[1;3m\n", + "Invoking: `sql_db_schema` with `Invoice,Customer`\n", + "\n", + "\n", + "\u001b[0m\u001b[33;1m\u001b[1;3m\n", + "CREATE TABLE \"Customer\" (\n", + "\t\"CustomerId\" INTEGER NOT NULL, \n", + "\t\"FirstName\" NVARCHAR(40) NOT NULL, \n", + "\t\"LastName\" NVARCHAR(20) NOT NULL, \n", + "\t\"Company\" NVARCHAR(80), \n", + "\t\"Address\" NVARCHAR(70), \n", + "\t\"City\" NVARCHAR(40), \n", + "\t\"State\" NVARCHAR(40), \n", + "\t\"Country\" NVARCHAR(40), \n", + "\t\"PostalCode\" NVARCHAR(10), \n", + "\t\"Phone\" NVARCHAR(24), \n", + "\t\"Fax\" NVARCHAR(24), \n", + "\t\"Email\" NVARCHAR(60) NOT NULL, \n", + "\t\"SupportRepId\" INTEGER, \n", + "\tPRIMARY KEY (\"CustomerId\"), \n", + "\tFOREIGN KEY(\"SupportRepId\") REFERENCES \"Employee\" (\"EmployeeId\")\n", + ")\n", + "\n", + "/*\n", + "3 rows from Customer table:\n", + "CustomerId\tFirstName\tLastName\tCompany\tAddress\tCity\tState\tCountry\tPostalCode\tPhone\tFax\tEmail\tSupportRepId\n", + "1\tLuís\tGonçalves\tEmbraer - Empresa Brasileira de Aeronáutica S.A.\tAv. Brigadeiro Faria Lima, 2170\tSão José dos Campos\tSP\tBrazil\t12227-000\t+55 (12) 3923-5555\t+55 (12) 3923-5566\tluisg@embraer.com.br\t3\n", + "2\tLeonie\tKöhler\tNone\tTheodor-Heuss-Straße 34\tStuttgart\tNone\tGermany\t70174\t+49 0711 2842222\tNone\tleonekohler@surfeu.de\t5\n", + "3\tFrançois\tTremblay\tNone\t1498 rue Bélanger\tMontréal\tQC\tCanada\tH2G 1A7\t+1 (514) 721-4711\tNone\tftremblay@gmail.com\t3\n", + "*/\n", + "\n", + "\n", + "CREATE TABLE \"Invoice\" (\n", + "\t\"InvoiceId\" INTEGER NOT NULL, \n", + "\t\"CustomerId\" INTEGER NOT NULL, \n", + "\t\"InvoiceDate\" DATETIME NOT NULL, \n", + "\t\"BillingAddress\" NVARCHAR(70), \n", + "\t\"BillingCity\" NVARCHAR(40), \n", + "\t\"BillingState\" NVARCHAR(40), \n", + "\t\"BillingCountry\" NVARCHAR(40), \n", + "\t\"BillingPostalCode\" NVARCHAR(10), \n", + "\t\"Total\" NUMERIC(10, 2) NOT NULL, \n", + "\tPRIMARY KEY (\"InvoiceId\"), \n", + "\tFOREIGN KEY(\"CustomerId\") REFERENCES \"Customer\" (\"CustomerId\")\n", + ")\n", + "\n", + "/*\n", + "3 rows from Invoice table:\n", + "InvoiceId\tCustomerId\tInvoiceDate\tBillingAddress\tBillingCity\tBillingState\tBillingCountry\tBillingPostalCode\tTotal\n", + "1\t2\t2009-01-01 00:00:00\tTheodor-Heuss-Straße 34\tStuttgart\tNone\tGermany\t70174\t1.98\n", + "2\t4\t2009-01-02 00:00:00\tUllevålsveien 14\tOslo\tNone\tNorway\t0171\t3.96\n", + "3\t8\t2009-01-03 00:00:00\tGrétrystraat 63\tBrussels\tNone\tBelgium\t1000\t5.94\n", + "*/\u001b[0m\u001b[32;1m\u001b[1;3m\n", + "Invoking: `sql_db_query` with `SELECT c.Country, SUM(i.Total) AS TotalSales FROM Invoice i JOIN Customer c ON i.CustomerId = c.CustomerId GROUP BY c.Country ORDER BY TotalSales DESC`\n", + "responded: To list the total sales per country, I can query the \"Invoice\" and \"Customer\" tables. I will join these tables on the \"CustomerId\" column and group the results by the \"BillingCountry\" column. Then, I will calculate the sum of the \"Total\" column to get the total sales per country. Finally, I will order the results in descending order of the total sales.\n", + "\n", + "Here is the SQL query:\n", + "\n", + "```sql\n", + "SELECT c.Country, SUM(i.Total) AS TotalSales\n", + "FROM Invoice i\n", + "JOIN Customer c ON i.CustomerId = c.CustomerId\n", + "GROUP BY c.Country\n", + "ORDER BY TotalSales DESC\n", + "```\n", + "\n", + "Now, I will execute this query to get the results.\n", + "\n", + "\u001b[0m\u001b[36;1m\u001b[1;3m[('USA', 523.0600000000003), ('Canada', 303.9599999999999), ('France', 195.09999999999994), ('Brazil', 190.09999999999997), ('Germany', 156.48), ('United Kingdom', 112.85999999999999), ('Czech Republic', 90.24000000000001), ('Portugal', 77.23999999999998), ('India', 75.25999999999999), ('Chile', 46.62), ('Ireland', 45.62), ('Hungary', 45.62), ('Austria', 42.62), ('Finland', 41.620000000000005), ('Netherlands', 40.62), ('Norway', 39.62), ('Sweden', 38.620000000000005), ('Poland', 37.620000000000005), ('Italy', 37.620000000000005), ('Denmark', 37.620000000000005), ('Australia', 37.620000000000005), ('Argentina', 37.620000000000005), ('Spain', 37.62), ('Belgium', 37.62)]\u001b[0m\u001b[32;1m\u001b[1;3mThe total sales per country are as follows:\n", + "\n", + "1. USA: $523.06\n", + "2. Canada: $303.96\n", + "3. France: $195.10\n", + "4. Brazil: $190.10\n", + "5. Germany: $156.48\n", + "6. United Kingdom: $112.86\n", + "7. Czech Republic: $90.24\n", + "8. Portugal: $77.24\n", + "9. India: $75.26\n", + "10. Chile: $46.62\n", + "\n", + "The country whose customers spent the most is the USA, with a total sales of $523.06.\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "{'input': \"List the total sales per country. Which country's customers spent the most?\",\n", + " 'output': 'The total sales per country are as follows:\\n\\n1. USA: $523.06\\n2. Canada: $303.96\\n3. France: $195.10\\n4. Brazil: $190.10\\n5. Germany: $156.48\\n6. United Kingdom: $112.86\\n7. Czech Republic: $90.24\\n8. Portugal: $77.24\\n9. India: $75.26\\n10. Chile: $46.62\\n\\nThe country whose customers spent the most is the USA, with a total sales of $523.06.'}" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "agent_executor.invoke(\n", + " \"List the total sales per country. Which country's customers spent the most?\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3m\n", + "Invoking: `sql_db_list_tables` with `{}`\n", + "\n", + "\n", + "\u001b[0m\u001b[38;5;200m\u001b[1;3mAlbum, Artist, Customer, Employee, Genre, Invoice, InvoiceLine, MediaType, Playlist, PlaylistTrack, Track\u001b[0m\u001b[32;1m\u001b[1;3m\n", + "Invoking: `sql_db_schema` with `PlaylistTrack`\n", + "\n", + "\n", + "\u001b[0m\u001b[33;1m\u001b[1;3m\n", + "CREATE TABLE \"PlaylistTrack\" (\n", + "\t\"PlaylistId\" INTEGER NOT NULL, \n", + "\t\"TrackId\" INTEGER NOT NULL, \n", + "\tPRIMARY KEY (\"PlaylistId\", \"TrackId\"), \n", + "\tFOREIGN KEY(\"TrackId\") REFERENCES \"Track\" (\"TrackId\"), \n", + "\tFOREIGN KEY(\"PlaylistId\") REFERENCES \"Playlist\" (\"PlaylistId\")\n", + ")\n", + "\n", + "/*\n", + "3 rows from PlaylistTrack table:\n", + "PlaylistId\tTrackId\n", + "1\t3402\n", + "1\t3389\n", + "1\t3390\n", + "*/\u001b[0m\u001b[32;1m\u001b[1;3mThe `PlaylistTrack` table has two columns: `PlaylistId` and `TrackId`. It is a junction table that represents the many-to-many relationship between playlists and tracks. \n", + "\n", + "Here is the schema of the `PlaylistTrack` table:\n", + "\n", + "```\n", + "CREATE TABLE \"PlaylistTrack\" (\n", + "\t\"PlaylistId\" INTEGER NOT NULL, \n", + "\t\"TrackId\" INTEGER NOT NULL, \n", + "\tPRIMARY KEY (\"PlaylistId\", \"TrackId\"), \n", + "\tFOREIGN KEY(\"TrackId\") REFERENCES \"Track\" (\"TrackId\"), \n", + "\tFOREIGN KEY(\"PlaylistId\") REFERENCES \"Playlist\" (\"PlaylistId\")\n", + ")\n", + "```\n", + "\n", + "The `PlaylistId` column is a foreign key referencing the `PlaylistId` column in the `Playlist` table. The `TrackId` column is a foreign key referencing the `TrackId` column in the `Track` table.\n", + "\n", + "Here are three sample rows from the `PlaylistTrack` table:\n", + "\n", + "```\n", + "PlaylistId TrackId\n", + "1 3402\n", + "1 3389\n", + "1 3390\n", + "```\n", + "\n", + "Please let me know if there is anything else I can help with.\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "{'input': 'Describe the playlisttrack table',\n", + " 'output': 'The `PlaylistTrack` table has two columns: `PlaylistId` and `TrackId`. It is a junction table that represents the many-to-many relationship between playlists and tracks. \\n\\nHere is the schema of the `PlaylistTrack` table:\\n\\n```\\nCREATE TABLE \"PlaylistTrack\" (\\n\\t\"PlaylistId\" INTEGER NOT NULL, \\n\\t\"TrackId\" INTEGER NOT NULL, \\n\\tPRIMARY KEY (\"PlaylistId\", \"TrackId\"), \\n\\tFOREIGN KEY(\"TrackId\") REFERENCES \"Track\" (\"TrackId\"), \\n\\tFOREIGN KEY(\"PlaylistId\") REFERENCES \"Playlist\" (\"PlaylistId\")\\n)\\n```\\n\\nThe `PlaylistId` column is a foreign key referencing the `PlaylistId` column in the `Playlist` table. The `TrackId` column is a foreign key referencing the `TrackId` column in the `Track` table.\\n\\nHere are three sample rows from the `PlaylistTrack` table:\\n\\n```\\nPlaylistId TrackId\\n1 3402\\n1 3389\\n1 3390\\n```\\n\\nPlease let me know if there is anything else I can help with.'}" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "agent_executor.invoke(\"Describe the playlisttrack table\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Using a dynamic few-shot prompt\n", + "\n", + "To optimize agent performance, we can provide a custom prompt with domain-specific knowledge. In this case we'll create a few shot prompt with an example selector, that will dynamically build the few shot prompt based on the user input.\n", + "\n", + "First we need some user input <> SQL query examples:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "examples = [\n", + " {\"input\": \"List all artists.\", \"query\": \"SELECT * FROM Artist;\"},\n", + " {\n", + " \"input\": \"Find all albums for the artist 'AC/DC'.\",\n", + " \"query\": \"SELECT * FROM Album WHERE ArtistId = (SELECT ArtistId FROM Artist WHERE Name = 'AC/DC');\",\n", + " },\n", + " {\n", + " \"input\": \"List all tracks in the 'Rock' genre.\",\n", + " \"query\": \"SELECT * FROM Track WHERE GenreId = (SELECT GenreId FROM Genre WHERE Name = 'Rock');\",\n", + " },\n", + " {\n", + " \"input\": \"Find the total duration of all tracks.\",\n", + " \"query\": \"SELECT SUM(Milliseconds) FROM Track;\",\n", + " },\n", + " {\n", + " \"input\": \"List all customers from Canada.\",\n", + " \"query\": \"SELECT * FROM Customer WHERE Country = 'Canada';\",\n", + " },\n", + " {\n", + " \"input\": \"How many tracks are there in the album with ID 5?\",\n", + " \"query\": \"SELECT COUNT(*) FROM Track WHERE AlbumId = 5;\",\n", + " },\n", + " {\n", + " \"input\": \"Find the total number of invoices.\",\n", + " \"query\": \"SELECT COUNT(*) FROM Invoice;\",\n", + " },\n", + " {\n", + " \"input\": \"List all tracks that are longer than 5 minutes.\",\n", + " \"query\": \"SELECT * FROM Track WHERE Milliseconds > 300000;\",\n", + " },\n", + " {\n", + " \"input\": \"Who are the top 5 customers by total purchase?\",\n", + " \"query\": \"SELECT CustomerId, SUM(Total) AS TotalPurchase FROM Invoice GROUP BY CustomerId ORDER BY TotalPurchase DESC LIMIT 5;\",\n", + " },\n", + " {\n", + " \"input\": \"Which albums are from the year 2000?\",\n", + " \"query\": \"SELECT * FROM Album WHERE strftime('%Y', ReleaseDate) = '2000';\",\n", + " },\n", + " {\n", + " \"input\": \"How many employees are there\",\n", + " \"query\": 'SELECT COUNT(*) FROM \"Employee\"',\n", + " },\n", + "]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we can create an example selector. This will take the actual user input and select some number of examples to add to our few-shot prompt. We'll use a SemanticSimilarityExampleSelector, which will perform a semantic search using the embeddings and vector store we configure to find the examples most similar to our input:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_community.vectorstores import FAISS\n", + "from langchain_core.example_selectors import SemanticSimilarityExampleSelector\n", + "from langchain_openai import OpenAIEmbeddings\n", + "\n", + "example_selector = SemanticSimilarityExampleSelector.from_examples(\n", + " examples,\n", + " OpenAIEmbeddings(),\n", + " FAISS,\n", + " k=5,\n", + " input_keys=[\"input\"],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we can create our FewShotPromptTemplate, which takes our example selector, an example prompt for formatting each example, and a string prefix and suffix to put before and after our formatted examples:" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_core.prompts import (\n", + " ChatPromptTemplate,\n", + " FewShotPromptTemplate,\n", + " MessagesPlaceholder,\n", + " PromptTemplate,\n", + " SystemMessagePromptTemplate,\n", + ")\n", + "\n", + "system_prefix = \"\"\"You are an agent designed to interact with a SQL database.\n", + "Given an input question, create a syntactically correct {dialect} query to run, then look at the results of the query and return the answer.\n", + "Unless the user specifies a specific number of examples they wish to obtain, always limit your query to at most {top_k} results.\n", + "You can order the results by a relevant column to return the most interesting examples in the database.\n", + "Never query for all the columns from a specific table, only ask for the relevant columns given the question.\n", + "You have access to tools for interacting with the database.\n", + "Only use the given tools. Only use the information returned by the tools to construct your final answer.\n", + "You MUST double check your query before executing it. If you get an error while executing a query, rewrite the query and try again.\n", + "\n", + "DO NOT make any DML statements (INSERT, UPDATE, DELETE, DROP etc.) to the database.\n", + "\n", + "If the question does not seem related to the database, just return \"I don't know\" as the answer.\n", + "\n", + "Here are some examples of user inputs and their corresponding SQL queries:\"\"\"\n", + "\n", + "few_shot_prompt = FewShotPromptTemplate(\n", + " example_selector=example_selector,\n", + " example_prompt=PromptTemplate.from_template(\n", + " \"User input: {input}\\nSQL query: {query}\"\n", + " ),\n", + " input_variables=[\"input\", \"dialect\", \"top_k\"],\n", + " prefix=system_prefix,\n", + " suffix=\"\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Since our underlying agent is an [OpenAI tools agent](/docs/modules/agents/agent_types/openai_tools), which uses OpenAI function calling, our full prompt should be a chat prompt with a human message template and an agent_scratchpad `MessagesPlaceholder`. The few-shot prompt will be used for our system message:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "full_prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " SystemMessagePromptTemplate(prompt=few_shot_prompt),\n", + " (\"human\", \"{input}\"),\n", + " MessagesPlaceholder(\"agent_scratchpad\"),\n", + " ]\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "System: You are an agent designed to interact with a SQL database.\n", + "Given an input question, create a syntactically correct SQLite query to run, then look at the results of the query and return the answer.\n", + "Unless the user specifies a specific number of examples they wish to obtain, always limit your query to at most 5 results.\n", + "You can order the results by a relevant column to return the most interesting examples in the database.\n", + "Never query for all the columns from a specific table, only ask for the relevant columns given the question.\n", + "You have access to tools for interacting with the database.\n", + "Only use the given tools. Only use the information returned by the tools to construct your final answer.\n", + "You MUST double check your query before executing it. If you get an error while executing a query, rewrite the query and try again.\n", + "\n", + "DO NOT make any DML statements (INSERT, UPDATE, DELETE, DROP etc.) to the database.\n", + "\n", + "If the question does not seem related to the database, just return \"I don't know\" as the answer.\n", + "\n", + "Here are some examples of user inputs and their corresponding SQL queries:\n", + "\n", + "User input: List all artists.\n", + "SQL query: SELECT * FROM Artist;\n", + "\n", + "User input: How many employees are there\n", + "SQL query: SELECT COUNT(*) FROM \"Employee\"\n", + "\n", + "User input: How many tracks are there in the album with ID 5?\n", + "SQL query: SELECT COUNT(*) FROM Track WHERE AlbumId = 5;\n", + "\n", + "User input: List all tracks in the 'Rock' genre.\n", + "SQL query: SELECT * FROM Track WHERE GenreId = (SELECT GenreId FROM Genre WHERE Name = 'Rock');\n", + "\n", + "User input: Which albums are from the year 2000?\n", + "SQL query: SELECT * FROM Album WHERE strftime('%Y', ReleaseDate) = '2000';\n", + "Human: How many arists are there\n" + ] + } + ], + "source": [ + "# Example formatted prompt\n", + "prompt_val = full_prompt.invoke(\n", + " {\n", + " \"input\": \"How many arists are there\",\n", + " \"top_k\": 5,\n", + " \"dialect\": \"SQLite\",\n", + " \"agent_scratchpad\": [],\n", + " }\n", + ")\n", + "print(prompt_val.to_string())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And now we can create our agent with our custom prompt:" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "agent = create_sql_agent(\n", + " llm=llm,\n", + " db=db,\n", + " prompt=full_prompt,\n", + " verbose=True,\n", + " agent_type=\"openai-tools\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's try it out:" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3m\n", + "Invoking: `sql_db_query` with `{'query': 'SELECT COUNT(*) FROM Artist'}`\n", + "\n", + "\n", + "\u001b[0m\u001b[36;1m\u001b[1;3m[(275,)]\u001b[0m\u001b[32;1m\u001b[1;3mThere are 275 artists in the database.\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "{'input': 'How many artists are there?',\n", + " 'output': 'There are 275 artists in the database.'}" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "agent.invoke({\"input\": \"How many artists are there?\"})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Dealing with high-cardinality columns\n", + "\n", + "In order to filter columns that contain proper nouns such as addresses, song names or artists, we first need to double-check the spelling in order to filter the data correctly. \n", + "\n", + "We can achieve this by creating a vector store with all the distinct proper nouns that exist in the database. We can then have the agent query that vector store each time the user includes a proper noun in their question, to find the correct spelling for that word. In this way, the agent can make sure it understands which entity the user is referring to before building the target query.\n", + "\n", + "First we need the unique values for each entity we want, for which we define a function that parses the result into a list of elements:" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['For Those About To Rock We Salute You',\n", + " 'Balls to the Wall',\n", + " 'Restless and Wild',\n", + " 'Let There Be Rock',\n", + " 'Big Ones']" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import ast\n", + "import re\n", + "\n", + "\n", + "def query_as_list(db, query):\n", + " res = db.run(query)\n", + " res = [el for sub in ast.literal_eval(res) for el in sub if el]\n", + " res = [re.sub(r\"\\b\\d+\\b\", \"\", string).strip() for string in res]\n", + " return res\n", + "\n", + "\n", + "artists = query_as_list(db, \"SELECT Name FROM Artist\")\n", + "albums = query_as_list(db, \"SELECT Title FROM Album\")\n", + "albums[:5]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we can proceed with creating the custom **retriever tool** and the final agent:" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "from langchain.agents.agent_toolkits import create_retriever_tool\n", + "\n", + "vector_db = FAISS.from_texts(artists + albums, OpenAIEmbeddings())\n", + "retriever = vector_db.as_retriever(search_kwargs={\"k\": 5})\n", + "description = \"\"\"Use to look up values to filter on. Input is an approximate spelling of the proper noun, output is \\\n", + "valid proper nouns. Use the noun most similar to the search.\"\"\"\n", + "retriever_tool = create_retriever_tool(\n", + " retriever,\n", + " name=\"search_proper_nouns\",\n", + " description=description,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "system = \"\"\"You are an agent designed to interact with a SQL database.\n", + "Given an input question, create a syntactically correct {dialect} query to run, then look at the results of the query and return the answer.\n", + "Unless the user specifies a specific number of examples they wish to obtain, always limit your query to at most {top_k} results.\n", + "You can order the results by a relevant column to return the most interesting examples in the database.\n", + "Never query for all the columns from a specific table, only ask for the relevant columns given the question.\n", + "You have access to tools for interacting with the database.\n", + "Only use the given tools. Only use the information returned by the tools to construct your final answer.\n", + "You MUST double check your query before executing it. If you get an error while executing a query, rewrite the query and try again.\n", + "\n", + "DO NOT make any DML statements (INSERT, UPDATE, DELETE, DROP etc.) to the database.\n", + "\n", + "If you need to filter on a proper noun, you must ALWAYS first look up the filter value using the \"search_proper_nouns\" tool!\n", + "\n", + "You have access to the following tables: {table_names}\n", + "\n", + "If the question does not seem related to the database, just return \"I don't know\" as the answer.\"\"\"\n", + "\n", + "prompt = ChatPromptTemplate.from_messages([(\"system\", system), (\"human\", \"{input}\"), MessagesPlaceholder(\"agent_scratchpad\")])\n", + "agent = create_sql_agent(\n", + " llm=llm,\n", + " db=db,\n", + " extra_tools=[retriever_tool],\n", + " prompt=prompt,\n", + " agent_type=\"openai-tools\",\n", + " verbose=True,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3m\n", + "Invoking: `search_proper_nouns` with `{'query': 'alice in chains'}`\n", + "\n", + "\n", + "\u001b[0m\u001b[36;1m\u001b[1;3mAlice In Chains\n", + "\n", + "Metallica\n", + "\n", + "Pearl Jam\n", + "\n", + "Pearl Jam\n", + "\n", + "Smashing Pumpkins\u001b[0m\u001b[32;1m\u001b[1;3m\n", + "Invoking: `sql_db_query` with `{'query': \"SELECT COUNT(*) FROM Album WHERE ArtistId = (SELECT ArtistId FROM Artist WHERE Name = 'Alice In Chains')\"}`\n", + "\n", + "\n", + "\u001b[0m\u001b[36;1m\u001b[1;3m[(1,)]\u001b[0m\u001b[32;1m\u001b[1;3mAlice In Chains has 1 album.\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "{'input': 'How many albums does alice in chains have?',\n", + " 'output': 'Alice In Chains has 1 album.'}" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "agent.invoke({\"input\": \"How many albums does alice in chains have?\"})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As we can see, the agent used the `search_proper_nouns` tool in order to check how to correctly query the database for this specific artist." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Next steps\n", + "\n", + "Under the hood, `create_sql_agent` is just passing in SQL tools to more generic agent constructors. To learn more about the built-in generic agent types as well as how to build custom agents, head to the [Agents Modules](/docs/modules/agents/).\n", + "\n", + "The built-in `AgentExecutor` runs a simple Agent action -> Tool call -> Agent action... loop. To build more complex agent runtimes, head to the [LangGraph section](/docs/langgraph)." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "poetry-venv", + "language": "python", + "name": "poetry-venv" + }, + "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.9.1" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/docs/use_cases/sql/index.ipynb b/docs/docs/use_cases/sql/index.ipynb new file mode 100644 index 0000000000000..1b706c831b168 --- /dev/null +++ b/docs/docs/use_cases/sql/index.ipynb @@ -0,0 +1,68 @@ +{ + "cells": [ + { + "cell_type": "raw", + "metadata": {}, + "source": [ + "---\n", + "sidebar_position: 0.5\n", + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# SQL\n", + "\n", + "One of the most common types of databases that we can build Q&A systems for are SQL databases. LangChain comes with a number of built-in chains and agents that are compatible with any SQL dialect supported by SQLAlchemy (e.g., MySQL, PostgreSQL, Oracle SQL, Databricks, SQLite). They enable use cases such as:\n", + "\n", + "* Generating queries that will be run based on natural language questions,\n", + "* Creating chatbots that can answer questions based on database data,\n", + "* Building custom dashboards based on insights a user wants to analyze,\n", + "\n", + "and much more.\n", + "\n", + "## ⚠️ Security note ⚠️\n", + "\n", + "Building Q&A systems of SQL databases requires executing model-generated SQL queries. There are inherent risks in doing this. Make sure that your database connection permissions are always scoped as narrowly as possible for your chain/agent's needs. This will mitigate though not eliminate the risks of building a model-driven system. For more on general security best practices, [see here](/docs/security).\n", + "\n", + "![sql_usecase.png](../../../static/img/sql_usecase.png)\n", + "\n", + "## Quickstart\n", + "\n", + "Head to the **[Quickstart](/docs/use_cases/sql/quickstart)** page to get started.\n", + "\n", + "## Advanced\n", + "\n", + "Once you've familiarized yourself with the basics, you can head to the advanced guides:\n", + "\n", + "* [Agents](/docs/use_cases/sql/agents): Building agents that can interact with SQL DBs.\n", + "* [Prompting strategies](/docs/use_cases/sql/prompting): Strategies for improving SQL query generation.\n", + "* [Query validation](/docs/use_cases/sql/query_checking): How to validate SQL queries.\n", + "* [Large databases](/docs/use_cases/sql/large_db): How to interact with DBs with many tables and high-cardinality columns." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "poetry-venv", + "language": "python", + "name": "poetry-venv" + }, + "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.9.1" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/docs/use_cases/sql/large_db.ipynb b/docs/docs/use_cases/sql/large_db.ipynb new file mode 100644 index 0000000000000..dd034037d1a4b --- /dev/null +++ b/docs/docs/use_cases/sql/large_db.ipynb @@ -0,0 +1,627 @@ +{ + "cells": [ + { + "cell_type": "raw", + "id": "b2788654-3f62-4e2a-ab00-471922cc54df", + "metadata": {}, + "source": [ + "---\n", + "sidebar_position: 4\n", + "---" + ] + }, + { + "cell_type": "markdown", + "id": "6751831d-9b08-434f-829b-d0052a3b119f", + "metadata": {}, + "source": [ + "# Large databases\n", + "\n", + "In order to write valid queries against a database, we need to feed the model the table names, table schemas, and feature values for it to query over. When there are many tables, columns, and/or high-cardinality columns, it becomes impossible for us to dump the full information about our database in every prompt. Instead, we must find ways to dynamically insert into the prompt only the most relevant information. Let's take a look at some techniques for doing this.\n", + "\n", + "\n", + "## Setup\n", + "\n", + "First, get required packages and set environment variables:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "9675e433-e608-469e-b04e-2847479a8310", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m A new release of pip is available: \u001b[0m\u001b[31;49m23.2.1\u001b[0m\u001b[39;49m -> \u001b[0m\u001b[32;49m23.3.2\u001b[0m\n", + "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m To update, run: \u001b[0m\u001b[32;49mpip install --upgrade pip\u001b[0m\n", + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "%pip install --upgrade --quiet langchain langchain-community langchain-openai" + ] + }, + { + "cell_type": "markdown", + "id": "4f56ff5d-b2e4-49e3-a0b4-fb99466cfedc", + "metadata": {}, + "source": [ + "We default to OpenAI models in this guide, but you can swap them out for the model provider of your choice." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "06d8dd03-2d7b-4fef-b145-43c074eacb8b", + "metadata": {}, + "outputs": [ + { + "name": "stdin", + "output_type": "stream", + "text": [ + " ········\n" + ] + } + ], + "source": [ + "import getpass\n", + "import os\n", + "\n", + "# os.environ[\"OPENAI_API_KEY\"] = getpass.getpass()\n", + "\n", + "# Uncomment the below to use LangSmith. Not required.\n", + "os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass()\n", + "# os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"" + ] + }, + { + "cell_type": "markdown", + "id": "590ee096-db88-42af-90d4-99b8149df753", + "metadata": {}, + "source": [ + "The below example will use a SQLite connection with Chinook database. Follow [these installation steps](https://database.guide/2-sample-databases-sqlite/) to create `Chinook.db` in the same directory as this notebook:\n", + "\n", + "* Save [this file](https://raw.githubusercontent.com/lerocha/chinook-database/master/ChinookDatabase/DataSources/Chinook_Sqlite.sql) as `Chinook_Sqlite.sql`\n", + "* Run `sqlite3 Chinook.db`\n", + "* Run `.read Chinook_Sqlite.sql`\n", + "* Test `SELECT * FROM Artist LIMIT 10;`\n", + "\n", + "Now, `Chinhook.db` is in our directory and we can interface with it using the SQLAlchemy-driven [SQLDatabase](https://api.python.langchain.com/en/latest/utilities/langchain_community.utilities.sql_database.SQLDatabase.html) class:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "cebd3915-f58f-4e73-8459-265630ae8cd4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "sqlite\n", + "['Album', 'Artist', 'Customer', 'Employee', 'Genre', 'Invoice', 'InvoiceLine', 'MediaType', 'Playlist', 'PlaylistTrack', 'Track']\n" + ] + }, + { + "data": { + "text/plain": [ + "\"[(1, 'AC/DC'), (2, 'Accept'), (3, 'Aerosmith'), (4, 'Alanis Morissette'), (5, 'Alice In Chains'), (6, 'Antônio Carlos Jobim'), (7, 'Apocalyptica'), (8, 'Audioslave'), (9, 'BackBeat'), (10, 'Billy Cobham')]\"" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from langchain_community.utilities import SQLDatabase\n", + "\n", + "db = SQLDatabase.from_uri(\"sqlite:///Chinook.db\")\n", + "print(db.dialect)\n", + "print(db.get_usable_table_names())\n", + "db.run(\"SELECT * FROM Artist LIMIT 10;\")" + ] + }, + { + "cell_type": "markdown", + "id": "2e572e1f-99b5-46a2-9023-76d1e6256c0a", + "metadata": {}, + "source": [ + "## Many tables\n", + "\n", + "One of the main pieces of information we need to include in our prompt is the schemas of the relevant tables. When we have very many tables, we can't fit all of the schemas in a single prompt. What we can do in such cases is first extract the names of the tables related to the user input, and then include only their schemas.\n", + "\n", + "One easy and reliable way to do this is using OpenAI function-calling and Pydantic models. LangChain comes with a built-in [create_extraction_chain_pydantic](https://api.python.langchain.com/en/latest/chains/langchain.chains.openai_tools.extraction.create_extraction_chain_pydantic.html) chain that lets us do just this:" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "id": "d8236886-c54f-4bdb-ad74-2514888628fd", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[Table(name='Genre'), Table(name='Artist'), Table(name='Track')]" + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from langchain.chains.openai_tools import create_extraction_chain_pydantic\n", + "from langchain_core.pydantic_v1 import BaseModel, Field\n", + "from langchain_openai import ChatOpenAI\n", + "\n", + "llm = ChatOpenAI(model=\"gpt-3.5-turbo-1106\", temperature=0)\n", + "\n", + "\n", + "class Table(BaseModel):\n", + " \"\"\"Table in SQL database.\"\"\"\n", + "\n", + " name: str = Field(description=\"Name of table in SQL database.\")\n", + "\n", + "\n", + "table_names = \"\\n\".join(db.get_usable_table_names())\n", + "system = f\"\"\"Return the names of ALL the SQL tables that MIGHT be relevant to the user question. \\\n", + "The tables are:\n", + "\n", + "{table_names}\n", + "\n", + "Remember to include ALL POTENTIALLY RELEVANT tables, even if you're not sure that they're needed.\"\"\"\n", + "table_chain = create_extraction_chain_pydantic(Table, llm, system_message=system)\n", + "table_chain.invoke({\"input\": \"What are all the genres of Alanis Morisette songs\"})" + ] + }, + { + "cell_type": "markdown", + "id": "1641dbba-d359-4cb2-ac52-82dfae99f392", + "metadata": {}, + "source": [ + "This works pretty well! Except, as we'll see below, we actually need a few other tables as well. This would be pretty difficult for the model to know based just on the user question. In this case, we might think to simplify our model's job by grouping the tables together. We'll just ask the model to choose between categories \"Music\" and \"Business\", and then take care of selecting all the relevant tables from there:" + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "id": "0ccb0bf5-c580-428f-9cde-a58772ae784e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[Table(name='Music')]" + ] + }, + "execution_count": 59, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "system = f\"\"\"Return the names of the SQL tables that are relevant to the user question. \\\n", + "The tables are:\n", + "\n", + "Music\n", + "Business\"\"\"\n", + "category_chain = create_extraction_chain_pydantic(Table, llm, system_message=system)\n", + "category_chain.invoke({\"input\": \"What are all the genres of Alanis Morisette songs\"})" + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "id": "ae4899fc-6f8a-4b10-983c-9e3fef4a7bb9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['Album', 'Artist', 'Genre', 'MediaType', 'Playlist', 'PlaylistTrack', 'Track']" + ] + }, + "execution_count": 60, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from typing import List\n", + "\n", + "\n", + "def get_tables(categories: List[Table]) -> List[str]:\n", + " tables = []\n", + " for category in categories:\n", + " if category.name == \"Music\":\n", + " tables.extend(\n", + " [\n", + " \"Album\",\n", + " \"Artist\",\n", + " \"Genre\",\n", + " \"MediaType\",\n", + " \"Playlist\",\n", + " \"PlaylistTrack\",\n", + " \"Track\",\n", + " ]\n", + " )\n", + " elif category.name == \"Business\":\n", + " tables.extend([\"Customer\", \"Employee\", \"Invoice\", \"InvoiceLine\"])\n", + " return tables\n", + "\n", + "\n", + "table_chain = category_chain | get_tables # noqa\n", + "table_chain.invoke({\"input\": \"What are all the genres of Alanis Morisette songs\"})" + ] + }, + { + "cell_type": "markdown", + "id": "04d52d01-1ccf-4753-b34a-0dcbc4921f78", + "metadata": {}, + "source": [ + "Now that we've got a chain that can output the relevant tables for any query we can combine this with our [create_sql_query_chain](https://api.python.langchain.com/en/latest/chains/langchain.chains.sql_database.query.create_sql_query_chain.html), which can accept a list of `table_names_to_use` to determine which table schemas are included in the prompt:" + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "id": "79f2a5a2-eb99-47e3-9c2b-e5751a800174", + "metadata": {}, + "outputs": [], + "source": [ + "from operator import itemgetter\n", + "\n", + "from langchain.chains import create_sql_query_chain\n", + "from langchain_core.runnables import RunnablePassthrough\n", + "\n", + "query_chain = create_sql_query_chain(llm, db)\n", + "# Convert \"question\" key to the \"input\" key expected by current table_chain.\n", + "table_chain = {\"input\": itemgetter(\"question\")} | table_chain\n", + "# Set table_names_to_use using table_chain.\n", + "full_chain = RunnablePassthrough.assign(table_names_to_use=table_chain) | query_chain" + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "id": "424a7564-f63c-4584-b734-88021926486d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "SELECT \"Genre\".\"Name\"\n", + "FROM \"Genre\"\n", + "JOIN \"Track\" ON \"Genre\".\"GenreId\" = \"Track\".\"GenreId\"\n", + "JOIN \"Album\" ON \"Track\".\"AlbumId\" = \"Album\".\"AlbumId\"\n", + "JOIN \"Artist\" ON \"Album\".\"ArtistId\" = \"Artist\".\"ArtistId\"\n", + "WHERE \"Artist\".\"Name\" = 'Alanis Morissette'\n" + ] + } + ], + "source": [ + "query = full_chain.invoke(\n", + " {\"question\": \"What are all the genres of Alanis Morisette songs\"}\n", + ")\n", + "print(query)" + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "id": "3fb715cf-69d1-46a6-a1a7-9715ee550a0c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"[('Rock',), ('Rock',), ('Rock',), ('Rock',), ('Rock',), ('Rock',), ('Rock',), ('Rock',), ('Rock',), ('Rock',), ('Rock',), ('Rock',), ('Rock',)]\"" + ] + }, + "execution_count": 53, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "db.run(query)" + ] + }, + { + "cell_type": "markdown", + "id": "bb3d12b0-81a6-4250-8bc4-d58fe762c4cc", + "metadata": {}, + "source": [ + "We might rephrase our question slightly to remove redundancy in the answer" + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "id": "010b5c3c-d55b-461a-8de5-8f1a8b2c56ec", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "SELECT DISTINCT g.Name\n", + "FROM Genre g\n", + "JOIN Track t ON g.GenreId = t.GenreId\n", + "JOIN Album a ON t.AlbumId = a.AlbumId\n", + "JOIN Artist ar ON a.ArtistId = ar.ArtistId\n", + "WHERE ar.Name = 'Alanis Morissette'\n" + ] + } + ], + "source": [ + "query = full_chain.invoke(\n", + " {\"question\": \"What is the set of all unique genres of Alanis Morisette songs\"}\n", + ")\n", + "print(query)" + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "id": "d21c0563-1f55-4577-8222-b0e9802f1c4b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"[('Rock',)]\"" + ] + }, + "execution_count": 58, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "db.run(query)" + ] + }, + { + "cell_type": "markdown", + "id": "7a717020-84c2-40f3-ba84-6624138d8e0c", + "metadata": {}, + "source": [ + "We can see the [LangSmith trace](https://smith.langchain.com/public/20b8ef90-1dac-4754-90f0-6bc11203c50a/r) for this run here.\n", + "\n", + "We've seen how to dynamically include a subset of table schemas in a prompt within a chain. Another possible approach to this problem is to let an Agent decide for itself when to look up tables by giving it a Tool to do so. You can see an example of this in the [SQL: Agents](/docs/use_cases/sql/agents) guide." + ] + }, + { + "cell_type": "markdown", + "id": "cb9e54fd-64ca-4ed5-847c-afc635aae4f5", + "metadata": {}, + "source": [ + "## High-cardinality columns\n", + "\n", + "In order to filter columns that contain proper nouns such as addresses, song names or artists, we first need to double-check the spelling in order to filter the data correctly. \n", + "\n", + "One naive strategy it to create a vector store with all the distinct proper nouns that exist in the database. We can then query that vector store each user input and inject the most relevant proper nouns into the prompt.\n", + "\n", + "First we need the unique values for each entity we want, for which we define a function that parses the result into a list of elements:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "dee1b9e1-36b0-4cc1-ab78-7a872ad87e29", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['AC/DC', 'Accept', 'Aerosmith', 'Alanis Morissette', 'Alice In Chains']" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import ast\n", + "import re\n", + "\n", + "\n", + "def query_as_list(db, query):\n", + " res = db.run(query)\n", + " res = [el for sub in ast.literal_eval(res) for el in sub if el]\n", + " res = [re.sub(r\"\\b\\d+\\b\", \"\", string).strip() for string in res]\n", + " return res\n", + "\n", + "\n", + "proper_nouns = query_as_list(db, \"SELECT Name FROM Artist\")\n", + "proper_nouns += query_as_list(db, \"SELECT Title FROM Album\")\n", + "proper_nouns += query_as_list(db, \"SELECT Name FROM Genre\")\n", + "len(proper_nouns)\n", + "proper_nouns[:5]" + ] + }, + { + "cell_type": "markdown", + "id": "22efa968-1879-4d7a-858f-7899dfa57454", + "metadata": {}, + "source": [ + "Now we can embed and store all of our values in a vector database:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "ea50abce-545a-4dc3-8795-8d364f7d142a", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_community.vectorstores import FAISS\n", + "from langchain_openai import OpenAIEmbeddings\n", + "\n", + "vector_db = FAISS.from_texts(proper_nouns, OpenAIEmbeddings())\n", + "retriever = vector_db.as_retriever(search_kwargs={\"k\": 15})" + ] + }, + { + "cell_type": "markdown", + "id": "a5d1d5c0-0928-40a4-b961-f1afe03cd5d3", + "metadata": {}, + "source": [ + "And put together a query construction chain that first retrieves values from the database and inserts them into the prompt:" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "aea123ae-d809-44a0-be5d-d883c60d6a11", + "metadata": {}, + "outputs": [], + "source": [ + "from operator import itemgetter\n", + "\n", + "from langchain_core.prompts import ChatPromptTemplate\n", + "from langchain_core.runnables import RunnablePassthrough\n", + "\n", + "system = \"\"\"You are a SQLite expert. Given an input question, create a syntactically \\\n", + "correct SQLite query to run. Unless otherwise specificed, do not return more than \\\n", + "{top_k} rows.\\n\\nHere is the relevant table info: {table_info}\\n\\nHere is a non-exhaustive \\\n", + "list of possible feature values. If filtering on a feature value make sure to check its spelling \\\n", + "against this list first:\\n\\n{proper_nouns}\"\"\"\n", + "\n", + "prompt = ChatPromptTemplate.from_messages([(\"system\", system), (\"human\", \"{input}\")])\n", + "\n", + "query_chain = create_sql_query_chain(llm, db, prompt=prompt)\n", + "retriever_chain = (\n", + " itemgetter(\"question\")\n", + " | retriever\n", + " | (lambda docs: \"\\n\".join(doc.page_content for doc in docs))\n", + ")\n", + "chain = RunnablePassthrough.assign(proper_nouns=retriever_chain) | query_chain" + ] + }, + { + "cell_type": "markdown", + "id": "12b0ed60-2536-4f82-85df-e096a272072a", + "metadata": {}, + "source": [ + "To try out our chain, let's see what happens when we try filtering on \"elenis moriset\", a mispelling of Alanis Morissette, without and with retrieval:" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "fcdd8432-07a4-4609-8214-b1591dd94950", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "SELECT DISTINCT Genre.Name\n", + "FROM Genre\n", + "JOIN Track ON Genre.GenreId = Track.GenreId\n", + "JOIN Album ON Track.AlbumId = Album.AlbumId\n", + "JOIN Artist ON Album.ArtistId = Artist.ArtistId\n", + "WHERE Artist.Name = 'Elenis Moriset'\n" + ] + }, + { + "data": { + "text/plain": [ + "''" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Without retrieval\n", + "query = query_chain.invoke(\n", + " {\"question\": \"What are all the genres of elenis moriset songs\", \"proper_nouns\": \"\"}\n", + ")\n", + "print(query)\n", + "db.run(query)" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "e8a3231a-8590-46f5-a954-da06829ee6df", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "SELECT DISTINCT Genre.Name\n", + "FROM Genre\n", + "JOIN Track ON Genre.GenreId = Track.GenreId\n", + "JOIN Album ON Track.AlbumId = Album.AlbumId\n", + "JOIN Artist ON Album.ArtistId = Artist.ArtistId\n", + "WHERE Artist.Name = 'Alanis Morissette'\n" + ] + }, + { + "data": { + "text/plain": [ + "\"[('Rock',)]\"" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# With retrieval\n", + "query = chain.invoke({\"question\": \"What are all the genres of elenis moriset songs\"})\n", + "print(query)\n", + "db.run(query)" + ] + }, + { + "cell_type": "markdown", + "id": "7f99181b-a75c-4ff3-b37b-33f99a506581", + "metadata": {}, + "source": [ + "We can see that with retrieval we're able to correct the spelling and get back a valid result.\n", + "\n", + "Another possible approach to this problem is to let an Agent decide for itself when to look up proper nouns. You can see an example of this in the [SQL: Agents](/docs/use_cases/sql/agents) guide." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "poetry-venv", + "language": "python", + "name": "poetry-venv" + }, + "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.9.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/docs/use_cases/sql/prompting.ipynb b/docs/docs/use_cases/sql/prompting.ipynb new file mode 100644 index 0000000000000..27a60b3de9d7e --- /dev/null +++ b/docs/docs/use_cases/sql/prompting.ipynb @@ -0,0 +1,789 @@ +{ + "cells": [ + { + "cell_type": "raw", + "metadata": {}, + "source": [ + "---\n", + "sidebar_position: 2\n", + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Prompting strategies\n", + "\n", + "In this guide we'll go over prompting strategies to improve SQL query generation. We'll largely focus on methods for getting relevant database-specific information in your prompt.\n", + "\n", + "## Setup\n", + "\n", + "First, get required packages and set environment variables:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet langchain langchain-community langchain-experimental langchain-openai" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We default to OpenAI models in this guide, but you can swap them out for the model provider of your choice." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import getpass\n", + "import os\n", + "\n", + "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass()\n", + "\n", + "# Uncomment the below to use LangSmith. Not required.\n", + "# os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass()\n", + "# os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The below example will use a SQLite connection with Chinook database. Follow [these installation steps](https://database.guide/2-sample-databases-sqlite/) to create `Chinook.db` in the same directory as this notebook:\n", + "\n", + "* Save [this file](https://raw.githubusercontent.com/lerocha/chinook-database/master/ChinookDatabase/DataSources/Chinook_Sqlite.sql) as `Chinook_Sqlite.sql`\n", + "* Run `sqlite3 Chinook.db`\n", + "* Run `.read Chinook_Sqlite.sql`\n", + "* Test `SELECT * FROM Artist LIMIT 10;`\n", + "\n", + "Now, `Chinhook.db` is in our directory and we can interface with it using the SQLAlchemy-driven `SQLDatabase` class:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "sqlite\n", + "['Album', 'Artist', 'Customer', 'Employee', 'Genre', 'Invoice', 'InvoiceLine', 'MediaType', 'Playlist', 'PlaylistTrack', 'Track']\n" + ] + }, + { + "data": { + "text/plain": [ + "\"[(1, 'AC/DC'), (2, 'Accept'), (3, 'Aerosmith'), (4, 'Alanis Morissette'), (5, 'Alice In Chains'), (6, 'Antônio Carlos Jobim'), (7, 'Apocalyptica'), (8, 'Audioslave'), (9, 'BackBeat'), (10, 'Billy Cobham')]\"" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from langchain_community.utilities import SQLDatabase\n", + "\n", + "db = SQLDatabase.from_uri(\"sqlite:///Chinook.db\", sample_rows_in_table_info=3)\n", + "print(db.dialect)\n", + "print(db.get_usable_table_names())\n", + "db.run(\"SELECT * FROM Artist LIMIT 10;\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Dialect-specific prompting\n", + "\n", + "One of the simplest things we can do is make our prompt specific to the SQL dialect we're using. When using the built-in [create_sql_query_chain](https://api.python.langchain.com/en/latest/chains/langchain.chains.sql_database.query.create_sql_query_chain.html) and [SQLDatabase](https://api.python.langchain.com/en/latest/utilities/langchain_community.utilities.sql_database.SQLDatabase.html), this is handled for you for any of the following dialects:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['crate',\n", + " 'duckdb',\n", + " 'googlesql',\n", + " 'mssql',\n", + " 'mysql',\n", + " 'mariadb',\n", + " 'oracle',\n", + " 'postgresql',\n", + " 'sqlite',\n", + " 'clickhouse',\n", + " 'prestodb']" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from langchain.chains.sql_database.prompt import SQL_PROMPTS\n", + "\n", + "list(SQL_PROMPTS)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For example, using our current DB we can see that we'll get a SQLite-specific prompt:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "You are a SQLite expert. Given an input question, first create a syntactically correct SQLite query to run, then look at the results of the query and return the answer to the input question.\n", + "Unless the user specifies in the question a specific number of examples to obtain, query for at most 5 results using the LIMIT clause as per SQLite. You can order the results to return the most informative data in the database.\n", + "Never query for all columns from a table. You must query only the columns that are needed to answer the question. Wrap each column name in double quotes (\") to denote them as delimited identifiers.\n", + "Pay attention to use only the column names you can see in the tables below. Be careful to not query for columns that do not exist. Also, pay attention to which column is in which table.\n", + "Pay attention to use date('now') function to get the current date, if the question involves \"today\".\n", + "\n", + "Use the following format:\n", + "\n", + "Question: Question here\n", + "SQLQuery: SQL Query to run\n", + "SQLResult: Result of the SQLQuery\n", + "Answer: Final answer here\n", + "\n", + "Only use the following tables:\n", + "\u001b[33;1m\u001b[1;3m{table_info}\u001b[0m\n", + "\n", + "Question: \u001b[33;1m\u001b[1;3m{input}\u001b[0m\n" + ] + } + ], + "source": [ + "from langchain.chains import create_sql_query_chain\n", + "from langchain_openai import ChatOpenAI\n", + "\n", + "llm = ChatOpenAI(model=\"gpt-3.5-turbo\", temperature=\"0\")\n", + "chain = create_sql_query_chain(llm, db)\n", + "chain.get_prompts()[0].pretty_print()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Table definitions and example rows\n", + "\n", + "In basically any SQL chain, we'll need to feed the model at least part of the database schema. Without this it won't be able to write valid queries. Our database comes with some convenience methods to give us the relevant context. Specifically, we can get the table names, their schemas, and a sample of rows from each table:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['table_info', 'table_names']\n", + "\n", + "CREATE TABLE \"Album\" (\n", + "\t\"AlbumId\" INTEGER NOT NULL, \n", + "\t\"Title\" NVARCHAR(160) NOT NULL, \n", + "\t\"ArtistId\" INTEGER NOT NULL, \n", + "\tPRIMARY KEY (\"AlbumId\"), \n", + "\tFOREIGN KEY(\"ArtistId\") REFERENCES \"Artist\" (\"ArtistId\")\n", + ")\n", + "\n", + "/*\n", + "3 rows from Album table:\n", + "AlbumId\tTitle\tArtistId\n", + "1\tFor Those About To Rock We Salute You\t1\n", + "2\tBalls to the Wall\t2\n", + "3\tRestless and Wild\t2\n", + "*/\n", + "\n", + "\n", + "CREATE TABLE \"Artist\" (\n", + "\t\"ArtistId\" INTEGER NOT NULL, \n", + "\t\"Name\" NVARCHAR(120), \n", + "\tPRIMARY KEY (\"ArtistId\")\n", + ")\n", + "\n", + "/*\n", + "3 rows from Artist table:\n", + "ArtistId\tName\n", + "1\tAC/DC\n", + "2\tAccept\n", + "3\tAerosmith\n", + "*/\n", + "\n", + "\n", + "CREATE TABLE \"Customer\" (\n", + "\t\"CustomerId\" INTEGER NOT NULL, \n", + "\t\"FirstName\" NVARCHAR(40) NOT NULL, \n", + "\t\"LastName\" NVARCHAR(20) NOT NULL, \n", + "\t\"Company\" NVARCHAR(80), \n", + "\t\"Address\" NVARCHAR(70), \n", + "\t\"City\" NVARCHAR(40), \n", + "\t\"State\" NVARCHAR(40), \n", + "\t\"Country\" NVARCHAR(40), \n", + "\t\"PostalCode\" NVARCHAR(10), \n", + "\t\"Phone\" NVARCHAR(24), \n", + "\t\"Fax\" NVARCHAR(24), \n", + "\t\"Email\" NVARCHAR(60) NOT NULL, \n", + "\t\"SupportRepId\" INTEGER, \n", + "\tPRIMARY KEY (\"CustomerId\"), \n", + "\tFOREIGN KEY(\"SupportRepId\") REFERENCES \"Employee\" (\"EmployeeId\")\n", + ")\n", + "\n", + "/*\n", + "3 rows from Customer table:\n", + "CustomerId\tFirstName\tLastName\tCompany\tAddress\tCity\tState\tCountry\tPostalCode\tPhone\tFax\tEmail\tSupportRepId\n", + "1\tLuís\tGonçalves\tEmbraer - Empresa Brasileira de Aeronáutica S.A.\tAv. Brigadeiro Faria Lima, 2170\tSão José dos Campos\tSP\tBrazil\t12227-000\t+55 (12) 3923-5555\t+55 (12) 3923-5566\tluisg@embraer.com.br\t3\n", + "2\tLeonie\tKöhler\tNone\tTheodor-Heuss-Straße 34\tStuttgart\tNone\tGermany\t70174\t+49 0711 2842222\tNone\tleonekohler@surfeu.de\t5\n", + "3\tFrançois\tTremblay\tNone\t1498 rue Bélanger\tMontréal\tQC\tCanada\tH2G 1A7\t+1 (514) 721-4711\tNone\tftremblay@gmail.com\t3\n", + "*/\n", + "\n", + "\n", + "CREATE TABLE \"Employee\" (\n", + "\t\"EmployeeId\" INTEGER NOT NULL, \n", + "\t\"LastName\" NVARCHAR(20) NOT NULL, \n", + "\t\"FirstName\" NVARCHAR(20) NOT NULL, \n", + "\t\"Title\" NVARCHAR(30), \n", + "\t\"ReportsTo\" INTEGER, \n", + "\t\"BirthDate\" DATETIME, \n", + "\t\"HireDate\" DATETIME, \n", + "\t\"Address\" NVARCHAR(70), \n", + "\t\"City\" NVARCHAR(40), \n", + "\t\"State\" NVARCHAR(40), \n", + "\t\"Country\" NVARCHAR(40), \n", + "\t\"PostalCode\" NVARCHAR(10), \n", + "\t\"Phone\" NVARCHAR(24), \n", + "\t\"Fax\" NVARCHAR(24), \n", + "\t\"Email\" NVARCHAR(60), \n", + "\tPRIMARY KEY (\"EmployeeId\"), \n", + "\tFOREIGN KEY(\"ReportsTo\") REFERENCES \"Employee\" (\"EmployeeId\")\n", + ")\n", + "\n", + "/*\n", + "3 rows from Employee table:\n", + "EmployeeId\tLastName\tFirstName\tTitle\tReportsTo\tBirthDate\tHireDate\tAddress\tCity\tState\tCountry\tPostalCode\tPhone\tFax\tEmail\n", + "1\tAdams\tAndrew\tGeneral Manager\tNone\t1962-02-18 00:00:00\t2002-08-14 00:00:00\t11120 Jasper Ave NW\tEdmonton\tAB\tCanada\tT5K 2N1\t+1 (780) 428-9482\t+1 (780) 428-3457\tandrew@chinookcorp.com\n", + "2\tEdwards\tNancy\tSales Manager\t1\t1958-12-08 00:00:00\t2002-05-01 00:00:00\t825 8 Ave SW\tCalgary\tAB\tCanada\tT2P 2T3\t+1 (403) 262-3443\t+1 (403) 262-3322\tnancy@chinookcorp.com\n", + "3\tPeacock\tJane\tSales Support Agent\t2\t1973-08-29 00:00:00\t2002-04-01 00:00:00\t1111 6 Ave SW\tCalgary\tAB\tCanada\tT2P 5M5\t+1 (403) 262-3443\t+1 (403) 262-6712\tjane@chinookcorp.com\n", + "*/\n", + "\n", + "\n", + "CREATE TABLE \"Genre\" (\n", + "\t\"GenreId\" INTEGER NOT NULL, \n", + "\t\"Name\" NVARCHAR(120), \n", + "\tPRIMARY KEY (\"GenreId\")\n", + ")\n", + "\n", + "/*\n", + "3 rows from Genre table:\n", + "GenreId\tName\n", + "1\tRock\n", + "2\tJazz\n", + "3\tMetal\n", + "*/\n", + "\n", + "\n", + "CREATE TABLE \"Invoice\" (\n", + "\t\"InvoiceId\" INTEGER NOT NULL, \n", + "\t\"CustomerId\" INTEGER NOT NULL, \n", + "\t\"InvoiceDate\" DATETIME NOT NULL, \n", + "\t\"BillingAddress\" NVARCHAR(70), \n", + "\t\"BillingCity\" NVARCHAR(40), \n", + "\t\"BillingState\" NVARCHAR(40), \n", + "\t\"BillingCountry\" NVARCHAR(40), \n", + "\t\"BillingPostalCode\" NVARCHAR(10), \n", + "\t\"Total\" NUMERIC(10, 2) NOT NULL, \n", + "\tPRIMARY KEY (\"InvoiceId\"), \n", + "\tFOREIGN KEY(\"CustomerId\") REFERENCES \"Customer\" (\"CustomerId\")\n", + ")\n", + "\n", + "/*\n", + "3 rows from Invoice table:\n", + "InvoiceId\tCustomerId\tInvoiceDate\tBillingAddress\tBillingCity\tBillingState\tBillingCountry\tBillingPostalCode\tTotal\n", + "1\t2\t2009-01-01 00:00:00\tTheodor-Heuss-Straße 34\tStuttgart\tNone\tGermany\t70174\t1.98\n", + "2\t4\t2009-01-02 00:00:00\tUllevålsveien 14\tOslo\tNone\tNorway\t0171\t3.96\n", + "3\t8\t2009-01-03 00:00:00\tGrétrystraat 63\tBrussels\tNone\tBelgium\t1000\t5.94\n", + "*/\n", + "\n", + "\n", + "CREATE TABLE \"InvoiceLine\" (\n", + "\t\"InvoiceLineId\" INTEGER NOT NULL, \n", + "\t\"InvoiceId\" INTEGER NOT NULL, \n", + "\t\"TrackId\" INTEGER NOT NULL, \n", + "\t\"UnitPrice\" NUMERIC(10, 2) NOT NULL, \n", + "\t\"Quantity\" INTEGER NOT NULL, \n", + "\tPRIMARY KEY (\"InvoiceLineId\"), \n", + "\tFOREIGN KEY(\"TrackId\") REFERENCES \"Track\" (\"TrackId\"), \n", + "\tFOREIGN KEY(\"InvoiceId\") REFERENCES \"Invoice\" (\"InvoiceId\")\n", + ")\n", + "\n", + "/*\n", + "3 rows from InvoiceLine table:\n", + "InvoiceLineId\tInvoiceId\tTrackId\tUnitPrice\tQuantity\n", + "1\t1\t2\t0.99\t1\n", + "2\t1\t4\t0.99\t1\n", + "3\t2\t6\t0.99\t1\n", + "*/\n", + "\n", + "\n", + "CREATE TABLE \"MediaType\" (\n", + "\t\"MediaTypeId\" INTEGER NOT NULL, \n", + "\t\"Name\" NVARCHAR(120), \n", + "\tPRIMARY KEY (\"MediaTypeId\")\n", + ")\n", + "\n", + "/*\n", + "3 rows from MediaType table:\n", + "MediaTypeId\tName\n", + "1\tMPEG audio file\n", + "2\tProtected AAC audio file\n", + "3\tProtected MPEG-4 video file\n", + "*/\n", + "\n", + "\n", + "CREATE TABLE \"Playlist\" (\n", + "\t\"PlaylistId\" INTEGER NOT NULL, \n", + "\t\"Name\" NVARCHAR(120), \n", + "\tPRIMARY KEY (\"PlaylistId\")\n", + ")\n", + "\n", + "/*\n", + "3 rows from Playlist table:\n", + "PlaylistId\tName\n", + "1\tMusic\n", + "2\tMovies\n", + "3\tTV Shows\n", + "*/\n", + "\n", + "\n", + "CREATE TABLE \"PlaylistTrack\" (\n", + "\t\"PlaylistId\" INTEGER NOT NULL, \n", + "\t\"TrackId\" INTEGER NOT NULL, \n", + "\tPRIMARY KEY (\"PlaylistId\", \"TrackId\"), \n", + "\tFOREIGN KEY(\"TrackId\") REFERENCES \"Track\" (\"TrackId\"), \n", + "\tFOREIGN KEY(\"PlaylistId\") REFERENCES \"Playlist\" (\"PlaylistId\")\n", + ")\n", + "\n", + "/*\n", + "3 rows from PlaylistTrack table:\n", + "PlaylistId\tTrackId\n", + "1\t3402\n", + "1\t3389\n", + "1\t3390\n", + "*/\n", + "\n", + "\n", + "CREATE TABLE \"Track\" (\n", + "\t\"TrackId\" INTEGER NOT NULL, \n", + "\t\"Name\" NVARCHAR(200) NOT NULL, \n", + "\t\"AlbumId\" INTEGER, \n", + "\t\"MediaTypeId\" INTEGER NOT NULL, \n", + "\t\"GenreId\" INTEGER, \n", + "\t\"Composer\" NVARCHAR(220), \n", + "\t\"Milliseconds\" INTEGER NOT NULL, \n", + "\t\"Bytes\" INTEGER, \n", + "\t\"UnitPrice\" NUMERIC(10, 2) NOT NULL, \n", + "\tPRIMARY KEY (\"TrackId\"), \n", + "\tFOREIGN KEY(\"MediaTypeId\") REFERENCES \"MediaType\" (\"MediaTypeId\"), \n", + "\tFOREIGN KEY(\"GenreId\") REFERENCES \"Genre\" (\"GenreId\"), \n", + "\tFOREIGN KEY(\"AlbumId\") REFERENCES \"Album\" (\"AlbumId\")\n", + ")\n", + "\n", + "/*\n", + "3 rows from Track table:\n", + "TrackId\tName\tAlbumId\tMediaTypeId\tGenreId\tComposer\tMilliseconds\tBytes\tUnitPrice\n", + "1\tFor Those About To Rock (We Salute You)\t1\t1\t1\tAngus Young, Malcolm Young, Brian Johnson\t343719\t11170334\t0.99\n", + "2\tBalls to the Wall\t2\t2\t1\tNone\t342562\t5510424\t0.99\n", + "3\tFast As a Shark\t3\t2\t1\tF. Baltes, S. Kaufman, U. Dirkscneider & W. Hoffman\t230619\t3990994\t0.99\n", + "*/\n" + ] + } + ], + "source": [ + "context = db.get_context()\n", + "print(list(context))\n", + "print(context[\"table_info\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "When we don't have too many, or too wide of, tables, we can just insert the entirety of this information in our prompt:" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "You are a SQLite expert. Given an input question, first create a syntactically correct SQLite query to run, then look at the results of the query and return the answer to the input question.\n", + "Unless the user specifies in the question a specific number of examples to obtain, query for at most 5 results using the LIMIT clause as per SQLite. You can order the results to return the most informative data in the database.\n", + "Never query for all columns from a table. You must query only the columns that are needed to answer the question. Wrap each column name in double quotes (\") to denote them as delimited identifiers.\n", + "Pay attention to use only the column names you can see in the tables below. Be careful to not query for columns that do not exist. Also, pay attention to which column is in which table.\n", + "Pay attention to use date('now') function to get the current date, if the question involves \"today\".\n", + "\n", + "Use the following format:\n", + "\n", + "Question: Question here\n", + "SQLQuery: SQL Query to run\n", + "SQLResult: Result of the SQLQuery\n", + "Answer: Final answer here\n", + "\n", + "Only use the following tables:\n", + "\n", + "CREATE TABLE \"Album\" (\n", + "\t\"AlbumId\" INTEGER NOT NULL, \n", + "\t\"Title\" NVARCHAR(160) NOT NULL, \n", + "\t\"ArtistId\" INTEGER NOT NULL, \n", + "\tPRIMARY KEY (\"AlbumId\"), \n", + "\tFOREIGN KEY(\"ArtistId\") REFERENCES \"Artist\" (\"ArtistId\")\n", + ")\n", + "\n", + "/*\n", + "3 rows from Album table:\n", + "AlbumId\tTitle\tArtistId\n", + "1\tFor Those About To Rock We Salute You\t1\n", + "2\tBalls to the Wall\t2\n", + "3\tRestless and Wild\t2\n", + "*/\n", + "\n", + "\n", + "CREATE TABLE \"Artist\" (\n", + "\t\"ArtistId\" INTEGER NOT NULL, \n", + "\t\"Name\" NVARCHAR(120)\n" + ] + } + ], + "source": [ + "prompt_with_context = chain.get_prompts()[0].partial(table_info=context[\"table_info\"])\n", + "print(prompt_with_context.pretty_repr()[:1500])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "When we do have database schemas that are too large to fit into our model's context window, we'll need to come up with ways of inserting only the relevant table definitions into the prompt based on the user input. For more on this head to the [Many tables, wide tables, high-cardinality feature](/docs/use_cases/sql/large_db) guide." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Few-shot examples\n", + "\n", + "Including examples of natural language questions being converted to valid SQL queries against our database in the prompt will often improve model performance, especially for complex queries.\n", + "\n", + "Let's say we have the following examples:" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [], + "source": [ + "examples = [\n", + " {\"input\": \"List all artists.\", \"query\": \"SELECT * FROM Artist;\"},\n", + " {\n", + " \"input\": \"Find all albums for the artist 'AC/DC'.\",\n", + " \"query\": \"SELECT * FROM Album WHERE ArtistId = (SELECT ArtistId FROM Artist WHERE Name = 'AC/DC');\",\n", + " },\n", + " {\n", + " \"input\": \"List all tracks in the 'Rock' genre.\",\n", + " \"query\": \"SELECT * FROM Track WHERE GenreId = (SELECT GenreId FROM Genre WHERE Name = 'Rock');\",\n", + " },\n", + " {\n", + " \"input\": \"Find the total duration of all tracks.\",\n", + " \"query\": \"SELECT SUM(Milliseconds) FROM Track;\",\n", + " },\n", + " {\n", + " \"input\": \"List all customers from Canada.\",\n", + " \"query\": \"SELECT * FROM Customer WHERE Country = 'Canada';\",\n", + " },\n", + " {\n", + " \"input\": \"How many tracks are there in the album with ID 5?\",\n", + " \"query\": \"SELECT COUNT(*) FROM Track WHERE AlbumId = 5;\",\n", + " },\n", + " {\n", + " \"input\": \"Find the total number of invoices.\",\n", + " \"query\": \"SELECT COUNT(*) FROM Invoice;\",\n", + " },\n", + " {\n", + " \"input\": \"List all tracks that are longer than 5 minutes.\",\n", + " \"query\": \"SELECT * FROM Track WHERE Milliseconds > 300000;\",\n", + " },\n", + " {\n", + " \"input\": \"Who are the top 5 customers by total purchase?\",\n", + " \"query\": \"SELECT CustomerId, SUM(Total) AS TotalPurchase FROM Invoice GROUP BY CustomerId ORDER BY TotalPurchase DESC LIMIT 5;\",\n", + " },\n", + " {\n", + " \"input\": \"Which albums are from the year 2000?\",\n", + " \"query\": \"SELECT * FROM Album WHERE strftime('%Y', ReleaseDate) = '2000';\",\n", + " },\n", + " {\n", + " \"input\": \"How many employees are there\",\n", + " \"query\": 'SELECT COUNT(*) FROM \"Employee\"',\n", + " },\n", + "]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can create a few-shot prompt with them like so:" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_core.prompts import FewShotPromptTemplate, PromptTemplate\n", + "\n", + "example_prompt = PromptTemplate.from_template(\"User input: {input}\\nSQL query: {query}\")\n", + "prompt = FewShotPromptTemplate(\n", + " examples=examples[:5],\n", + " example_prompt=example_prompt,\n", + " prefix=\"You are a SQLite expert. Given an input question, create a syntactically correct SQLite query to run. Unless otherwise specificed, do not return more than {top_k} rows.\\n\\nHere is the relevant table info: {table_info}\\n\\nBelow are a number of examples of questions and their corresponding SQL queries.\",\n", + " suffix=\"User input: {input}\\nSQL query: \",\n", + " input_variables=[\"input\", \"top_k\", \"table_info\"],\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "You are a SQLite expert. Given an input question, create a syntactically correct SQLite query to run. Unless otherwise specificed, do not return more than 3 rows.\n", + "\n", + "Here is the relevant table info: foo\n", + "\n", + "Below are a number of examples of questions and their corresponding SQL queries.\n", + "\n", + "User input: List all artists.\n", + "SQL query: SELECT * FROM Artist;\n", + "\n", + "User input: Find all albums for the artist 'AC/DC'.\n", + "SQL query: SELECT * FROM Album WHERE ArtistId = (SELECT ArtistId FROM Artist WHERE Name = 'AC/DC');\n", + "\n", + "User input: List all tracks in the 'Rock' genre.\n", + "SQL query: SELECT * FROM Track WHERE GenreId = (SELECT GenreId FROM Genre WHERE Name = 'Rock');\n", + "\n", + "User input: Find the total duration of all tracks.\n", + "SQL query: SELECT SUM(Milliseconds) FROM Track;\n", + "\n", + "User input: List all customers from Canada.\n", + "SQL query: SELECT * FROM Customer WHERE Country = 'Canada';\n", + "\n", + "User input: How many artists are there?\n", + "SQL query: \n" + ] + } + ], + "source": [ + "print(prompt.format(input=\"How many artists are there?\", top_k=3, table_info=\"foo\"))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Dynamic few-shot examples\n", + "\n", + "If we have enough examples, we may want to only include the most relevant ones in the prompt, either because they don't fit in the model's context window or because the long tail of examples distracts the model. And specifically, given any input we want to include the examples most relevant to that input.\n", + "\n", + "We can do just this using an ExampleSelector. In this case we'll use a [SemanticSimilarityExampleSelector](https://api.python.langchain.com/en/latest/example_selectors/langchain_core.example_selectors.semantic_similarity.SemanticSimilarityExampleSelector.html), which will store the examples in the vector database of our choosing. At runtime it will perform a similarity search between the input and our examples, and return the most semantically similar ones: " + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_community.vectorstores import FAISS\n", + "from langchain_core.example_selectors import SemanticSimilarityExampleSelector\n", + "from langchain_openai import OpenAIEmbeddings\n", + "\n", + "example_selector = SemanticSimilarityExampleSelector.from_examples(\n", + " examples,\n", + " OpenAIEmbeddings(),\n", + " FAISS,\n", + " k=5,\n", + " input_keys=[\"input\"],\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[{'input': 'List all artists.', 'query': 'SELECT * FROM Artist;'},\n", + " {'input': 'How many employees are there',\n", + " 'query': 'SELECT COUNT(*) FROM \"Employee\"'},\n", + " {'input': 'How many tracks are there in the album with ID 5?',\n", + " 'query': 'SELECT COUNT(*) FROM Track WHERE AlbumId = 5;'},\n", + " {'input': 'Which albums are from the year 2000?',\n", + " 'query': \"SELECT * FROM Album WHERE strftime('%Y', ReleaseDate) = '2000';\"},\n", + " {'input': \"List all tracks in the 'Rock' genre.\",\n", + " 'query': \"SELECT * FROM Track WHERE GenreId = (SELECT GenreId FROM Genre WHERE Name = 'Rock');\"}]" + ] + }, + "execution_count": 46, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "example_selector.select_examples({\"input\": \"how many artists are there?\"})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To use it, we can pass the ExampleSelector directly in to our FewShotPromptTemplate:" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": {}, + "outputs": [], + "source": [ + "prompt = FewShotPromptTemplate(\n", + " example_selector=example_selector,\n", + " example_prompt=example_prompt,\n", + " prefix=\"You are a SQLite expert. Given an input question, create a syntactically correct SQLite query to run. Unless otherwise specificed, do not return more than {top_k} rows.\\n\\nHere is the relevant table info: {table_info}\\n\\nBelow are a number of examples of questions and their corresponding SQL queries.\",\n", + " suffix=\"User input: {input}\\nSQL query: \",\n", + " input_variables=[\"input\", \"top_k\", \"table_info\"],\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "You are a SQLite expert. Given an input question, create a syntactically correct SQLite query to run. Unless otherwise specificed, do not return more than 3 rows.\n", + "\n", + "Here is the relevant table info: foo\n", + "\n", + "Below are a number of examples of questions and their corresponding SQL queries.\n", + "\n", + "User input: List all artists.\n", + "SQL query: SELECT * FROM Artist;\n", + "\n", + "User input: How many employees are there\n", + "SQL query: SELECT COUNT(*) FROM \"Employee\"\n", + "\n", + "User input: How many tracks are there in the album with ID 5?\n", + "SQL query: SELECT COUNT(*) FROM Track WHERE AlbumId = 5;\n", + "\n", + "User input: Which albums are from the year 2000?\n", + "SQL query: SELECT * FROM Album WHERE strftime('%Y', ReleaseDate) = '2000';\n", + "\n", + "User input: List all tracks in the 'Rock' genre.\n", + "SQL query: SELECT * FROM Track WHERE GenreId = (SELECT GenreId FROM Genre WHERE Name = 'Rock');\n", + "\n", + "User input: how many artists are there?\n", + "SQL query: \n" + ] + } + ], + "source": [ + "print(prompt.format(input=\"how many artists are there?\", top_k=3, table_info=\"foo\"))" + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'SELECT COUNT(*) FROM Artist;'" + ] + }, + "execution_count": 52, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "chain = create_sql_query_chain(llm, db, prompt)\n", + "chain.invoke({\"question\": \"how many artists are there?\"})" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "poetry-venv", + "language": "python", + "name": "poetry-venv" + }, + "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.9.1" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/docs/use_cases/sql/query_checking.ipynb b/docs/docs/use_cases/sql/query_checking.ipynb new file mode 100644 index 0000000000000..fcb5d44a2ba30 --- /dev/null +++ b/docs/docs/use_cases/sql/query_checking.ipynb @@ -0,0 +1,389 @@ +{ + "cells": [ + { + "cell_type": "raw", + "id": "494149c1-9a1a-4b75-8982-6bb19cc5e14e", + "metadata": {}, + "source": [ + "---\n", + "sidebar_position: 3\n", + "---" + ] + }, + { + "cell_type": "markdown", + "id": "4da7ae91-4973-4e97-a570-fa24024ec65d", + "metadata": {}, + "source": [ + "# Query validation\n", + "\n", + "Perhaps the most error-prone part of any SQL chain or agent is writing valid and safe SQL queries. In this guide we'll go over some strategies for validating our queries and handling invalid queries.\n", + "\n", + "## Setup\n", + "\n", + "First, get required packages and set environment variables:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5d40d5bc-3647-4b5d-808a-db470d40fe7a", + "metadata": {}, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet langchain langchain-community langchain-openai" + ] + }, + { + "cell_type": "markdown", + "id": "c998536a-b1ff-46e7-ac51-dc6deb55d22b", + "metadata": {}, + "source": [ + "We default to OpenAI models in this guide, but you can swap them out for the model provider of your choice." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "71f46270-e1c6-45b4-b36e-ea2e9f860eba", + "metadata": {}, + "outputs": [], + "source": [ + "import getpass\n", + "import os\n", + "\n", + "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass()\n", + "\n", + "# Uncomment the below to use LangSmith. Not required.\n", + "# os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass()\n", + "# os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"" + ] + }, + { + "cell_type": "markdown", + "id": "a0a2151b-cecf-4559-92a1-ca48824fed18", + "metadata": {}, + "source": [ + "The below example will use a SQLite connection with Chinook database. Follow [these installation steps](https://database.guide/2-sample-databases-sqlite/) to create `Chinook.db` in the same directory as this notebook:\n", + "\n", + "* Save [this file](https://raw.githubusercontent.com/lerocha/chinook-database/master/ChinookDatabase/DataSources/Chinook_Sqlite.sql) as `Chinook_Sqlite.sql`\n", + "* Run `sqlite3 Chinook.db`\n", + "* Run `.read Chinook_Sqlite.sql`\n", + "* Test `SELECT * FROM Artist LIMIT 10;`\n", + "\n", + "Now, `Chinhook.db` is in our directory and we can interface with it using the SQLAlchemy-driven `SQLDatabase` class:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "8cedc936-5268-4bfa-b838-bdcc1ee9573c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "sqlite\n", + "['Album', 'Artist', 'Customer', 'Employee', 'Genre', 'Invoice', 'InvoiceLine', 'MediaType', 'Playlist', 'PlaylistTrack', 'Track']\n" + ] + }, + { + "data": { + "text/plain": [ + "\"[(1, 'AC/DC'), (2, 'Accept'), (3, 'Aerosmith'), (4, 'Alanis Morissette'), (5, 'Alice In Chains'), (6, 'Antônio Carlos Jobim'), (7, 'Apocalyptica'), (8, 'Audioslave'), (9, 'BackBeat'), (10, 'Billy Cobham')]\"" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from langchain_community.utilities import SQLDatabase\n", + "\n", + "db = SQLDatabase.from_uri(\"sqlite:///Chinook.db\")\n", + "print(db.dialect)\n", + "print(db.get_usable_table_names())\n", + "db.run(\"SELECT * FROM Artist LIMIT 10;\")" + ] + }, + { + "cell_type": "markdown", + "id": "2d203315-fab7-4621-80da-41e9bf82d803", + "metadata": {}, + "source": [ + "## Query checker\n", + "\n", + "Perhaps the simplest strategy is to ask the model itself to check the original query for common mistakes. Suppose we have the following SQL query chain:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ec66bb76-b1ad-48ad-a7d4-b518e9421b86", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain.chains import create_sql_query_chain\n", + "from langchain_openai import ChatOpenAI\n", + "\n", + "llm = ChatOpenAI(model=\"gpt-3.5-turbo\", temperature=0)\n", + "chain = create_sql_query_chain(llm, db)" + ] + }, + { + "cell_type": "markdown", + "id": "da01023d-cc05-43e3-a38d-ed9d56d3ad15", + "metadata": {}, + "source": [ + "And we want to validate its outputs. We can do so by extending the chain with a second prompt and model call:" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "16686750-d8ee-4c60-8d67-b28281cb6164", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_core.output_parsers import StrOutputParser\n", + "from langchain_core.prompts import ChatPromptTemplate\n", + "\n", + "system = \"\"\"Double check the user's {dialect} query for common mistakes, including:\n", + "- Using NOT IN with NULL values\n", + "- Using UNION when UNION ALL should have been used\n", + "- Using BETWEEN for exclusive ranges\n", + "- Data type mismatch in predicates\n", + "- Properly quoting identifiers\n", + "- Using the correct number of arguments for functions\n", + "- Casting to the correct data type\n", + "- Using the proper columns for joins\n", + "\n", + "If there are any of the above mistakes, rewrite the query. If there are no mistakes, just reproduce the original query.\n", + "\n", + "Output the final SQL query only.\"\"\"\n", + "prompt = ChatPromptTemplate.from_messages(\n", + " [(\"system\", system), (\"human\", \"{query}\")]\n", + ").partial(dialect=db.dialect)\n", + "validation_chain = prompt | llm | StrOutputParser()\n", + "\n", + "full_chain = {\"query\": chain} | validation_chain" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "id": "3a910260-205d-4f4e-afc6-9477572dc947", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"SELECT AVG(Invoice.Total) AS AverageInvoice\\nFROM Invoice\\nJOIN Customer ON Invoice.CustomerId = Customer.CustomerId\\nWHERE Customer.Country = 'USA'\\nAND Customer.Fax IS NULL\\nAND Invoice.InvoiceDate >= '2003-01-01'\\nAND Invoice.InvoiceDate < '2010-01-01'\"" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "query = full_chain.invoke(\n", + " {\n", + " \"question\": \"What's the average Invoice from an American customer whose Fax is missing since 2003 but before 2010\"\n", + " }\n", + ")\n", + "query" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "id": "d01d78b5-89a0-4c12-b743-707ebe64ba86", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'[(6.632999999999998,)]'" + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "db.run(query)" + ] + }, + { + "cell_type": "markdown", + "id": "6e133526-26bd-49da-9cfa-7adc0e59fd72", + "metadata": {}, + "source": [ + "The obvious downside of this approach is that we need to make two model calls instead of one to generate our query. To get around this we can try to perform the query generation and query check in a single model invocation:" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "id": "7af0030a-549e-4e69-9298-3d0a038c2fdd", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "================================\u001b[1m System Message \u001b[0m================================\n", + "\n", + "You are a \u001b[33;1m\u001b[1;3m{dialect}\u001b[0m expert. Given an input question, creat a syntactically correct \u001b[33;1m\u001b[1;3m{dialect}\u001b[0m query to run.\n", + "Unless the user specifies in the question a specific number of examples to obtain, query for at most \u001b[33;1m\u001b[1;3m{top_k}\u001b[0m results using the LIMIT clause as per \u001b[33;1m\u001b[1;3m{dialect}\u001b[0m. You can order the results to return the most informative data in the database.\n", + "Never query for all columns from a table. You must query only the columns that are needed to answer the question. Wrap each column name in double quotes (\") to denote them as delimited identifiers.\n", + "Pay attention to use only the column names you can see in the tables below. Be careful to not query for columns that do not exist. Also, pay attention to which column is in which table.\n", + "Pay attention to use date('now') function to get the current date, if the question involves \"today\".\n", + "\n", + "Only use the following tables:\n", + "\u001b[33;1m\u001b[1;3m{table_info}\u001b[0m\n", + "\n", + "Write an initial draft of the query. Then double check the \u001b[33;1m\u001b[1;3m{dialect}\u001b[0m query for common mistakes, including:\n", + "- Using NOT IN with NULL values\n", + "- Using UNION when UNION ALL should have been used\n", + "- Using BETWEEN for exclusive ranges\n", + "- Data type mismatch in predicates\n", + "- Properly quoting identifiers\n", + "- Using the correct number of arguments for functions\n", + "- Casting to the correct data type\n", + "- Using the proper columns for joins\n", + "\n", + "Use format:\n", + "\n", + "First draft: <>\n", + "Final answer: <>\n", + "\n", + "\n", + "================================\u001b[1m Human Message \u001b[0m=================================\n", + "\n", + "\u001b[33;1m\u001b[1;3m{input}\u001b[0m\n" + ] + } + ], + "source": [ + "system = \"\"\"You are a {dialect} expert. Given an input question, creat a syntactically correct {dialect} query to run.\n", + "Unless the user specifies in the question a specific number of examples to obtain, query for at most {top_k} results using the LIMIT clause as per {dialect}. You can order the results to return the most informative data in the database.\n", + "Never query for all columns from a table. You must query only the columns that are needed to answer the question. Wrap each column name in double quotes (\") to denote them as delimited identifiers.\n", + "Pay attention to use only the column names you can see in the tables below. Be careful to not query for columns that do not exist. Also, pay attention to which column is in which table.\n", + "Pay attention to use date('now') function to get the current date, if the question involves \"today\".\n", + "\n", + "Only use the following tables:\n", + "{table_info}\n", + "\n", + "Write an initial draft of the query. Then double check the {dialect} query for common mistakes, including:\n", + "- Using NOT IN with NULL values\n", + "- Using UNION when UNION ALL should have been used\n", + "- Using BETWEEN for exclusive ranges\n", + "- Data type mismatch in predicates\n", + "- Properly quoting identifiers\n", + "- Using the correct number of arguments for functions\n", + "- Casting to the correct data type\n", + "- Using the proper columns for joins\n", + "\n", + "Use format:\n", + "\n", + "First draft: <>\n", + "Final answer: <>\n", + "\"\"\"\n", + "prompt = ChatPromptTemplate.from_messages([(\"system\", system), (\"human\", \"{input}\")]).partial(dialect=db.dialect)\n", + "\n", + "def parse_final_answer(output: str) -> str:\n", + " return output.split(\"Final answer: \")[1]\n", + " \n", + "chain = create_sql_query_chain(llm, db, prompt=prompt) | parse_final_answer\n", + "prompt.pretty_print()" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "id": "806e27a2-e511-45ea-a4ed-8ce8fa6e1d58", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"\\nSELECT AVG(i.Total) AS AverageInvoice\\nFROM Invoice i\\nJOIN Customer c ON i.CustomerId = c.CustomerId\\nWHERE c.Country = 'USA' AND c.Fax IS NULL AND i.InvoiceDate >= date('2003-01-01') AND i.InvoiceDate < date('2010-01-01')\"" + ] + }, + "execution_count": 46, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "query = chain.invoke(\n", + " {\n", + " \"question\": \"What's the average Invoice from an American customer whose Fax is missing since 2003 but before 2010\"\n", + " }\n", + ")\n", + "query" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "id": "70fff2fa-1f86-4f83-9fd2-e87a5234d329", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'[(6.632999999999998,)]'" + ] + }, + "execution_count": 47, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "db.run(query)" + ] + }, + { + "cell_type": "markdown", + "id": "fc8af115-7c23-421a-8fd7-29bf1b6687a4", + "metadata": {}, + "source": [ + "## Human-in-the-loop\n", + "\n", + "In some cases our data is sensitive enough that we never want to execute a SQL query without a human approving it first. Head to the [Tool use: Human-in-the-loop](/docs/use_cases/tool_use/human_in_the_loop) page to learn how to add a human-in-the-loop to any tool, chain or agent.\n", + "\n", + "## Error handling\n", + "\n", + "At some point, the model will make a mistake and craft an invalid SQL query. Or an issue will arise with our database. Or the model API will go down. We'll want to add some error handling behavior to our chains and agents so that we fail gracefully in these situations, and perhaps even automatically recover. To learn about error handling with tools, head to the [Tool use: Error handling](/docs/use_cases/tool_use/tool_error_handling) page." + ] + } + ], + "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.9.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/docs/use_cases/sql/quickstart.ipynb b/docs/docs/use_cases/sql/quickstart.ipynb new file mode 100644 index 0000000000000..4a0ee60f3ed3a --- /dev/null +++ b/docs/docs/use_cases/sql/quickstart.ipynb @@ -0,0 +1,603 @@ +{ + "cells": [ + { + "cell_type": "raw", + "metadata": {}, + "source": [ + "---\n", + "sidebar_position: 0\n", + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Quickstart\n", + "\n", + "In this guide we'll go over the basic ways to create a Q&A chain and agent over a SQL database. These systems will allow us to ask a question about the data in a SQL database and get back a natural language answer. The main difference between the two is that our agent can query the database in a loop as many time as it needs to answer the question.\n", + "\n", + "## ⚠️ Security note ⚠️\n", + "\n", + "Building Q&A systems of SQL databases requires executing model-generated SQL queries. There are inherent risks in doing this. Make sure that your database connection permissions are always scoped as narrowly as possible for your chain/agent's needs. This will mitigate though not eliminate the risks of building a model-driven system. For more on general security best practices, [see here](/docs/security).\n", + "\n", + "\n", + "## Architecture\n", + "\n", + "At a high-level, the steps of any SQL chain and agent are:\n", + "\n", + "1. **Convert question to SQL query**: Model converts user input to a SQL query.\n", + "2. **Execute SQL query**: Execute the SQL query.\n", + "3. **Answer the question**: Model responds to user input using the query results.\n", + "\n", + "\n", + "![sql_usecase.png](../../../static/img/sql_usecase.png)\n", + "\n", + "## Setup\n", + "\n", + "First, get required packages and set environment variables:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet langchain langchain-community langchain-openai" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We default to OpenAI models in this guide." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import getpass\n", + "import os\n", + "\n", + "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass()\n", + "\n", + "# Uncomment the below to use LangSmith. Not required.\n", + "# os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass()\n", + "# os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The below example will use a SQLite connection with Chinook database. Follow [these installation steps](https://database.guide/2-sample-databases-sqlite/) to create `Chinook.db` in the same directory as this notebook:\n", + "\n", + "* Save [this file](https://raw.githubusercontent.com/lerocha/chinook-database/master/ChinookDatabase/DataSources/Chinook_Sqlite.sql) as `Chinook_Sqlite.sql`\n", + "* Run `sqlite3 Chinook.db`\n", + "* Run `.read Chinook_Sqlite.sql`\n", + "* Test `SELECT * FROM Artist LIMIT 10;`\n", + "\n", + "Now, `Chinhook.db` is in our directory and we can interface with it using the SQLAlchemy-driven `SQLDatabase` class:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "sqlite\n", + "['Album', 'Artist', 'Customer', 'Employee', 'Genre', 'Invoice', 'InvoiceLine', 'MediaType', 'Playlist', 'PlaylistTrack', 'Track']\n" + ] + }, + { + "data": { + "text/plain": [ + "\"[(1, 'AC/DC'), (2, 'Accept'), (3, 'Aerosmith'), (4, 'Alanis Morissette'), (5, 'Alice In Chains'), (6, 'Antônio Carlos Jobim'), (7, 'Apocalyptica'), (8, 'Audioslave'), (9, 'BackBeat'), (10, 'Billy Cobham')]\"" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from langchain_community.utilities import SQLDatabase\n", + "\n", + "db = SQLDatabase.from_uri(\"sqlite:///Chinook.db\")\n", + "print(db.dialect)\n", + "print(db.get_usable_table_names())\n", + "db.run(\"SELECT * FROM Artist LIMIT 10;\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Great! We've got a SQL database that we can query. Now let's try hooking it up to an LLM.\n", + "\n", + "## Chain\n", + "\n", + "Let's create a simple chain that takes a question, turns it into a SQL query, executes the query, and uses the result to answer the original question.\n", + "\n", + "### Convert question to SQL query\n", + "\n", + "The first step in a SQL chain or agent is to take the user input and convert it to a SQL query. LangChain comes with a built-in chain for this: [create_sql_query_chain](https://api.python.langchain.com/en/latest/chains/langchain.chains.sql_database.query.create_sql_query_chain.html)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'SELECT COUNT(*) FROM Employee'" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from langchain.chains import create_sql_query_chain\n", + "from langchain_openai import ChatOpenAI\n", + "\n", + "llm = ChatOpenAI(model=\"gpt-3.5-turbo\", temperature=0)\n", + "chain = create_sql_query_chain(llm, db)\n", + "response = chain.invoke({\"question\": \"How many employees are there\"})\n", + "response" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can execute the query to make sure it's valid:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'[(8,)]'" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "db.run(response)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can look at the [LangSmith trace](https://smith.langchain.com/public/c8fa52ea-be46-4829-bde2-52894970b830/r) to get a better understanding of what this chain is doing. We can also inspect the chain directly for its prompts. Looking at the prompt (below), we can see that it is:\n", + "\n", + "* Dialect-specific. In this case it references SQLite explicitly.\n", + "* Has definitions for all the available tables.\n", + "* Has three examples rows for each table.\n", + "\n", + "This technique is inspired by papers like [this](https://arxiv.org/pdf/2204.00498.pdf), which suggest showing examples rows and being explicit about tables improves performance. We can also in" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "You are a SQLite expert. Given an input question, first create a syntactically correct SQLite query to run, then look at the results of the query and return the answer to the input question.\n", + "Unless the user specifies in the question a specific number of examples to obtain, query for at most 5 results using the LIMIT clause as per SQLite. You can order the results to return the most informative data in the database.\n", + "Never query for all columns from a table. You must query only the columns that are needed to answer the question. Wrap each column name in double quotes (\") to denote them as delimited identifiers.\n", + "Pay attention to use only the column names you can see in the tables below. Be careful to not query for columns that do not exist. Also, pay attention to which column is in which table.\n", + "Pay attention to use date('now') function to get the current date, if the question involves \"today\".\n", + "\n", + "Use the following format:\n", + "\n", + "Question: Question here\n", + "SQLQuery: SQL Query to run\n", + "SQLResult: Result of the SQLQuery\n", + "Answer: Final answer here\n", + "\n", + "Only use the following tables:\n", + "\u001b[33;1m\u001b[1;3m{table_info}\u001b[0m\n", + "\n", + "Question: \u001b[33;1m\u001b[1;3m{input}\u001b[0m\n" + ] + } + ], + "source": [ + "chain.get_prompts()[0].pretty_print()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Execute SQL query\n", + "\n", + "Now that we've generated a SQL query, we'll want to execute it. **This is the most dangerous part of creating a SQL chain.** Consider carefully if it is OK to run automated queries over your data. Minimize the database connection permissions as much as possible. Consider adding a human approval step to you chains before query execution (see below).\n", + "\n", + "We can use the `QuerySQLDatabaseTool` to easily add query execution to our chain:" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'[(8,)]'" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from langchain_community.tools.sql_database.tool import QuerySQLDataBaseTool\n", + "\n", + "execute_query = QuerySQLDataBaseTool(db=db)\n", + "write_query = create_sql_query_chain(llm, db)\n", + "chain = write_query | execute_query\n", + "chain.invoke({\"question\": \"How many employees are there\"})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Answer the question\n", + "\n", + "Now that we've got a way to automatically generate and execute queries, we just need to combine the original question and SQL query result to generate a final answer. We can do this by passing question and result to the LLM once more:" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'There are 8 employees.'" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from operator import itemgetter\n", + "\n", + "from langchain_core.output_parsers import StrOutputParser\n", + "from langchain_core.prompts import PromptTemplate\n", + "from langchain_core.runnables import RunnablePassthrough\n", + "\n", + "answer_prompt = PromptTemplate.from_template(\n", + " \"\"\"Given the following user question, corresponding SQL query, and SQL result, answer the user question.\n", + "\n", + "Question: {question}\n", + "SQL Query: {query}\n", + "SQL Result: {result}\n", + "Answer: \"\"\"\n", + ")\n", + "\n", + "answer = answer_prompt | llm | StrOutputParser()\n", + "chain = (\n", + " RunnablePassthrough.assign(query=write_query).assign(\n", + " result=itemgetter(\"query\") | execute_query\n", + " )\n", + " | answer\n", + ")\n", + "\n", + "chain.invoke({\"question\": \"How many employees are there\"})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Next steps\n", + "\n", + "For more complex query-generation, we may want to create few-shot prompts or add query-checking steps. For advanced techniques like this and more check out:\n", + "\n", + "* [Prompting strategies](/docs/use_cases/sql/prompting): Advanced prompt engineering techniques.\n", + "* [Query checking](/docs/use_cases/sql/query_checking): Add query validation and error handling.\n", + "* [Large databses](/docs/use_cases/sql/large_db): Techniques for working with large databases." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Agents\n", + "\n", + "LangChain has an SQL Agent which provides a more flexible way of interacting with SQL databases. The main advantages of using the SQL Agent are:\n", + "\n", + "- It can answer questions based on the databases' schema as well as on the databases' content (like describing a specific table).\n", + "- It can recover from errors by running a generated query, catching the traceback and regenerating it correctly.\n", + "- It can answer questions that require multiple dependent queries.\n", + "\n", + "To initialize the agent, we use `create_sql_agent` function. This agent contains the `SQLDatabaseToolkit` which contains tools to: \n", + "\n", + "* Create and execute queries\n", + "* Check query syntax\n", + "* Retrieve table descriptions\n", + "* ... and more\n", + "\n", + "### Initializing agent" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_community.agent_toolkits import create_sql_agent\n", + "\n", + "agent_executor = create_sql_agent(llm, db=db, agent_type=\"openai-tools\", verbose=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3m\n", + "Invoking: `sql_db_list_tables` with `{}`\n", + "\n", + "\n", + "\u001b[0m\u001b[38;5;200m\u001b[1;3mAlbum, Artist, Customer, Employee, Genre, Invoice, InvoiceLine, MediaType, Playlist, PlaylistTrack, Track\u001b[0m\u001b[32;1m\u001b[1;3m\n", + "Invoking: `sql_db_schema` with `Invoice,Customer`\n", + "\n", + "\n", + "\u001b[0m\u001b[33;1m\u001b[1;3m\n", + "CREATE TABLE \"Customer\" (\n", + "\t\"CustomerId\" INTEGER NOT NULL, \n", + "\t\"FirstName\" NVARCHAR(40) NOT NULL, \n", + "\t\"LastName\" NVARCHAR(20) NOT NULL, \n", + "\t\"Company\" NVARCHAR(80), \n", + "\t\"Address\" NVARCHAR(70), \n", + "\t\"City\" NVARCHAR(40), \n", + "\t\"State\" NVARCHAR(40), \n", + "\t\"Country\" NVARCHAR(40), \n", + "\t\"PostalCode\" NVARCHAR(10), \n", + "\t\"Phone\" NVARCHAR(24), \n", + "\t\"Fax\" NVARCHAR(24), \n", + "\t\"Email\" NVARCHAR(60) NOT NULL, \n", + "\t\"SupportRepId\" INTEGER, \n", + "\tPRIMARY KEY (\"CustomerId\"), \n", + "\tFOREIGN KEY(\"SupportRepId\") REFERENCES \"Employee\" (\"EmployeeId\")\n", + ")\n", + "\n", + "/*\n", + "3 rows from Customer table:\n", + "CustomerId\tFirstName\tLastName\tCompany\tAddress\tCity\tState\tCountry\tPostalCode\tPhone\tFax\tEmail\tSupportRepId\n", + "1\tLuís\tGonçalves\tEmbraer - Empresa Brasileira de Aeronáutica S.A.\tAv. Brigadeiro Faria Lima, 2170\tSão José dos Campos\tSP\tBrazil\t12227-000\t+55 (12) 3923-5555\t+55 (12) 3923-5566\tluisg@embraer.com.br\t3\n", + "2\tLeonie\tKöhler\tNone\tTheodor-Heuss-Straße 34\tStuttgart\tNone\tGermany\t70174\t+49 0711 2842222\tNone\tleonekohler@surfeu.de\t5\n", + "3\tFrançois\tTremblay\tNone\t1498 rue Bélanger\tMontréal\tQC\tCanada\tH2G 1A7\t+1 (514) 721-4711\tNone\tftremblay@gmail.com\t3\n", + "*/\n", + "\n", + "\n", + "CREATE TABLE \"Invoice\" (\n", + "\t\"InvoiceId\" INTEGER NOT NULL, \n", + "\t\"CustomerId\" INTEGER NOT NULL, \n", + "\t\"InvoiceDate\" DATETIME NOT NULL, \n", + "\t\"BillingAddress\" NVARCHAR(70), \n", + "\t\"BillingCity\" NVARCHAR(40), \n", + "\t\"BillingState\" NVARCHAR(40), \n", + "\t\"BillingCountry\" NVARCHAR(40), \n", + "\t\"BillingPostalCode\" NVARCHAR(10), \n", + "\t\"Total\" NUMERIC(10, 2) NOT NULL, \n", + "\tPRIMARY KEY (\"InvoiceId\"), \n", + "\tFOREIGN KEY(\"CustomerId\") REFERENCES \"Customer\" (\"CustomerId\")\n", + ")\n", + "\n", + "/*\n", + "3 rows from Invoice table:\n", + "InvoiceId\tCustomerId\tInvoiceDate\tBillingAddress\tBillingCity\tBillingState\tBillingCountry\tBillingPostalCode\tTotal\n", + "1\t2\t2009-01-01 00:00:00\tTheodor-Heuss-Straße 34\tStuttgart\tNone\tGermany\t70174\t1.98\n", + "2\t4\t2009-01-02 00:00:00\tUllevålsveien 14\tOslo\tNone\tNorway\t0171\t3.96\n", + "3\t8\t2009-01-03 00:00:00\tGrétrystraat 63\tBrussels\tNone\tBelgium\t1000\t5.94\n", + "*/\u001b[0m\u001b[32;1m\u001b[1;3m\n", + "Invoking: `sql_db_query` with `SELECT c.Country, SUM(i.Total) AS TotalSales FROM Invoice i JOIN Customer c ON i.CustomerId = c.CustomerId GROUP BY c.Country ORDER BY TotalSales DESC LIMIT 10;`\n", + "responded: To list the total sales per country, I can query the \"Invoice\" and \"Customer\" tables. I will join these tables on the \"CustomerId\" column and group the results by the \"BillingCountry\" column. Then, I will calculate the sum of the \"Total\" column to get the total sales per country. Finally, I will order the results in descending order of the total sales.\n", + "\n", + "Here is the SQL query:\n", + "\n", + "```sql\n", + "SELECT c.Country, SUM(i.Total) AS TotalSales\n", + "FROM Invoice i\n", + "JOIN Customer c ON i.CustomerId = c.CustomerId\n", + "GROUP BY c.Country\n", + "ORDER BY TotalSales DESC\n", + "LIMIT 10;\n", + "```\n", + "\n", + "Now, I will execute this query to get the total sales per country.\n", + "\n", + "\u001b[0m\u001b[36;1m\u001b[1;3m[('USA', 523.0600000000003), ('Canada', 303.9599999999999), ('France', 195.09999999999994), ('Brazil', 190.09999999999997), ('Germany', 156.48), ('United Kingdom', 112.85999999999999), ('Czech Republic', 90.24000000000001), ('Portugal', 77.23999999999998), ('India', 75.25999999999999), ('Chile', 46.62)]\u001b[0m\u001b[32;1m\u001b[1;3mThe total sales per country are as follows:\n", + "\n", + "1. USA: $523.06\n", + "2. Canada: $303.96\n", + "3. France: $195.10\n", + "4. Brazil: $190.10\n", + "5. Germany: $156.48\n", + "6. United Kingdom: $112.86\n", + "7. Czech Republic: $90.24\n", + "8. Portugal: $77.24\n", + "9. India: $75.26\n", + "10. Chile: $46.62\n", + "\n", + "To answer the second question, the country whose customers spent the most is the USA, with a total sales of $523.06.\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "{'input': \"List the total sales per country. Which country's customers spent the most?\",\n", + " 'output': 'The total sales per country are as follows:\\n\\n1. USA: $523.06\\n2. Canada: $303.96\\n3. France: $195.10\\n4. Brazil: $190.10\\n5. Germany: $156.48\\n6. United Kingdom: $112.86\\n7. Czech Republic: $90.24\\n8. Portugal: $77.24\\n9. India: $75.26\\n10. Chile: $46.62\\n\\nTo answer the second question, the country whose customers spent the most is the USA, with a total sales of $523.06.'}" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "agent_executor.invoke(\n", + " {\n", + " \"input\": \"List the total sales per country. Which country's customers spent the most?\"\n", + " }\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3m\n", + "Invoking: `sql_db_list_tables` with `{}`\n", + "\n", + "\n", + "\u001b[0m\u001b[38;5;200m\u001b[1;3mAlbum, Artist, Customer, Employee, Genre, Invoice, InvoiceLine, MediaType, Playlist, PlaylistTrack, Track\u001b[0m\u001b[32;1m\u001b[1;3m\n", + "Invoking: `sql_db_schema` with `PlaylistTrack`\n", + "\n", + "\n", + "\u001b[0m\u001b[33;1m\u001b[1;3m\n", + "CREATE TABLE \"PlaylistTrack\" (\n", + "\t\"PlaylistId\" INTEGER NOT NULL, \n", + "\t\"TrackId\" INTEGER NOT NULL, \n", + "\tPRIMARY KEY (\"PlaylistId\", \"TrackId\"), \n", + "\tFOREIGN KEY(\"TrackId\") REFERENCES \"Track\" (\"TrackId\"), \n", + "\tFOREIGN KEY(\"PlaylistId\") REFERENCES \"Playlist\" (\"PlaylistId\")\n", + ")\n", + "\n", + "/*\n", + "3 rows from PlaylistTrack table:\n", + "PlaylistId\tTrackId\n", + "1\t3402\n", + "1\t3389\n", + "1\t3390\n", + "*/\u001b[0m\u001b[32;1m\u001b[1;3mThe `PlaylistTrack` table has two columns: `PlaylistId` and `TrackId`. It is a junction table that represents the many-to-many relationship between playlists and tracks. \n", + "\n", + "Here is the schema of the `PlaylistTrack` table:\n", + "\n", + "```\n", + "CREATE TABLE \"PlaylistTrack\" (\n", + "\t\"PlaylistId\" INTEGER NOT NULL, \n", + "\t\"TrackId\" INTEGER NOT NULL, \n", + "\tPRIMARY KEY (\"PlaylistId\", \"TrackId\"), \n", + "\tFOREIGN KEY(\"TrackId\") REFERENCES \"Track\" (\"TrackId\"), \n", + "\tFOREIGN KEY(\"PlaylistId\") REFERENCES \"Playlist\" (\"PlaylistId\")\n", + ")\n", + "```\n", + "\n", + "The `PlaylistId` column is a foreign key referencing the `PlaylistId` column in the `Playlist` table. The `TrackId` column is a foreign key referencing the `TrackId` column in the `Track` table.\n", + "\n", + "Here are three sample rows from the `PlaylistTrack` table:\n", + "\n", + "```\n", + "PlaylistId TrackId\n", + "1 3402\n", + "1 3389\n", + "1 3390\n", + "```\n", + "\n", + "Please let me know if there is anything else I can help with.\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "{'input': 'Describe the playlisttrack table',\n", + " 'output': 'The `PlaylistTrack` table has two columns: `PlaylistId` and `TrackId`. It is a junction table that represents the many-to-many relationship between playlists and tracks. \\n\\nHere is the schema of the `PlaylistTrack` table:\\n\\n```\\nCREATE TABLE \"PlaylistTrack\" (\\n\\t\"PlaylistId\" INTEGER NOT NULL, \\n\\t\"TrackId\" INTEGER NOT NULL, \\n\\tPRIMARY KEY (\"PlaylistId\", \"TrackId\"), \\n\\tFOREIGN KEY(\"TrackId\") REFERENCES \"Track\" (\"TrackId\"), \\n\\tFOREIGN KEY(\"PlaylistId\") REFERENCES \"Playlist\" (\"PlaylistId\")\\n)\\n```\\n\\nThe `PlaylistId` column is a foreign key referencing the `PlaylistId` column in the `Playlist` table. The `TrackId` column is a foreign key referencing the `TrackId` column in the `Track` table.\\n\\nHere are three sample rows from the `PlaylistTrack` table:\\n\\n```\\nPlaylistId TrackId\\n1 3402\\n1 3389\\n1 3390\\n```\\n\\nPlease let me know if there is anything else I can help with.'}" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "agent_executor.invoke({\"input\": \"Describe the playlisttrack table\"})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Next steps\n", + "\n", + "For more on how to use and customize agents head to the [Agents](/docs/use_cases/sql/agents) page." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "poetry-venv", + "language": "python", + "name": "poetry-venv" + }, + "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.9.1" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/docs/use_cases/tool_use/agents.ipynb b/docs/docs/use_cases/tool_use/agents.ipynb new file mode 100644 index 0000000000000..77cbef71d6658 --- /dev/null +++ b/docs/docs/use_cases/tool_use/agents.ipynb @@ -0,0 +1,289 @@ +{ + "cells": [ + { + "cell_type": "raw", + "id": "7b68af90-bfab-4407-93b6-d084cf948b4b", + "metadata": {}, + "source": [ + "---\n", + "sidebar_position: 1\n", + "---" + ] + }, + { + "cell_type": "markdown", + "id": "1925a807-fa01-44bc-8a03-d9907311c7f9", + "metadata": { + "jp-MarkdownHeadingCollapsed": true + }, + "source": [ + "## Agents\n", + "\n", + "Chains are great when we know the specific sequence of tool usage needed for any user input. But for certain use cases, how many times we use tools depends on the input. In these cases, we want to let the model itself decide how many times to use tools and in what order. [Agents](/docs/modules/agents/) let us do just this.\n", + "\n", + "LangChain comes with a number of built-in agents that are optimized for different use cases. Read about all the [agent types here](/docs/modules/agents/agent_types/).\n", + "\n", + "As an example, let's try out the OpenAI tools agent, which makes use of the new OpenAI tool-calling API (this is only available in the latest OpenAI models, and differs from function-calling in that the model can return multiple function invocations at once).\n", + "\n", + "![agent](../../../static/img/tool_agent.svg)" + ] + }, + { + "cell_type": "markdown", + "id": "c224a321-2f5a-410c-b466-a10d0199bad8", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "We'll need to install the following packages:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f6303995-a8f7-4504-8b29-e227683f375e", + "metadata": {}, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet langchain langchain-openai" + ] + }, + { + "cell_type": "markdown", + "id": "a33915ce-00c5-4379-8a83-c0053e471cdb", + "metadata": {}, + "source": [ + "And set these environment variables:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "54667a49-c226-486d-a887-33120c90cc91", + "metadata": {}, + "outputs": [], + "source": [ + "import getpass\n", + "import os\n", + "\n", + "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass()\n", + "\n", + "# If you'd like to use LangSmith, uncomment the below\n", + "# os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "# os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass()" + ] + }, + { + "cell_type": "markdown", + "id": "aaaad3ad-085b-494e-84aa-9cb3e983c80b", + "metadata": {}, + "source": [ + "## Create tools\n", + "\n", + "First, we need to create some tool to call. For this example, we will create custom tools from functions. For more information on creating custom tools, please see [this guide](/docs/modules/agents/tools/)." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "1c44ba79-6ab2-4d55-8247-82fca4d9b70c", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_core.tools import tool\n", + "\n", + "\n", + "@tool\n", + "def multiply(first_int: int, second_int: int) -> int:\n", + " \"\"\"Multiply two integers together.\"\"\"\n", + " return first_int * second_int\n", + "\n", + "\n", + "@tool\n", + "def add(first_int: int, second_int: int) -> int:\n", + " \"Add two integers.\"\n", + " return first_int + second_int\n", + "\n", + "\n", + "@tool\n", + "def exponentiate(base: int, exponent: int) -> int:\n", + " \"Exponentiate the base to the exponent power.\"\n", + " return base**exponent\n", + "\n", + "\n", + "tools = [multiply, add, exponentiate]" + ] + }, + { + "cell_type": "markdown", + "id": "a3d0c8ca-72bd-4187-b1e6-f5eef92eeb52", + "metadata": {}, + "source": [ + "## Create prompt" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "e27a4e1a-938b-4b60-8e32-25e4ee530274", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain import hub\n", + "from langchain.agents import AgentExecutor, create_openai_tools_agent\n", + "from langchain_openai import ChatOpenAI" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "bcc9536e-0328-4e29-9d3d-133f3e63e589", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "================================\u001b[1m System Message \u001b[0m================================\n", + "\n", + "You are a helpful assistant\n", + "\n", + "=============================\u001b[1m Messages Placeholder \u001b[0m=============================\n", + "\n", + "\u001b[33;1m\u001b[1;3m{chat_history}\u001b[0m\n", + "\n", + "================================\u001b[1m Human Message \u001b[0m=================================\n", + "\n", + "\u001b[33;1m\u001b[1;3m{input}\u001b[0m\n", + "\n", + "=============================\u001b[1m Messages Placeholder \u001b[0m=============================\n", + "\n", + "\u001b[33;1m\u001b[1;3m{agent_scratchpad}\u001b[0m\n" + ] + } + ], + "source": [ + "# Get the prompt to use - you can modify this!\n", + "prompt = hub.pull(\"hwchase17/openai-tools-agent\")\n", + "prompt.pretty_print()" + ] + }, + { + "cell_type": "markdown", + "id": "85e9875a-d8d4-4712-b3f0-b513c684451b", + "metadata": {}, + "source": [ + "## Create agent" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "a1c5319d-6609-449d-8dd0-127e9a600656", + "metadata": {}, + "outputs": [], + "source": [ + "# Choose the LLM that will drive the agent\n", + "# Only certain models support this\n", + "model = ChatOpenAI(model=\"gpt-3.5-turbo-1106\", temperature=0)\n", + "\n", + "# Construct the OpenAI Tools agent\n", + "agent = create_openai_tools_agent(model, tools, prompt)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "c86bfe50-c5b3-49ed-86c8-1fe8dcd0c83a", + "metadata": {}, + "outputs": [], + "source": [ + "# Create an agent executor by passing in the agent and tools\n", + "agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)" + ] + }, + { + "cell_type": "markdown", + "id": "448d5ef2-9820-44d0-96d3-ff1d648e4b01", + "metadata": {}, + "source": [ + "## Invoke agent" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "c098f8df-fd7f-4c13-963a-8e34194d3f84", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3m\n", + "Invoking: `exponentiate` with `{'base': 3, 'exponent': 5}`\n", + "\n", + "\n", + "\u001b[0m\u001b[38;5;200m\u001b[1;3m243\u001b[0m\u001b[32;1m\u001b[1;3m\n", + "Invoking: `add` with `{'first_int': 12, 'second_int': 3}`\n", + "\n", + "\n", + "\u001b[0m\u001b[33;1m\u001b[1;3m15\u001b[0m\u001b[32;1m\u001b[1;3m\n", + "Invoking: `multiply` with `{'first_int': 243, 'second_int': 15}`\n", + "\n", + "\n", + "\u001b[0m\u001b[36;1m\u001b[1;3m3645\u001b[0m\u001b[32;1m\u001b[1;3m\n", + "Invoking: `exponentiate` with `{'base': 3645, 'exponent': 2}`\n", + "\n", + "\n", + "\u001b[0m\u001b[38;5;200m\u001b[1;3m13286025\u001b[0m\u001b[32;1m\u001b[1;3mThe result of raising 3 to the fifth power and multiplying that by the sum of twelve and three, then squaring the whole result is 13,286,025.\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "{'input': 'Take 3 to the fifth power and multiply that by the sum of twelve and three, then square the whole result',\n", + " 'output': 'The result of raising 3 to the fifth power and multiplying that by the sum of twelve and three, then squaring the whole result is 13,286,025.'}" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "agent_executor.invoke(\n", + " {\n", + " \"input\": \"Take 3 to the fifth power and multiply that by the sum of twelve and three, then square the whole result\"\n", + " }\n", + ")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "poetry-venv", + "language": "python", + "name": "poetry-venv" + }, + "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.9.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/docs/use_cases/tool_use/human_in_the_loop.ipynb b/docs/docs/use_cases/tool_use/human_in_the_loop.ipynb new file mode 100644 index 0000000000000..137b3f5310406 --- /dev/null +++ b/docs/docs/use_cases/tool_use/human_in_the_loop.ipynb @@ -0,0 +1,274 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "b09b745d-f006-4ecc-8772-afa266c43605", + "metadata": {}, + "source": [ + "# Human-in-the-loop\n", + "\n", + "There are certain tools that we don't trust a model to execute on its own. One thing we can do in such situations is require human approval before the tool is invoked." + ] + }, + { + "cell_type": "markdown", + "id": "09178c30-a633-4d7b-88ea-092316f14b6f", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "We'll need to install the following packages:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e44bec05-9aa4-47b1-a660-c0a183533598", + "metadata": {}, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet langchain langchain-openai" + ] + }, + { + "cell_type": "markdown", + "id": "f09629b6-7f62-4879-a791-464739ca6b6b", + "metadata": {}, + "source": [ + "And set these environment variables:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2bed0ccf-20cc-4fd3-9947-55471dd8c4da", + "metadata": {}, + "outputs": [], + "source": [ + "import getpass\n", + "import os\n", + "\n", + "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass()\n", + "\n", + "# If you'd like to use LangSmith, uncomment the below:\n", + "# os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "# os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass()" + ] + }, + { + "cell_type": "markdown", + "id": "43721981-4595-4721-bea0-5c67696426d3", + "metadata": {}, + "source": [ + "## Chain\n", + "\n", + "Suppose we have the following (dummy) tools and tool-calling chain:" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "0221fdfd-2a18-4449-a123-e6b0b15bb3d9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[{'type': 'count_emails', 'args': {'last_n_days': 5}, 'output': 10}]" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from operator import itemgetter\n", + "\n", + "from langchain.output_parsers import JsonOutputToolsParser\n", + "from langchain_community.tools.convert_to_openai import format_tool_to_openai_tool\n", + "from langchain_core.runnables import Runnable, RunnableLambda, RunnablePassthrough\n", + "from langchain_core.tools import tool\n", + "from langchain_openai import ChatOpenAI\n", + "\n", + "\n", + "@tool\n", + "def count_emails(last_n_days: int) -> int:\n", + " \"\"\"Multiply two integers together.\"\"\"\n", + " return last_n_days * 2\n", + "\n", + "\n", + "@tool\n", + "def send_email(message: str, recipient: str) -> str:\n", + " \"Add two integers.\"\n", + " return f\"Successfully sent email to {recipient}.\"\n", + "\n", + "\n", + "tools = [count_emails, send_email]\n", + "model = ChatOpenAI(model=\"gpt-3.5-turbo\", temperature=0).bind(\n", + " tools=[format_tool_to_openai_tool(t) for t in tools]\n", + ")\n", + "\n", + "\n", + "def call_tool(tool_invocation: dict) -> Runnable:\n", + " \"\"\"Function for dynamically constructing the end of the chain based on the model-selected tool.\"\"\"\n", + " tool_map = {tool.name: tool for tool in tools}\n", + " tool = tool_map[tool_invocation[\"type\"]]\n", + " return RunnablePassthrough.assign(output=itemgetter(\"args\") | tool)\n", + "\n", + "\n", + "# .map() allows us to apply a function to a list of inputs.\n", + "call_tool_list = RunnableLambda(call_tool).map()\n", + "chain = model | JsonOutputToolsParser() | call_tool_list\n", + "chain.invoke(\"how many emails did i get in the last 5 days?\")" + ] + }, + { + "cell_type": "markdown", + "id": "258c1c7b-a765-4558-93fe-d0defbc29223", + "metadata": {}, + "source": [ + "## Adding human approval\n", + "\n", + "We can add a simple human approval step to our tool_chain function:" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "341fb055-0315-47bc-8f72-ed6103d2981f", + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "\n", + "\n", + "def human_approval(tool_invocations: list) -> Runnable:\n", + " tool_strs = \"\\n\\n\".join(\n", + " json.dumps(tool_call, indent=2) for tool_call in tool_invocations\n", + " )\n", + " msg = (\n", + " f\"Do you approve of the following tool invocations\\n\\n{tool_strs}\\n\\n\"\n", + " \"Anything except 'Y'/'Yes' (case-insensitive) will be treated as a no.\"\n", + " )\n", + " resp = input(msg)\n", + " if resp.lower() not in (\"yes\", \"y\"):\n", + " raise ValueError(f\"Tool invocations not approved:\\n\\n{tool_strs}\")\n", + " return tool_invocations" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "id": "25dca07b-56ca-4b94-9955-d4f3e9895e03", + "metadata": {}, + "outputs": [ + { + "name": "stdin", + "output_type": "stream", + "text": [ + "Do you approve of the following tool invocations\n", + "\n", + "{\n", + " \"type\": \"count_emails\",\n", + " \"args\": {\n", + " \"last_n_days\": 5\n", + " }\n", + "}\n", + "\n", + "Anything except 'Y'/'Yes' (case-insensitive) will be treated as a no. y\n" + ] + }, + { + "data": { + "text/plain": [ + "[{'type': 'count_emails', 'args': {'last_n_days': 5}, 'output': 10}]" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "chain = model | JsonOutputToolsParser() | human_approval | call_tool_list\n", + "chain.invoke(\"how many emails did i get in the last 5 days?\")" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "id": "f558f2cd-847b-4ef9-a770-3961082b540c", + "metadata": {}, + "outputs": [ + { + "name": "stdin", + "output_type": "stream", + "text": [ + "Do you approve of the following tool invocations\n", + "\n", + "{\n", + " \"type\": \"send_email\",\n", + " \"args\": {\n", + " \"message\": \"What's up homie\",\n", + " \"recipient\": \"sally@gmail.com\"\n", + " }\n", + "}\n", + "\n", + "Anything except 'Y'/'Yes' (case-insensitive) will be treated as a no. no\n" + ] + }, + { + "ename": "ValueError", + "evalue": "Tool invocations not approved:\n\n{\n \"type\": \"send_email\",\n \"args\": {\n \"message\": \"What's up homie\",\n \"recipient\": \"sally@gmail.com\"\n }\n}", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[32], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mchain\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43minvoke\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mSend sally@gmail.com an email saying \u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mWhat\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43ms up homie\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/langchain/libs/core/langchain_core/runnables/base.py:1774\u001b[0m, in \u001b[0;36mRunnableSequence.invoke\u001b[0;34m(self, input, config)\u001b[0m\n\u001b[1;32m 1772\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m 1773\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m i, step \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28menumerate\u001b[39m(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39msteps):\n\u001b[0;32m-> 1774\u001b[0m \u001b[38;5;28minput\u001b[39m \u001b[38;5;241m=\u001b[39m \u001b[43mstep\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43minvoke\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 1775\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43minput\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1776\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;66;43;03m# mark each step as a child run\u001b[39;49;00m\n\u001b[1;32m 1777\u001b[0m \u001b[43m \u001b[49m\u001b[43mpatch_config\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 1778\u001b[0m \u001b[43m \u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mcallbacks\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mrun_manager\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget_child\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43mf\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mseq:step:\u001b[39;49m\u001b[38;5;132;43;01m{\u001b[39;49;00m\u001b[43mi\u001b[49m\u001b[38;5;241;43m+\u001b[39;49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[38;5;132;43;01m}\u001b[39;49;00m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[1;32m 1779\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1780\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 1781\u001b[0m \u001b[38;5;66;03m# finish the root run\u001b[39;00m\n\u001b[1;32m 1782\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mBaseException\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m e:\n", + "File \u001b[0;32m~/langchain/libs/core/langchain_core/runnables/base.py:3074\u001b[0m, in \u001b[0;36mRunnableLambda.invoke\u001b[0;34m(self, input, config, **kwargs)\u001b[0m\n\u001b[1;32m 3072\u001b[0m \u001b[38;5;250m\u001b[39m\u001b[38;5;124;03m\"\"\"Invoke this runnable synchronously.\"\"\"\u001b[39;00m\n\u001b[1;32m 3073\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mhasattr\u001b[39m(\u001b[38;5;28mself\u001b[39m, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mfunc\u001b[39m\u001b[38;5;124m\"\u001b[39m):\n\u001b[0;32m-> 3074\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_call_with_config\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 3075\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_invoke\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 3076\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43minput\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 3077\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_config\u001b[49m\u001b[43m(\u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mfunc\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 3078\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 3079\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 3080\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[1;32m 3081\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mTypeError\u001b[39;00m(\n\u001b[1;32m 3082\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mCannot invoke a coroutine function synchronously.\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 3083\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mUse `ainvoke` instead.\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 3084\u001b[0m )\n", + "File \u001b[0;32m~/langchain/libs/core/langchain_core/runnables/base.py:975\u001b[0m, in \u001b[0;36mRunnable._call_with_config\u001b[0;34m(self, func, input, config, run_type, **kwargs)\u001b[0m\n\u001b[1;32m 971\u001b[0m context \u001b[38;5;241m=\u001b[39m copy_context()\n\u001b[1;32m 972\u001b[0m context\u001b[38;5;241m.\u001b[39mrun(var_child_runnable_config\u001b[38;5;241m.\u001b[39mset, child_config)\n\u001b[1;32m 973\u001b[0m output \u001b[38;5;241m=\u001b[39m cast(\n\u001b[1;32m 974\u001b[0m Output,\n\u001b[0;32m--> 975\u001b[0m \u001b[43mcontext\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrun\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 976\u001b[0m \u001b[43m \u001b[49m\u001b[43mcall_func_with_variable_args\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 977\u001b[0m \u001b[43m \u001b[49m\u001b[43mfunc\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;66;43;03m# type: ignore[arg-type]\u001b[39;49;00m\n\u001b[1;32m 978\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43minput\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;66;43;03m# type: ignore[arg-type]\u001b[39;49;00m\n\u001b[1;32m 979\u001b[0m \u001b[43m \u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 980\u001b[0m \u001b[43m \u001b[49m\u001b[43mrun_manager\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 981\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 982\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m,\n\u001b[1;32m 983\u001b[0m )\n\u001b[1;32m 984\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mBaseException\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m e:\n\u001b[1;32m 985\u001b[0m run_manager\u001b[38;5;241m.\u001b[39mon_chain_error(e)\n", + "File \u001b[0;32m~/langchain/libs/core/langchain_core/runnables/config.py:323\u001b[0m, in \u001b[0;36mcall_func_with_variable_args\u001b[0;34m(func, input, config, run_manager, **kwargs)\u001b[0m\n\u001b[1;32m 321\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m run_manager \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m \u001b[38;5;129;01mand\u001b[39;00m accepts_run_manager(func):\n\u001b[1;32m 322\u001b[0m kwargs[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mrun_manager\u001b[39m\u001b[38;5;124m\"\u001b[39m] \u001b[38;5;241m=\u001b[39m run_manager\n\u001b[0;32m--> 323\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43minput\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/langchain/libs/core/langchain_core/runnables/base.py:2950\u001b[0m, in \u001b[0;36mRunnableLambda._invoke\u001b[0;34m(self, input, run_manager, config, **kwargs)\u001b[0m\n\u001b[1;32m 2948\u001b[0m output \u001b[38;5;241m=\u001b[39m chunk\n\u001b[1;32m 2949\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m-> 2950\u001b[0m output \u001b[38;5;241m=\u001b[39m \u001b[43mcall_func_with_variable_args\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 2951\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mfunc\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43minput\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mrun_manager\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\n\u001b[1;32m 2952\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 2953\u001b[0m \u001b[38;5;66;03m# If the output is a runnable, invoke it\u001b[39;00m\n\u001b[1;32m 2954\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(output, Runnable):\n", + "File \u001b[0;32m~/langchain/libs/core/langchain_core/runnables/config.py:323\u001b[0m, in \u001b[0;36mcall_func_with_variable_args\u001b[0;34m(func, input, config, run_manager, **kwargs)\u001b[0m\n\u001b[1;32m 321\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m run_manager \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m \u001b[38;5;129;01mand\u001b[39;00m accepts_run_manager(func):\n\u001b[1;32m 322\u001b[0m kwargs[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mrun_manager\u001b[39m\u001b[38;5;124m\"\u001b[39m] \u001b[38;5;241m=\u001b[39m run_manager\n\u001b[0;32m--> 323\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43minput\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", + "Cell \u001b[0;32mIn[30], line 11\u001b[0m, in \u001b[0;36mhuman_approval\u001b[0;34m(tool_invocations)\u001b[0m\n\u001b[1;32m 9\u001b[0m resp \u001b[38;5;241m=\u001b[39m \u001b[38;5;28minput\u001b[39m(msg)\n\u001b[1;32m 10\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m resp\u001b[38;5;241m.\u001b[39mlower() \u001b[38;5;129;01min\u001b[39;00m (\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124myes\u001b[39m\u001b[38;5;124m\"\u001b[39m, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124my\u001b[39m\u001b[38;5;124m\"\u001b[39m):\n\u001b[0;32m---> 11\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mTool invocations not approved:\u001b[39m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[38;5;132;01m{\u001b[39;00mtool_strs\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 12\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m tool_invocations\n", + "\u001b[0;31mValueError\u001b[0m: Tool invocations not approved:\n\n{\n \"type\": \"send_email\",\n \"args\": {\n \"message\": \"What's up homie\",\n \"recipient\": \"sally@gmail.com\"\n }\n}" + ] + } + ], + "source": [ + "chain.invoke(\"Send sally@gmail.com an email saying 'What's up homie'\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e938d8f1-df93-4726-a465-78e596312246", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "poetry-venv", + "language": "python", + "name": "poetry-venv" + }, + "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.9.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/docs/use_cases/tool_use/index.ipynb b/docs/docs/use_cases/tool_use/index.ipynb new file mode 100644 index 0000000000000..b31e5bb397b66 --- /dev/null +++ b/docs/docs/use_cases/tool_use/index.ipynb @@ -0,0 +1,61 @@ +{ + "cells": [ + { + "cell_type": "raw", + "id": "451cda29-bed0-4558-9ed7-099bdd12ad60", + "metadata": {}, + "source": [ + "---\n", + "sidebar_position: 0.9\n", + "---" + ] + }, + { + "cell_type": "markdown", + "id": "14b94240", + "metadata": {}, + "source": [ + "# Tool use\n", + "\n", + "An exciting use case for LLMs is building natural language interfaces for other \"tools\", whether those are APIs, functions, databases, etc. LangChain is great for building such interfaces because it has:\n", + "\n", + "- Good model output parsing, which makes it easy to extract JSON, XML, OpenAI function-calls, etc. from model outputs.\n", + "- A large collection of built-in [Tools](/docs/integrations/tools).\n", + "- Provides a lot of flexibility in how you call these tools.\n", + "\n", + "There are two main ways to use tools: [chains](/docs/modules/chains) and [agents](/docs/modules/agents/). \n", + "\n", + "Chains lets you create a pre-defined sequence of tool usage(s). \n", + "\n", + "![chain](../../../static/img/tool_chain.svg)\n", + "\n", + "Agents let the model use tools in a loop, so that it can decide how many times to use tools.\n", + "\n", + "![agent](../../../static/img/tool_agent.svg)\n", + "\n", + "To get started with both approaches, head to the [Quickstart](/docs/use_cases/tool_use/quickstart) page." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "poetry-venv", + "language": "python", + "name": "poetry-venv" + }, + "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.9.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/docs/use_cases/tool_use/multiple_tools.ipynb b/docs/docs/use_cases/tool_use/multiple_tools.ipynb new file mode 100644 index 0000000000000..fd520a951a5de --- /dev/null +++ b/docs/docs/use_cases/tool_use/multiple_tools.ipynb @@ -0,0 +1,259 @@ +{ + "cells": [ + { + "cell_type": "raw", + "id": "1ea1fe24-fe1e-463b-a52c-79f0ef02328e", + "metadata": {}, + "source": [ + "---\n", + "sidebar_position: 2\n", + "---" + ] + }, + { + "cell_type": "markdown", + "id": "95982bf1-7d9d-4dd6-a4ad-9de0719fe17f", + "metadata": {}, + "source": [ + "# Choosing between multiple tools\n", + "\n", + "In our [Quickstart](/docs/use_cases/tool_use/quickstart) we went over how to build a Chain that calls a single `multiply` tool. Now let's take a look at how we might augment this chain so that it can pick from a number of tools to call. We'll focus on Chains since [Agents](/docs/use_cases/tool_use/agents) can route between multiple tools by default." + ] + }, + { + "cell_type": "markdown", + "id": "3fafec38-443a-42ad-a913-5be7667e3734", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "We'll need to install the following packages for this guide:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "78411bf1-0117-4f33-a3d7-f3d77a97bb78", + "metadata": {}, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet langchain langchain-openai" + ] + }, + { + "cell_type": "markdown", + "id": "59d08fd0-ddd9-4c74-bcea-a5ca3a86e542", + "metadata": {}, + "source": [ + "And set these environment variables:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4185e74b-0500-4cad-ace0-bac37de466ac", + "metadata": {}, + "outputs": [], + "source": [ + "import getpass\n", + "import os\n", + "\n", + "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass()\n", + "\n", + "# If you'd like to use LangSmith, uncomment the below\n", + "# os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "# os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass()" + ] + }, + { + "cell_type": "markdown", + "id": "d28159f5-b7d0-4385-aa44-4cd1b64507bb", + "metadata": {}, + "source": [ + "## Tools\n", + "\n", + "Recall we already had a `multiply` tool:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "e13ec98c-8521-4d63-b521-caf92da87b70", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_core.tools import tool\n", + "\n", + "\n", + "@tool\n", + "def multiply(first_int: int, second_int: int) -> int:\n", + " \"\"\"Multiply two integers together.\"\"\"\n", + " return first_int * second_int" + ] + }, + { + "cell_type": "markdown", + "id": "3de233af-b3bd-4f0c-8b1a-83527143a8db", + "metadata": {}, + "source": [ + "And now we can add to it a `exponentiate` and `add` tool:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "e93661cd-a2ba-4ada-91ad-baf1b60879ec", + "metadata": {}, + "outputs": [], + "source": [ + "@tool\n", + "def add(first_int: int, second_int: int) -> int:\n", + " \"Add two integers.\"\n", + " return first_int + second_int\n", + "\n", + "\n", + "@tool\n", + "def exponentiate(base: int, exponent: int) -> int:\n", + " \"Exponentiate the base to the exponent power.\"\n", + " return base**exponent" + ] + }, + { + "cell_type": "markdown", + "id": "bbea4555-ed10-4a18-b802-e9a3071f132b", + "metadata": {}, + "source": [ + "The main difference between using one Tool and many, is that in the case of many we can't be sure which Tool the model will invoke. So we cannot hardcode, like we did in the [Quickstart](/docs/use_cases/tool_use/quickstart), a specific tool into our chain. Instead we'll add `call_tool_list`, a `RunnableLambda` that takes the `JsonOutputToolsParser` output and actually builds the end of the chain based on it, meaning it appends the Tools that were envoked to the end of the chain at runtime. We can do this because LCEL has the cool property that in any Runnable (the core building block of LCEL) sequence, if one component returns more Runnables, those are run as part of the chain." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "c35359ae-a740-48c5-b5e7-1a377fb25aa2", + "metadata": {}, + "outputs": [], + "source": [ + "from operator import itemgetter\n", + "from typing import Union\n", + "\n", + "from langchain.output_parsers import JsonOutputToolsParser\n", + "from langchain_community.tools.convert_to_openai import (\n", + " format_tool_to_openai_tool,\n", + ")\n", + "from langchain_core.runnables import (\n", + " Runnable,\n", + " RunnableLambda,\n", + " RunnableMap,\n", + " RunnablePassthrough,\n", + ")\n", + "from langchain_openai import ChatOpenAI\n", + "\n", + "model = ChatOpenAI(model=\"gpt-3.5-turbo\")\n", + "tools = [multiply, exponentiate, add]\n", + "model_with_tools = model.bind(tools=[format_tool_to_openai_tool(t) for t in tools])\n", + "tool_map = {tool.name: tool for tool in tools}\n", + "\n", + "\n", + "def call_tool(tool_invocation: dict) -> Union[str, Runnable]:\n", + " \"\"\"Function for dynamically constructing the end of the chain based on the model-selected tool.\"\"\"\n", + " tool = tool_map[tool_invocation[\"type\"]]\n", + " return RunnablePassthrough.assign(output=itemgetter(\"args\") | tool)\n", + "\n", + "\n", + "# .map() allows us to apply a function to a list of inputs.\n", + "call_tool_list = RunnableLambda(call_tool).map()\n", + "chain = model_with_tools | JsonOutputToolsParser() | call_tool_list" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "ea6dbb32-ec9b-4c70-a90f-a2db93978cf1", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[{'type': 'multiply',\n", + " 'args': {'first_int': 23, 'second_int': 7},\n", + " 'output': 161}]" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "chain.invoke(\"What's 23 times 7\")" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "b1c6c0f8-6d04-40d4-a40e-8719ca7b27c2", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[{'type': 'add',\n", + " 'args': {'first_int': 1000000, 'second_int': 1000000000},\n", + " 'output': 1001000000}]" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "chain.invoke(\"add a million plus a billion\")" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "ce76f299-1a4d-421c-afa4-a6346e34285c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[{'type': 'exponentiate',\n", + " 'args': {'base': 37, 'exponent': 3},\n", + " 'output': 50653}]" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "chain.invoke(\"cube thirty-seven\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "poetry-venv", + "language": "python", + "name": "poetry-venv" + }, + "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.9.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/docs/use_cases/tool_use/parallel.ipynb b/docs/docs/use_cases/tool_use/parallel.ipynb new file mode 100644 index 0000000000000..0220fb491e0b8 --- /dev/null +++ b/docs/docs/use_cases/tool_use/parallel.ipynb @@ -0,0 +1,197 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "95982bf1-7d9d-4dd6-a4ad-9de0719fe17f", + "metadata": {}, + "source": [ + "# Parallel tool use\n", + "\n", + "In the [Chains with multiple tools](/docs/use_cases/tool_use/multiple_tools) guide we saw how to build function-calling chains that select between multiple tools. Some models, like the OpenAI models released in Fall 2023, also support parallel function calling, which allows you to invoke multiple functions (or the same function multiple times) in a single model call. Our previous chain from the multiple tools guides actually already supports this, we just need to use an OpenAI model capable of parallel function calling." + ] + }, + { + "cell_type": "markdown", + "id": "3fafec38-443a-42ad-a913-5be7667e3734", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "We'll need to install the following packages for this guide:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "78411bf1-0117-4f33-a3d7-f3d77a97bb78", + "metadata": {}, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet langchain langchain-openai" + ] + }, + { + "cell_type": "markdown", + "id": "59d08fd0-ddd9-4c74-bcea-a5ca3a86e542", + "metadata": {}, + "source": [ + "And set these environment variables:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4185e74b-0500-4cad-ace0-bac37de466ac", + "metadata": {}, + "outputs": [], + "source": [ + "import getpass\n", + "import os\n", + "\n", + "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass()\n", + "\n", + "# If you'd like to use LangSmith, uncomment the below\n", + "# os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "# os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass()" + ] + }, + { + "cell_type": "markdown", + "id": "d28159f5-b7d0-4385-aa44-4cd1b64507bb", + "metadata": {}, + "source": [ + "## Tools" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "e13ec98c-8521-4d63-b521-caf92da87b70", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_core.tools import tool\n", + "\n", + "\n", + "@tool\n", + "def multiply(first_int: int, second_int: int) -> int:\n", + " \"\"\"Multiply two integers together.\"\"\"\n", + " return first_int * second_int\n", + "\n", + "\n", + "@tool\n", + "def add(first_int: int, second_int: int) -> int:\n", + " \"Add two integers.\"\n", + " return first_int + second_int\n", + "\n", + "\n", + "@tool\n", + "def exponentiate(base: int, exponent: int) -> int:\n", + " \"Exponentiate the base to the exponent power.\"\n", + " return base**exponent" + ] + }, + { + "cell_type": "markdown", + "id": "119d419c-1c61-4e0d-834a-5dabb72f5514", + "metadata": {}, + "source": [ + "# Chain\n", + "\n", + "Notice we use an `-1106` model, which as of this writing is the only kind that supports parallel function calling:" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "c35359ae-a740-48c5-b5e7-1a377fb25aa2", + "metadata": {}, + "outputs": [], + "source": [ + "from operator import itemgetter\n", + "from typing import Union\n", + "\n", + "from langchain.output_parsers import JsonOutputToolsParser\n", + "from langchain_community.tools.convert_to_openai import (\n", + " format_tool_to_openai_tool,\n", + ")\n", + "from langchain_core.runnables import (\n", + " Runnable,\n", + " RunnableLambda,\n", + " RunnableMap,\n", + " RunnablePassthrough,\n", + ")\n", + "from langchain_openai import ChatOpenAI\n", + "\n", + "model = ChatOpenAI(model=\"gpt-3.5-turbo-1106\")\n", + "tools = [multiply, exponentiate, add]\n", + "model_with_tools = model.bind(tools=[format_tool_to_openai_tool(t) for t in tools])\n", + "tool_map = {tool.name: tool for tool in tools}\n", + "\n", + "\n", + "def call_tool(tool_invocation: dict) -> Union[str, Runnable]:\n", + " \"\"\"Function for dynamically constructing the end of the chain based on the model-selected tool.\"\"\"\n", + " tool = tool_map[tool_invocation[\"type\"]]\n", + " return RunnablePassthrough.assign(output=itemgetter(\"args\") | tool)\n", + "\n", + "\n", + "# .map() allows us to apply a function to a list of inputs.\n", + "call_tool_list = RunnableLambda(call_tool).map()\n", + "chain = model_with_tools | JsonOutputToolsParser() | call_tool_list" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "ea6dbb32-ec9b-4c70-a90f-a2db93978cf1", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[{'type': 'multiply',\n", + " 'args': {'first_int': 23, 'second_int': 7},\n", + " 'output': 161},\n", + " {'type': 'add', 'args': {'first_int': 5, 'second_int': 18}, 'output': 23},\n", + " {'type': 'add',\n", + " 'args': {'first_int': 1000000, 'second_int': 1000000000},\n", + " 'output': 1001000000},\n", + " {'type': 'exponentiate',\n", + " 'args': {'base': 37, 'exponent': 3},\n", + " 'output': 50653}]" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "chain.invoke(\n", + " \"What's 23 times 7, and what's five times 18 and add a million plus a billion and cube thirty-seven\"\n", + ")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "poetry-venv", + "language": "python", + "name": "poetry-venv" + }, + "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.9.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/docs/use_cases/tool_use/prompting.ipynb b/docs/docs/use_cases/tool_use/prompting.ipynb new file mode 100644 index 0000000000000..6cd877fe2e468 --- /dev/null +++ b/docs/docs/use_cases/tool_use/prompting.ipynb @@ -0,0 +1,415 @@ +{ + "cells": [ + { + "cell_type": "raw", + "id": "3243cb05-8243-421f-99fa-98201abb3094", + "metadata": {}, + "source": [ + "---\n", + "sidebar_position: 3\n", + "---" + ] + }, + { + "cell_type": "markdown", + "id": "14b94240", + "metadata": {}, + "source": [ + "# Tool use without function calling\n", + "\n", + "In this guide we'll build a Chain that does not rely on any special model APIs (like function-calling, which we showed in the [Quickstart](/docs/use_cases/tool_use/quickstart)) and instead just prompts the model directly to invoke tools." + ] + }, + { + "cell_type": "markdown", + "id": "a0a22cb8-19e7-450a-9d1b-6848d2c81cd1", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "We'll need to install the following packages:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8c556c5e-b785-428b-8e7d-efd34a2a1adb", + "metadata": {}, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet langchain langchain-openai" + ] + }, + { + "cell_type": "markdown", + "id": "5e727d22-f861-4eee-882a-688f8efc885e", + "metadata": {}, + "source": [ + "And set these environment variables:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "527ef906-0104-4872-b4e5-f371cf73feba", + "metadata": {}, + "outputs": [], + "source": [ + "import getpass\n", + "import os\n", + "\n", + "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass()\n", + "\n", + "# If you'd like to use LangSmith, uncomment the below:\n", + "# os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "# os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass()" + ] + }, + { + "cell_type": "markdown", + "id": "68946881", + "metadata": {}, + "source": [ + "## Create a tool\n", + "\n", + "First, we need to create a tool to call. For this example, we will create a custom tool from a function. For more information on all details related to creating custom tools, please see [this guide](/docs/modules/agents/tools/)." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "90187d07", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_core.tools import tool\n", + "\n", + "\n", + "@tool\n", + "def multiply(first_int: int, second_int: int) -> int:\n", + " \"\"\"Multiply two integers together.\"\"\"\n", + " return first_int * second_int" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "d7009e1a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "multiply\n", + "multiply(first_int: int, second_int: int) -> int - Multiply two integers together.\n", + "{'first_int': {'title': 'First Int', 'type': 'integer'}, 'second_int': {'title': 'Second Int', 'type': 'integer'}}\n" + ] + } + ], + "source": [ + "print(multiply.name)\n", + "print(multiply.description)\n", + "print(multiply.args)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "be77e780", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "20" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "multiply.invoke({\"first_int\": 4, \"second_int\": 5})" + ] + }, + { + "cell_type": "markdown", + "id": "15dd690e-e54d-4209-91a4-181f69a452ac", + "metadata": {}, + "source": [ + "## Creating our prompt\n", + "\n", + "We'll want to write a prompt that specifies the tools the model has access to, the arguments to those tools, and the desired output format of the model. In this case we'll instruct it to output a JSON blob of the form `{\"name\": \"...\", \"arguments\": {...}}`." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "c64818f0-9364-423c-922e-bdfb8f01e726", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'multiply: multiply(first_int: int, second_int: int) -> int - Multiply two integers together.'" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from langchain.tools.render import render_text_description\n", + "\n", + "rendered_tools = render_text_description([multiply])\n", + "rendered_tools" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "63552d4d-8bd6-4aca-8805-56e236f6552d", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_core.prompts import ChatPromptTemplate\n", + "\n", + "system_prompt = f\"\"\"You are an assistant that has access to the following set of tools. Here are the names and descriptions for each tool:\n", + "\n", + "{rendered_tools}\n", + "\n", + "Given the user input, return the name and input of the tool to use. Return your response as a JSON blob with 'name' and 'arguments' keys.\"\"\"\n", + "\n", + "prompt = ChatPromptTemplate.from_messages(\n", + " [(\"system\", system_prompt), (\"user\", \"{input}\")]\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "14df2cd5-b6fa-4b10-892d-e8692c7931e5", + "metadata": {}, + "source": [ + "## Adding an output parser\n", + "\n", + "We'll use the `JsonOutputParser` for parsing our models output to JSON." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "f129f5bd-127c-4c95-8f34-8f437da7ca8f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'name': 'multiply', 'arguments': {'first_int': 13, 'second_int': 4}}" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from langchain_core.output_parsers import JsonOutputParser\n", + "from langchain_openai import ChatOpenAI\n", + "\n", + "model = ChatOpenAI(model=\"gpt-3.5-turbo\", temperature=0)\n", + "chain = prompt | model | JsonOutputParser()\n", + "chain.invoke({\"input\": \"what's thirteen times 4\"})" + ] + }, + { + "cell_type": "markdown", + "id": "8e29dd4c-8eb5-457f-92d1-8add076404dc", + "metadata": {}, + "source": [ + "## Invoking the tool\n", + "\n", + "We can invoke the tool as part of the chain by passing along the model-generated \"arguments\" to it:" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "0555b384-fde6-4404-86e0-7ea199003d58", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "52" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from operator import itemgetter\n", + "\n", + "chain = prompt | model | JsonOutputParser() | itemgetter(\"arguments\") | multiply\n", + "chain.invoke({\"input\": \"what's thirteen times 4\"})" + ] + }, + { + "cell_type": "markdown", + "id": "8d60b2cb-6ce0-48fc-8d18-d2337161a53d", + "metadata": {}, + "source": [ + "## Choosing from multiple tools\n", + "\n", + "Suppose we have multiple tools we want the chain to be able to choose from:" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "95c86d32-ee45-4c87-a28c-14eff19b49e9", + "metadata": {}, + "outputs": [], + "source": [ + "@tool\n", + "def add(first_int: int, second_int: int) -> int:\n", + " \"Add two integers.\"\n", + " return first_int + second_int\n", + "\n", + "\n", + "@tool\n", + "def exponentiate(base: int, exponent: int) -> int:\n", + " \"Exponentiate the base to the exponent power.\"\n", + " return base**exponent" + ] + }, + { + "cell_type": "markdown", + "id": "748405ff-4c85-4bd7-82e1-30458b5a4106", + "metadata": {}, + "source": [ + "With function calling, we can do this like so:" + ] + }, + { + "cell_type": "markdown", + "id": "eb3aa89e-40e1-45ec-b1f3-ab28cfc8e42d", + "metadata": {}, + "source": [ + "If we want to run the model selected tool, we can do so using a function that returns the tool based on the model output. Specifically, our function will action return it's own subchain that gets the \"arguments\" part of the model output and passes it to the chosen tool:" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "db254773-5b8e-43d0-aabe-c21566c154cd", + "metadata": {}, + "outputs": [], + "source": [ + "tools = [add, exponentiate, multiply]\n", + "\n", + "\n", + "def tool_chain(model_output):\n", + " tool_map = {tool.name: tool for tool in tools}\n", + " chosen_tool = tool_map[model_output[\"name\"]]\n", + " return itemgetter(\"arguments\") | chosen_tool" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "ad9f5cff-b86a-45fc-9ce4-b0aa9025a378", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "1135" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "rendered_tools = render_text_description(tools)\n", + "system_prompt = f\"\"\"You are an assistant that has access to the following set of tools. Here are the names and descriptions for each tool:\n", + "\n", + "{rendered_tools}\n", + "\n", + "Given the user input, return the name and input of the tool to use. Return your response as a JSON blob with 'name' and 'arguments' keys.\"\"\"\n", + "\n", + "prompt = ChatPromptTemplate.from_messages(\n", + " [(\"system\", system_prompt), (\"user\", \"{input}\")]\n", + ")\n", + "\n", + "chain = prompt | model | JsonOutputParser() | tool_chain\n", + "chain.invoke({\"input\": \"what's 3 plus 1132\"})" + ] + }, + { + "cell_type": "markdown", + "id": "b4a9c5aa-f60a-4017-af6f-1ff6e04bfb61", + "metadata": {}, + "source": [ + "## Returning tool inputs\n", + "\n", + "It can be helpful to return not only tool outputs but also tool inputs. We can easily do this with LCEL by `RunnablePassthrough.assign`-ing the tool output. This will take whatever the input is to the RunnablePassrthrough components (assumed to be a dictionary) and add a key to it while still passing through everything that's currently in the input:" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "45404406-859d-4caa-8b9d-5838162c80a0", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'name': 'add',\n", + " 'arguments': {'first_int': 3, 'second_int': 1132},\n", + " 'output': 1135}" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from langchain_core.runnables import RunnablePassthrough\n", + "\n", + "chain = (\n", + " prompt | model | JsonOutputParser() | RunnablePassthrough.assign(output=tool_chain)\n", + ")\n", + "chain.invoke({\"input\": \"what's 3 plus 1132\"})" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "poetry-venv", + "language": "python", + "name": "poetry-venv" + }, + "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.9.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/docs/use_cases/tool_use/quickstart.ipynb b/docs/docs/use_cases/tool_use/quickstart.ipynb new file mode 100644 index 0000000000000..921e9f1a2e3c2 --- /dev/null +++ b/docs/docs/use_cases/tool_use/quickstart.ipynb @@ -0,0 +1,535 @@ +{ + "cells": [ + { + "cell_type": "raw", + "id": "500e8846-91c2-4716-9bd6-b9672c6daf78", + "metadata": {}, + "source": [ + "---\n", + "sidebar_position: 0\n", + "---" + ] + }, + { + "cell_type": "markdown", + "id": "14b94240", + "metadata": {}, + "source": [ + "# Quickstart\n", + "\n", + "In this guide, we will go over the basic ways to create Chains and Agents that call Tools. Tools can be just about anything — APIs, functions, databases, etc. Tools allow us to extend the capabilities of a model beyond just outputting text/messages. The key to using models with tools is correctly prompting a model and parsing its response so that it chooses the right ools and provides the right inputs for them." + ] + }, + { + "cell_type": "markdown", + "id": "e6b79a42-0349-42c6-9ce8-72220e838e8d", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "We'll need to install the following packages for this guide:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f2274266-755a-4e90-b257-5180fb089af2", + "metadata": {}, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet langchain langchain-openai" + ] + }, + { + "cell_type": "markdown", + "id": "36a9c6fc-8264-462f-b8d7-9c7bbec22ef9", + "metadata": {}, + "source": [ + "And set these environment variables:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "57a81b7a-4fd9-4f28-bc32-7b98b522e1b0", + "metadata": {}, + "outputs": [], + "source": [ + "import getpass\n", + "import os\n", + "\n", + "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass()\n", + "\n", + "# If you'd like to use LangSmith, uncomment the below\n", + "# os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "# os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass()" + ] + }, + { + "cell_type": "markdown", + "id": "68946881", + "metadata": {}, + "source": [ + "## Create a tool\n", + "\n", + "First, we need to create a tool to call. For this example, we will create a custom tool from a function. For more information on creating custom tools, please see [this guide](/docs/modules/agents/tools/)." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "90187d07", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_core.tools import tool\n", + "\n", + "\n", + "@tool\n", + "def multiply(first_int: int, second_int: int) -> int:\n", + " \"\"\"Multiply two integers together.\"\"\"\n", + " return first_int * second_int" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "d7009e1a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "multiply\n", + "multiply(first_int: int, second_int: int) -> int - Multiply two integers together.\n", + "{'first_int': {'title': 'First Int', 'type': 'integer'}, 'second_int': {'title': 'Second Int', 'type': 'integer'}}\n" + ] + } + ], + "source": [ + "print(multiply.name)\n", + "print(multiply.description)\n", + "print(multiply.args)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "be77e780", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "20" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "multiply.invoke({\"first_int\": 4, \"second_int\": 5})" + ] + }, + { + "cell_type": "markdown", + "id": "19ba4d63", + "metadata": {}, + "source": [ + "## Chains\n", + "\n", + "If we know that we only need to use a tool a fixed number of times, we can create a chain for doing so. Let's create a simple chain that just multiplies user-specified numbers.\n", + "\n", + "![chain](../../../static/img/tool_chain.svg)\n", + "\n", + "### Function calling\n", + "One of the most reliable ways to use tools with LLMs is with function calling APIs (also sometimes called tool calling or parallel function calling). This only works with models that explicitly support function calling, like OpenAI models.\n", + "\n", + "First we'll define our model and tools. We'll start with just a single tool, `multiply`." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "9bce8935-1465-45ac-8a93-314222c753c4", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_openai.chat_models import ChatOpenAI\n", + "\n", + "model = ChatOpenAI(model=\"gpt-3.5-turbo-1106\")" + ] + }, + { + "cell_type": "markdown", + "id": "c22e6f0f-c5ad-4c0f-9514-e626704ea51c", + "metadata": {}, + "source": [ + "Next we'll convert our LangChain Tool to an OpenAI format JSONSchema function, and bind this as the `tools` argument to be passed to all ChatOpenAI calls. Since we only have a single Tool and in this initial chain we want to make sure it's always used, we'll also specify `tool_choice`. See the [OpenAI chat API reference](https://platform.openai.com/docs/api-reference/chat/create#chat-create-tool_choice) for more on these parameters." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "2babd759-bccd-4d50-95ad-365a07347926", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[{'type': 'function',\n", + " 'function': {'name': 'multiply',\n", + " 'description': 'multiply(first_int: int, second_int: int) -> int - Multiply two integers together.',\n", + " 'parameters': {'title': 'multiplySchemaSchema',\n", + " 'type': 'object',\n", + " 'properties': {'first_int': {'title': 'First Int', 'type': 'integer'},\n", + " 'second_int': {'title': 'Second Int', 'type': 'integer'}},\n", + " 'required': ['first_int', 'second_int']}}}]" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from langchain_community.tools.convert_to_openai import (\n", + " format_tool_to_openai_tool,\n", + ")\n", + "\n", + "formatted_tools = [format_tool_to_openai_tool(multiply)]\n", + "formatted_tools" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "3bfe2cdc-7d72-457c-a9a1-5fa1e0bcde55", + "metadata": {}, + "outputs": [], + "source": [ + "model_with_tools = model.bind(\n", + " tools=formatted_tools,\n", + " # We specify tool_choice to enforce that the 'multiply' function is called by the model.\n", + " tool_choice={\"type\": \"function\", \"function\": {\"name\": \"multiply\"}},\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "9fa2ba14-9a97-4960-a6c7-422edecdaf4b", + "metadata": {}, + "source": [ + "Now we'll compose out tool-calling model with a `JsonOutputToolsParser`, a built-in LangChain output parser that converts an OpenAI function-calling response to a list of `{\"type\": \"TOOL_NAME\", \"args\": {...}}` dicts with the tools to invoke and arguments to invoke them with." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "5518aba4-c44d-4896-9b63-fc9d56c245df", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[{'type': 'multiply', 'args': {'first_int': 4, 'second_int': 23}}]" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from langchain.output_parsers import JsonOutputToolsParser\n", + "\n", + "chain = model_with_tools | JsonOutputToolsParser()\n", + "chain.invoke(\"What's four times 23\")" + ] + }, + { + "cell_type": "markdown", + "id": "7f712d8d-0314-4d3d-b563-378b72fd8bb5", + "metadata": {}, + "source": [ + "Since we know we're always invoking the `multiply` tool, we can simplify our output a bit to return only the args for the `multiply` tool using the `JsonoutputKeyToolsParser`. To further simplify we'll specify `return_single=True`, so that instead of a list of tool invocations our output parser returns only the first tool invocation." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "cfacfcdc-8a45-4c60-a175-7efe9534f83e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'first_int': 4, 'second_int': 23}" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from langchain.output_parsers import JsonOutputKeyToolsParser\n", + "\n", + "chain = model_with_tools | JsonOutputKeyToolsParser(\n", + " key_name=\"multiply\", return_single=True\n", + ")\n", + "chain.invoke(\"What's four times 23\")" + ] + }, + { + "cell_type": "markdown", + "id": "8ba1764d-0272-4f98-adcf-b48cb2c0a315", + "metadata": {}, + "source": [ + "### Invoking the tool\n", + "\n", + "Great! We're able to generate tool invocations. But what if we want to actually call the tool? To do that we just need to pass them to the tool:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "4f5325ca-e5dc-4d1a-ba36-b085a029c90a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "92" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from operator import itemgetter\n", + "\n", + "# Note: the `.map()` at the end of `multiply` allows us to pass in a list of `multiply` arguments instead of a single one.\n", + "chain = (\n", + " model_with_tools\n", + " | JsonOutputKeyToolsParser(key_name=\"multiply\", return_single=True)\n", + " | multiply\n", + ")\n", + "chain.invoke(\"What's four times 23\")" + ] + }, + { + "cell_type": "markdown", + "id": "0521d3d5", + "metadata": {}, + "source": [ + "## Agents\n", + "\n", + "Chains are great when we know the specific sequence of tool usage needed for any user input. But for certain use cases, how many times we use tools depends on the input. In these cases, we want to let the model itself decide how many times to use tools and in what order. [Agents](/docs/modules/agents/) let us do just this.\n", + "\n", + "LangChain comes with a number of built-in agents that are optimized for different use cases. Read about all the [agent types here](/docs/modules/agents/agent_types/).\n", + "\n", + "As an example, let's try out the OpenAI tools agent, which makes use of the new OpenAI tool-calling API (this is only available in the latest OpenAI models, and differs from function-calling in that the model can return multiple function invocations at once)\n", + "\n", + "![agent](../../../static/img/tool_agent.svg)" + ] + }, + { + "cell_type": "code", + "execution_count": 86, + "id": "21723cf4-9421-4a8d-92a6-eeeb8f4367f1", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain import hub\n", + "from langchain.agents import AgentExecutor, create_openai_tools_agent\n", + "from langchain_openai import ChatOpenAI" + ] + }, + { + "cell_type": "code", + "execution_count": 88, + "id": "6be83879-9da3-4dd9-b147-a79f76affd7a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[SystemMessagePromptTemplate(prompt=PromptTemplate(input_variables=[], template='You are a helpful assistant')),\n", + " MessagesPlaceholder(variable_name='chat_history', optional=True),\n", + " HumanMessagePromptTemplate(prompt=PromptTemplate(input_variables=['input'], template='{input}')),\n", + " MessagesPlaceholder(variable_name='agent_scratchpad')]" + ] + }, + "execution_count": 88, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Get the prompt to use - you can modify this!\n", + "prompt = hub.pull(\"hwchase17/openai-tools-agent\")\n", + "prompt.messages" + ] + }, + { + "cell_type": "markdown", + "id": "616f9714-5b18-4eed-b88a-d38e4cb1de99", + "metadata": {}, + "source": [ + "Agents are also great because they make it easy to use multiple tools. To learn how to build Chains that use multiple tools, check out the [Chains with multiple tools](/docs/use_cases/tool_use/multiple_tools) page." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "95c86d32-ee45-4c87-a28c-14eff19b49e9", + "metadata": {}, + "outputs": [], + "source": [ + "@tool\n", + "def add(first_int: int, second_int: int) -> int:\n", + " \"Add two integers.\"\n", + " return first_int + second_int\n", + "\n", + "\n", + "@tool\n", + "def exponentiate(base: int, exponent: int) -> int:\n", + " \"Exponentiate the base to the exponent power.\"\n", + " return base**exponent\n", + "\n", + "\n", + "tools = [multiply, add, exponentiate]" + ] + }, + { + "cell_type": "code", + "execution_count": 90, + "id": "17b09ac6-c9b7-4340-a8a0-3d3061f7888c", + "metadata": {}, + "outputs": [], + "source": [ + "# Choose the LLM that will drive the agent\n", + "# Only certain models support this\n", + "model = ChatOpenAI(model=\"gpt-3.5-turbo-1106\", temperature=0)\n", + "\n", + "# Construct the OpenAI Tools agent\n", + "agent = create_openai_tools_agent(model, tools, prompt)" + ] + }, + { + "cell_type": "code", + "execution_count": 91, + "id": "675091d2-cac9-45c4-a5d7-b760ee6c1986", + "metadata": {}, + "outputs": [], + "source": [ + "# Create an agent executor by passing in the agent and tools\n", + "agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)" + ] + }, + { + "cell_type": "markdown", + "id": "a6099ab6-2fa6-452d-b73c-7fb65daab451", + "metadata": {}, + "source": [ + "With an agent, we can ask questions that require arbitrarily-many uses of our tools:" + ] + }, + { + "cell_type": "code", + "execution_count": 95, + "id": "f7dbb240-809e-4e41-8f63-1a4636e8e26d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3m\n", + "Invoking: `exponentiate` with `{'base': 3, 'exponent': 5}`\n", + "\n", + "\n", + "\u001b[0m\u001b[38;5;200m\u001b[1;3m243\u001b[0m\u001b[32;1m\u001b[1;3m\n", + "Invoking: `add` with `{'first_int': 12, 'second_int': 3}`\n", + "\n", + "\n", + "\u001b[0m\u001b[33;1m\u001b[1;3m15\u001b[0m\u001b[32;1m\u001b[1;3m\n", + "Invoking: `multiply` with `{'first_int': 243, 'second_int': 15}`\n", + "\n", + "\n", + "\u001b[0m\u001b[36;1m\u001b[1;3m3645\u001b[0m\u001b[32;1m\u001b[1;3m\n", + "Invoking: `exponentiate` with `{'base': 3645, 'exponent': 2}`\n", + "\n", + "\n", + "\u001b[0m\u001b[38;5;200m\u001b[1;3m13286025\u001b[0m\u001b[32;1m\u001b[1;3mThe result of raising 3 to the fifth power and multiplying that by the sum of twelve and three, then squaring the whole result is 13,286,025.\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "{'input': 'Take 3 to the fifth power and multiply that by the sum of twelve and three, then square the whole result',\n", + " 'output': 'The result of raising 3 to the fifth power and multiplying that by the sum of twelve and three, then squaring the whole result is 13,286,025.'}" + ] + }, + "execution_count": 95, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "agent_executor.invoke(\n", + " {\n", + " \"input\": \"Take 3 to the fifth power and multiply that by the sum of twelve and three, then square the whole result\"\n", + " }\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "b0e4b7f4-58ce-4ca0-a986-d05a436a7ccf", + "metadata": {}, + "source": [ + "## Next steps\n", + "\n", + "Here we've gone over the basic ways to use Tools with Chains and Agents. We recommend the following sections to explore next:\n", + "\n", + "- [Agents](/docs/modules/agents/): Everything related to Agents.\n", + "- [Choosing between multiple tools](/docs/use_cases/tool_use/multiple_tools): How to make tool chains that select from multiple tools.\n", + "- [Prompting for tool use](/docs/use_cases/tool_use/prompting): How to make tool chains that prompt models directly, without using function-calling APIs.\n", + "- [Parallel tool use](/docs/use_cases/tool_use/parallel): How to make tool chains that invoke multiple tools at once." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "poetry-venv", + "language": "python", + "name": "poetry-venv" + }, + "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.9.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/docs/use_cases/tool_use/tool_error_handling.ipynb b/docs/docs/use_cases/tool_use/tool_error_handling.ipynb new file mode 100644 index 0000000000000..81800a9fa495d --- /dev/null +++ b/docs/docs/use_cases/tool_use/tool_error_handling.ipynb @@ -0,0 +1,426 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "5d60cbb9-2a6a-43ea-a9e9-f67b16ddd2b2", + "metadata": {}, + "source": [ + "# Tool error handling\n", + "\n", + "Using a model to invoke a tool has some obvious potential failure modes. Firstly, the model needs to return a output that can be parsed at all. Secondly, the model needs to return tool arguments that are valid.\n", + "\n", + "We can build error handling into our chains to mitigate these failure modes." + ] + }, + { + "cell_type": "markdown", + "id": "712c774f-27c7-4351-a196-39900ca155f5", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "We'll need to install the following packages:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "63056c24-9834-4e3d-8bc5-54b1e6c5df86", + "metadata": {}, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet langchain langchain-openai" + ] + }, + { + "cell_type": "markdown", + "id": "68107597-0c8c-4bb5-8c12-9992fabdf71a", + "metadata": {}, + "source": [ + "And set these environment variables:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "08785b6d-722d-4620-b6ec-36deb3842c69", + "metadata": {}, + "outputs": [], + "source": [ + "import getpass\n", + "import os\n", + "\n", + "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass()\n", + "\n", + "# If you'd like to use LangSmith, uncomment the below:\n", + "# os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "# os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass()" + ] + }, + { + "cell_type": "markdown", + "id": "0a50f93a-5d6f-4691-8f98-27239a1c2f95", + "metadata": {}, + "source": [ + "## Chain\n", + "\n", + "Suppose we have the following (dummy) tool and tool-calling chain. We'll make our tool intentionally convoluted to try and trip up the model." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "1d20604e-c4d1-4d21-841b-23e4f61aec36", + "metadata": {}, + "outputs": [], + "source": [ + "# Define tool\n", + "from langchain_core.tools import tool\n", + "\n", + "\n", + "@tool\n", + "def complex_tool(int_arg: int, float_arg: float, dict_arg: dict) -> int:\n", + " \"\"\"Do something complex with a complex tool.\"\"\"\n", + " return int_arg * float_arg" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "553c2c13-28c8-4451-8a3a-6c31d52dc31d", + "metadata": {}, + "outputs": [], + "source": [ + "# Define model and bind tool\n", + "from langchain_community.tools.convert_to_openai import format_tool_to_openai_tool\n", + "from langchain_openai import ChatOpenAI\n", + "\n", + "model = ChatOpenAI(model=\"gpt-3.5-turbo\", temperature=0)\n", + "model_with_tools = model.bind(\n", + " tools=[format_tool_to_openai_tool(complex_tool)],\n", + " tool_choice={\"type\": \"function\", \"function\": {\"name\": \"complex_tool\"}},\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "802b2eca-9f79-4d6c-8257-85139ca5c752", + "metadata": {}, + "outputs": [], + "source": [ + "# Define chain\n", + "from operator import itemgetter\n", + "\n", + "from langchain.output_parsers import JsonOutputKeyToolsParser\n", + "from langchain_core.runnables import Runnable, RunnableLambda, RunnablePassthrough\n", + "\n", + "chain = (\n", + " model_with_tools\n", + " | JsonOutputKeyToolsParser(key_name=\"complex_tool\", return_single=True)\n", + " | complex_tool\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "c34f005e-63f0-4841-9461-ca36c36607fc", + "metadata": {}, + "source": [ + "We can see that when we try to invoke this chain with even a fairly explicit input, the model fails to correctly call the tool (it forgets the `dict_arg` argument)." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "d354664c-ac44-4967-a35f-8912b3ad9477", + "metadata": {}, + "outputs": [ + { + "ename": "ValidationError", + "evalue": "1 validation error for complex_toolSchemaSchema\ndict_arg\n field required (type=value_error.missing)", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mValidationError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[6], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mchain\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43minvoke\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 2\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43muse complex tool. the args are 5, 2.1, empty dictionary. don\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mt forget dict_arg\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\n\u001b[1;32m 3\u001b[0m \u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/langchain/libs/core/langchain_core/runnables/base.py:1774\u001b[0m, in \u001b[0;36mRunnableSequence.invoke\u001b[0;34m(self, input, config)\u001b[0m\n\u001b[1;32m 1772\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m 1773\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m i, step \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28menumerate\u001b[39m(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39msteps):\n\u001b[0;32m-> 1774\u001b[0m \u001b[38;5;28minput\u001b[39m \u001b[38;5;241m=\u001b[39m \u001b[43mstep\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43minvoke\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 1775\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43minput\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1776\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;66;43;03m# mark each step as a child run\u001b[39;49;00m\n\u001b[1;32m 1777\u001b[0m \u001b[43m \u001b[49m\u001b[43mpatch_config\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 1778\u001b[0m \u001b[43m \u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mcallbacks\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mrun_manager\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget_child\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43mf\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mseq:step:\u001b[39;49m\u001b[38;5;132;43;01m{\u001b[39;49;00m\u001b[43mi\u001b[49m\u001b[38;5;241;43m+\u001b[39;49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[38;5;132;43;01m}\u001b[39;49;00m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[1;32m 1779\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1780\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 1781\u001b[0m \u001b[38;5;66;03m# finish the root run\u001b[39;00m\n\u001b[1;32m 1782\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mBaseException\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m e:\n", + "File \u001b[0;32m~/langchain/libs/core/langchain_core/tools.py:210\u001b[0m, in \u001b[0;36mBaseTool.invoke\u001b[0;34m(self, input, config, **kwargs)\u001b[0m\n\u001b[1;32m 203\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21minvoke\u001b[39m(\n\u001b[1;32m 204\u001b[0m \u001b[38;5;28mself\u001b[39m,\n\u001b[1;32m 205\u001b[0m \u001b[38;5;28minput\u001b[39m: Union[\u001b[38;5;28mstr\u001b[39m, Dict],\n\u001b[1;32m 206\u001b[0m config: Optional[RunnableConfig] \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m,\n\u001b[1;32m 207\u001b[0m \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs: Any,\n\u001b[1;32m 208\u001b[0m ) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m Any:\n\u001b[1;32m 209\u001b[0m config \u001b[38;5;241m=\u001b[39m ensure_config(config)\n\u001b[0;32m--> 210\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrun\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 211\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43minput\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 212\u001b[0m \u001b[43m \u001b[49m\u001b[43mcallbacks\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mconfig\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mcallbacks\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 213\u001b[0m \u001b[43m \u001b[49m\u001b[43mtags\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mconfig\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mtags\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 214\u001b[0m \u001b[43m \u001b[49m\u001b[43mmetadata\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mconfig\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mmetadata\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 215\u001b[0m \u001b[43m \u001b[49m\u001b[43mrun_name\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mconfig\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mrun_name\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 216\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 217\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/langchain/libs/core/langchain_core/tools.py:315\u001b[0m, in \u001b[0;36mBaseTool.run\u001b[0;34m(self, tool_input, verbose, start_color, color, callbacks, tags, metadata, run_name, **kwargs)\u001b[0m\n\u001b[1;32m 301\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mrun\u001b[39m(\n\u001b[1;32m 302\u001b[0m \u001b[38;5;28mself\u001b[39m,\n\u001b[1;32m 303\u001b[0m tool_input: Union[\u001b[38;5;28mstr\u001b[39m, Dict],\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 312\u001b[0m \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs: Any,\n\u001b[1;32m 313\u001b[0m ) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m Any:\n\u001b[1;32m 314\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"Run the tool.\"\"\"\u001b[39;00m\n\u001b[0;32m--> 315\u001b[0m parsed_input \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_parse_input\u001b[49m\u001b[43m(\u001b[49m\u001b[43mtool_input\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 316\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mverbose \u001b[38;5;129;01mand\u001b[39;00m verbose \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[1;32m 317\u001b[0m verbose_ \u001b[38;5;241m=\u001b[39m verbose\n", + "File \u001b[0;32m~/langchain/libs/core/langchain_core/tools.py:250\u001b[0m, in \u001b[0;36mBaseTool._parse_input\u001b[0;34m(self, tool_input)\u001b[0m\n\u001b[1;32m 248\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[1;32m 249\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m input_args \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[0;32m--> 250\u001b[0m result \u001b[38;5;241m=\u001b[39m \u001b[43minput_args\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mparse_obj\u001b[49m\u001b[43m(\u001b[49m\u001b[43mtool_input\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 251\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m {\n\u001b[1;32m 252\u001b[0m k: \u001b[38;5;28mgetattr\u001b[39m(result, k)\n\u001b[1;32m 253\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m k, v \u001b[38;5;129;01min\u001b[39;00m result\u001b[38;5;241m.\u001b[39mdict()\u001b[38;5;241m.\u001b[39mitems()\n\u001b[1;32m 254\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m k \u001b[38;5;129;01min\u001b[39;00m tool_input\n\u001b[1;32m 255\u001b[0m }\n\u001b[1;32m 256\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m tool_input\n", + "File \u001b[0;32m~/langchain/.venv/lib/python3.9/site-packages/pydantic/v1/main.py:526\u001b[0m, in \u001b[0;36mBaseModel.parse_obj\u001b[0;34m(cls, obj)\u001b[0m\n\u001b[1;32m 524\u001b[0m exc \u001b[38;5;241m=\u001b[39m \u001b[38;5;167;01mTypeError\u001b[39;00m(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mcls\u001b[39m\u001b[38;5;241m.\u001b[39m\u001b[38;5;18m__name__\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m expected dict not \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mobj\u001b[38;5;241m.\u001b[39m\u001b[38;5;18m__class__\u001b[39m\u001b[38;5;241m.\u001b[39m\u001b[38;5;18m__name__\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m'\u001b[39m)\n\u001b[1;32m 525\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m ValidationError([ErrorWrapper(exc, loc\u001b[38;5;241m=\u001b[39mROOT_KEY)], \u001b[38;5;28mcls\u001b[39m) \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01me\u001b[39;00m\n\u001b[0;32m--> 526\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mcls\u001b[39;49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mobj\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/langchain/.venv/lib/python3.9/site-packages/pydantic/v1/main.py:341\u001b[0m, in \u001b[0;36mBaseModel.__init__\u001b[0;34m(__pydantic_self__, **data)\u001b[0m\n\u001b[1;32m 339\u001b[0m values, fields_set, validation_error \u001b[38;5;241m=\u001b[39m validate_model(__pydantic_self__\u001b[38;5;241m.\u001b[39m\u001b[38;5;18m__class__\u001b[39m, data)\n\u001b[1;32m 340\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m validation_error:\n\u001b[0;32m--> 341\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m validation_error\n\u001b[1;32m 342\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m 343\u001b[0m object_setattr(__pydantic_self__, \u001b[38;5;124m'\u001b[39m\u001b[38;5;124m__dict__\u001b[39m\u001b[38;5;124m'\u001b[39m, values)\n", + "\u001b[0;31mValidationError\u001b[0m: 1 validation error for complex_toolSchemaSchema\ndict_arg\n field required (type=value_error.missing)" + ] + } + ], + "source": [ + "chain.invoke(\n", + " \"use complex tool. the args are 5, 2.1, empty dictionary. don't forget dict_arg\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "890d989d-2d39-4571-9a55-d3496b9b5d27", + "metadata": {}, + "source": [ + "## Try/except tool call\n", + "\n", + "The simplest way to more gracefully handle errors is to try/except the tool-calling step and return a helpful message on errors:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "8fedb550-683d-45ae-8876-ae7acb332019", + "metadata": {}, + "outputs": [], + "source": [ + "from typing import Any\n", + "\n", + "from langchain_core.runnables import RunnableConfig\n", + "\n", + "\n", + "def try_except_tool(tool_args: dict, config: RunnableConfig) -> Runnable:\n", + " try:\n", + " complex_tool.invoke(tool_args, config=config)\n", + " except Exception as e:\n", + " return f\"Calling tool with arguments:\\n\\n{tool_args}\\n\\nraised the following error:\\n\\n{type(e)}: {e}\"\n", + "\n", + "\n", + "chain = (\n", + " model_with_tools\n", + " | JsonOutputKeyToolsParser(key_name=\"complex_tool\", return_single=True)\n", + " | try_except_tool\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "71a2c98d-c0be-4c0a-bb3d-41ad4596526c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Calling tool with arguments:\n", + "\n", + "{'int_arg': 5, 'float_arg': 2.1}\n", + "\n", + "raised the following error:\n", + "\n", + ": 1 validation error for complex_toolSchemaSchema\n", + "dict_arg\n", + " field required (type=value_error.missing)\n" + ] + } + ], + "source": [ + "print(\n", + " chain.invoke(\n", + " \"use complex tool. the args are 5, 2.1, empty dictionary. don't forget dict_arg\"\n", + " )\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "3b2f6393-cb47-49d0-921c-09550a049fe4", + "metadata": {}, + "source": [ + "## Fallbacks\n", + "\n", + "We can also try to fallback to a better model in the event of a tool invocation error. In this case we'll fall back to an identical chain that uses `gpt-4-1106-preview` instead of `gpt-3.5-turbo`." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "02cc4223-35fa-4240-976a-012299ca703c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "10.5" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "chain = (\n", + " model_with_tools\n", + " | JsonOutputKeyToolsParser(key_name=\"complex_tool\", return_single=True)\n", + " | complex_tool\n", + ")\n", + "better_model = ChatOpenAI(model=\"gpt-4-1106-preview\", temperature=0).bind(\n", + " tools=[format_tool_to_openai_tool(complex_tool)],\n", + " tool_choice={\"type\": \"function\", \"function\": {\"name\": \"complex_tool\"}},\n", + ")\n", + "better_chain = (\n", + " better_model\n", + " | JsonOutputKeyToolsParser(key_name=\"complex_tool\", return_single=True)\n", + " | complex_tool\n", + ")\n", + "\n", + "chain_with_fallback = chain.with_fallbacks([better_chain])\n", + "chain_with_fallback.invoke(\n", + " \"use complex tool. the args are 5, 2.1, empty dictionary. don't forget dict_arg\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "412f8c4e-cc83-4d87-84a1-5ba2f8edb1e9", + "metadata": {}, + "source": [ + "Looking at the [Langsmith trace](https://smith.langchain.com/public/241e1266-8555-4d49-99dc-b8df46109c39/r) for this chain run, we can see that the first chain call fails as expected and it's the fallback that succeeds." + ] + }, + { + "cell_type": "markdown", + "id": "304b59cd-cd25-4205-9769-36595c8f3b59", + "metadata": {}, + "source": [ + "## Retry with exception\n", + "\n", + "To take things one step further, we can try to automatically re-run the chain with the exception passed in, so that the model may be able to correct its behavior:" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "b5659956-9454-468a-9753-a3ff9052b8f5", + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "from typing import Any\n", + "\n", + "from langchain_core.messages import AIMessage, HumanMessage, ToolMessage\n", + "from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n", + "from langchain_core.runnables import RunnablePassthrough\n", + "\n", + "\n", + "class CustomToolException(Exception):\n", + " \"\"\"Custom LangChain tool exception.\"\"\"\n", + "\n", + " def __init__(self, tool_call: dict, exception: Exception) -> None:\n", + " super().__init__()\n", + " self.tool_call = tool_call\n", + " self.exception = exception\n", + "\n", + "\n", + "def tool_custom_exception(tool_call: dict, config: RunnableConfig) -> Runnable:\n", + " try:\n", + " return complex_tool.invoke(tool_call[\"args\"], config=config)\n", + " except Exception as e:\n", + " raise CustomToolException(tool_call, e)\n", + "\n", + "\n", + "def exception_to_messages(inputs: dict) -> dict:\n", + " exception = inputs.pop(\"exception\")\n", + " tool_call = {\n", + " \"type\": \"function\",\n", + " \"function\": {\n", + " \"name\": \"complex_tool\",\n", + " \"arguments\": json.dumps(exception.tool_call[\"args\"]),\n", + " },\n", + " \"id\": exception.tool_call[\"id\"],\n", + " }\n", + "\n", + " # Add historical messages to the original input, so the model knows that it made a mistake with the last tool call.\n", + " messages = [\n", + " AIMessage(content=\"\", additional_kwargs={\"tool_calls\": [tool_call]}),\n", + " ToolMessage(tool_call_id=tool_call[\"id\"], content=str(exception.exception)),\n", + " HumanMessage(\n", + " content=\"The last tool calls raised exceptions. Try calling the tools again with corrected arguments.\"\n", + " ),\n", + " ]\n", + " inputs[\"last_output\"] = messages\n", + " return inputs\n", + "\n", + "\n", + "# We add a last_output MessagesPlaceholder to our prompt which if not passed in doesn't\n", + "# affect the prompt at all, but gives us the option to insert an arbitrary list of Messages\n", + "# into the prompt if needed. We'll use this on retries to insert the error message.\n", + "prompt = ChatPromptTemplate.from_messages(\n", + " [(\"human\", \"{input}\"), MessagesPlaceholder(\"last_output\", optional=True)]\n", + ")\n", + "chain = (\n", + " prompt\n", + " | model_with_tools\n", + " | JsonOutputKeyToolsParser(\n", + " key_name=\"complex_tool\", return_id=True, return_single=True\n", + " )\n", + " | tool_custom_exception\n", + ")\n", + "\n", + "# If the initial chain call fails, we rerun it withe the exception passed in as a message.\n", + "self_correcting_chain = chain.with_fallbacks(\n", + " [exception_to_messages | chain], exception_key=\"exception\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "4c45f5bd-cbb4-47d5-b4b6-aec50673c750", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "10.5" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "self_correcting_chain.invoke(\n", + " {\n", + " \"input\": \"use complex tool. the args are 5, 2.1, empty dictionary. don't forget dict_arg\"\n", + " }\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "50d269a9-3cab-4a37-ba2f-805296453627", + "metadata": {}, + "source": [ + "And our chain succeeds! Looking at the [LangSmith trace](https://smith.langchain.com/public/b780b740-daf5-43aa-a217-6d4600aba41b/r), we can see that indeed our initial chain still fails, and it's only on retrying that the chain succeeds." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "poetry-venv", + "language": "python", + "name": "poetry-venv" + }, + "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.9.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/docs/use_cases/web_scraping.ipynb b/docs/docs/use_cases/web_scraping.ipynb index 1478220d64a1f..19848f19d6aa6 100644 --- a/docs/docs/use_cases/web_scraping.ipynb +++ b/docs/docs/use_cases/web_scraping.ipynb @@ -144,11 +144,11 @@ "\n", "### AsyncHtmlLoader\n", "\n", - "The [AsyncHtmlLoader](docs/integrations/document_loaders/async_html) uses the `aiohttp` library to make asynchronous HTTP requests, suitable for simpler and lightweight scraping.\n", + "The [AsyncHtmlLoader](/docs/integrations/document_loaders/async_html) uses the `aiohttp` library to make asynchronous HTTP requests, suitable for simpler and lightweight scraping.\n", "\n", "### AsyncChromiumLoader\n", "\n", - "The [AsyncChromiumLoader](docs/integrations/document_loaders/async_chromium) uses Playwright to launch a Chromium instance, which can handle JavaScript rendering and more complex web interactions.\n", + "The [AsyncChromiumLoader](/docs/integrations/document_loaders/async_chromium) uses Playwright to launch a Chromium instance, which can handle JavaScript rendering and more complex web interactions.\n", "\n", "Chromium is one of the browsers supported by Playwright, a library used to control browser automation. \n", "\n", @@ -178,7 +178,7 @@ "\n", "### HTML2Text\n", "\n", - "[HTML2Text](docs/integrations/document_transformers/html2text) provides a straightforward conversion of HTML content into plain text (with markdown-like formatting) without any specific tag manipulation. \n", + "[HTML2Text](/docs/integrations/document_transformers/html2text) provides a straightforward conversion of HTML content into plain text (with markdown-like formatting) without any specific tag manipulation. \n", "\n", "It's best suited for scenarios where the goal is to extract human-readable text without needing to manipulate specific HTML elements.\n", "\n", diff --git a/docs/static/img/ollama_example_img.jpg b/docs/static/img/ollama_example_img.jpg new file mode 100644 index 0000000000000..86149b798443b Binary files /dev/null and b/docs/static/img/ollama_example_img.jpg differ diff --git a/docs/static/img/self_querying.jpg b/docs/static/img/self_querying.jpg index 7970423415d7a..1f3055b53bdb4 100644 Binary files a/docs/static/img/self_querying.jpg and b/docs/static/img/self_querying.jpg differ diff --git a/docs/static/img/tool_agent.svg b/docs/static/img/tool_agent.svg new file mode 100644 index 0000000000000..0992054658df0 --- /dev/null +++ b/docs/static/img/tool_agent.svg @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/static/img/tool_chain.svg b/docs/static/img/tool_chain.svg new file mode 100644 index 0000000000000..35dc814e8040b --- /dev/null +++ b/docs/static/img/tool_chain.svg @@ -0,0 +1,306 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/vercel.json b/docs/vercel.json index 8fc8597da84bd..11ef901e656f8 100644 --- a/docs/vercel.json +++ b/docs/vercel.json @@ -1,5 +1,9 @@ { "redirects": [ + { + "source": "/docs/use_cases/qa_structured/sql", + "destination": "/docs/use_cases/sql/" + }, { "source": "/docs/contributing/packages", "destination": "/docs/packages" @@ -294,7 +298,7 @@ }, { "source": "/docs/use_cases/qa_structured/integrations/sqlite", - "destination": "/cookbook" + "destination": "/docs/use_cases/sql/" }, { "source": "/docs/use_cases/more/graph/tot", @@ -380,10 +384,6 @@ "source": "/docs/modules/agents/agents/examples/mrkl_chat(.html?)", "destination": "/docs/modules/agents/" }, - { - "source": "/docs/use_cases(/?)", - "destination": "/docs/use_cases/question_answering/" - }, { "source": "/docs/integrations(/?)", "destination": "/docs/integrations/providers/" @@ -1422,7 +1422,7 @@ }, { "source": "/docs/integrations/tools/sqlite", - "destination": "/docs/use_cases/qa_structured/sql" + "destination": "/docs/use_cases/sql" }, { "source": "/en/latest/modules/callbacks/filecallbackhandler.html", @@ -3510,11 +3510,7 @@ }, { "source": "/en/latest/use_cases/tabular.html", - "destination": "/docs/use_cases/qa_structured" - }, - { - "source": "/docs/use_cases/sql(/?)", - "destination": "/docs/use_cases/qa_structured/sql" + "destination": "/docs/use_cases/sql" }, { "source": "/en/latest/youtube.html", diff --git a/libs/cli/langchain_cli/integration_template/Makefile b/libs/cli/langchain_cli/integration_template/Makefile index cf748963e2263..bed6f5bda53a8 100644 --- a/libs/cli/langchain_cli/integration_template/Makefile +++ b/libs/cli/langchain_cli/integration_template/Makefile @@ -6,7 +6,9 @@ all: help # Define a variable for the test file path. TEST_FILE ?= tests/unit_tests/ -test: +integration_tests: TEST_FILE = tests/integration_tests/ + +test integration_tests: poetry run pytest $(TEST_FILE) tests: diff --git a/libs/cli/langchain_cli/integration_template/README.md b/libs/cli/langchain_cli/integration_template/README.md index e1f3e352472a9..19c73c4a5ba12 100644 --- a/libs/cli/langchain_cli/integration_template/README.md +++ b/libs/cli/langchain_cli/integration_template/README.md @@ -1 +1,45 @@ # __package_name__ + +This package contains the LangChain integration with __ModuleName__ + +## Installation + +```bash +pip install -U __package_name__ +``` + +And you should configure credentials by setting the following environment variables: + +* TODO: fill this out + +## Chat Models + +`Chat__ModuleName__` class exposes chat models from __ModuleName__. + +```python +from __module_name__ import Chat__ModuleName__ + +llm = Chat__ModuleName__() +llm.invoke("Sing a ballad of LangChain.") +``` + +## Embeddings + +`__ModuleName__Embeddings` class exposes embeddings from __ModuleName__. + +```python +from __module_name__ import __ModuleName__Embeddings + +embeddings = __ModuleName__Embeddings() +embeddings.embed_query("What is the meaning of life?") +``` + +## LLMs +`__ModuleName__LLM` class exposes LLMs from __ModuleName__. + +```python +from __module_name__ import __ModuleName__LLM + +llm = __ModuleName__LLM() +llm.invoke("The meaning of life is") +``` diff --git a/libs/cli/langchain_cli/integration_template/pyproject.toml b/libs/cli/langchain_cli/integration_template/pyproject.toml index 95d8fbcb1850b..b4703aa60e04c 100644 --- a/libs/cli/langchain_cli/integration_template/pyproject.toml +++ b/libs/cli/langchain_cli/integration_template/pyproject.toml @@ -4,6 +4,11 @@ version = "0.0.1" description = "An integration package connecting __ModuleName__ and LangChain" authors = [] readme = "README.md" +repository = "https://github.com/langchain-ai/langchain" +license = "MIT" + +[tool.poetry.urls] +"Source Code" = "https://github.com/langchain-ai/langchain/tree/master/libs/partners/__package_name_short__" [tool.poetry.dependencies] python = ">=3.8.1,<4.0" diff --git a/libs/cli/pyproject.toml b/libs/cli/pyproject.toml index 978a03052aa34..43b01fd8f5400 100644 --- a/libs/cli/pyproject.toml +++ b/libs/cli/pyproject.toml @@ -1,10 +1,14 @@ [tool.poetry] name = "langchain-cli" -version = "0.0.20" +version = "0.0.21" description = "CLI for interacting with LangChain" authors = ["Erick Friis "] -license = "MIT" readme = "README.md" +repository = "https://github.com/langchain-ai/langchain" +license = "MIT" + +[tool.poetry.urls] +"Source Code" = "https://github.com/langchain-ai/langchain/tree/master/libs/cli" [tool.poetry.dependencies] python = ">=3.8.1,<4.0" diff --git a/libs/community/_test_minimum_requirements.txt b/libs/community/_test_minimum_requirements.txt index 052ee52661d78..ce12ce8c6a6b7 100644 --- a/libs/community/_test_minimum_requirements.txt +++ b/libs/community/_test_minimum_requirements.txt @@ -1 +1 @@ -langchain-core==0.1.9 \ No newline at end of file +langchain-core==0.1.14 diff --git a/libs/community/langchain_community/agent_toolkits/__init__.py b/libs/community/langchain_community/agent_toolkits/__init__.py index b39e2751f7e9d..9b5279ee85fe5 100644 --- a/libs/community/langchain_community/agent_toolkits/__init__.py +++ b/libs/community/langchain_community/agent_toolkits/__init__.py @@ -34,6 +34,7 @@ from langchain_community.agent_toolkits.playwright.toolkit import ( PlayWrightBrowserToolkit, ) +from langchain_community.agent_toolkits.polygon.toolkit import PolygonToolkit from langchain_community.agent_toolkits.powerbi.base import create_pbi_agent from langchain_community.agent_toolkits.powerbi.chat_base import create_pbi_chat_agent from langchain_community.agent_toolkits.powerbi.toolkit import PowerBIToolkit @@ -59,6 +60,7 @@ "O365Toolkit", "OpenAPIToolkit", "PlayWrightBrowserToolkit", + "PolygonToolkit", "PowerBIToolkit", "SlackToolkit", "SteamToolkit", diff --git a/libs/community/langchain_community/agent_toolkits/polygon/__init__.py b/libs/community/langchain_community/agent_toolkits/polygon/__init__.py new file mode 100644 index 0000000000000..b9a3bf13b76c5 --- /dev/null +++ b/libs/community/langchain_community/agent_toolkits/polygon/__init__.py @@ -0,0 +1 @@ +"""Polygon Toolkit""" diff --git a/libs/community/langchain_community/agent_toolkits/polygon/toolkit.py b/libs/community/langchain_community/agent_toolkits/polygon/toolkit.py new file mode 100644 index 0000000000000..748c84c1eef9d --- /dev/null +++ b/libs/community/langchain_community/agent_toolkits/polygon/toolkit.py @@ -0,0 +1,27 @@ +from typing import List + +from langchain_community.agent_toolkits.base import BaseToolkit +from langchain_community.tools import BaseTool +from langchain_community.tools.polygon import PolygonLastQuote +from langchain_community.utilities.polygon import PolygonAPIWrapper + + +class PolygonToolkit(BaseToolkit): + """Polygon Toolkit.""" + + tools: List[BaseTool] = [] + + @classmethod + def from_polygon_api_wrapper( + cls, polygon_api_wrapper: PolygonAPIWrapper + ) -> "PolygonToolkit": + tools = [ + PolygonLastQuote( + api_wrapper=polygon_api_wrapper, + ) + ] + return cls(tools=tools) + + def get_tools(self) -> List[BaseTool]: + """Get the tools in the toolkit.""" + return self.tools diff --git a/libs/community/langchain_community/agent_toolkits/sql/base.py b/libs/community/langchain_community/agent_toolkits/sql/base.py index c2451f7c23162..66c3c57dc6e4c 100644 --- a/libs/community/langchain_community/agent_toolkits/sql/base.py +++ b/libs/community/langchain_community/agent_toolkits/sql/base.py @@ -1,11 +1,11 @@ """SQL agent.""" from __future__ import annotations -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence +import warnings +from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Sequence, Union -from langchain_core.callbacks import BaseCallbackManager -from langchain_core.language_models import BaseLanguageModel from langchain_core.messages import AIMessage, SystemMessage +from langchain_core.prompts import BasePromptTemplate, PromptTemplate from langchain_core.prompts.chat import ( ChatPromptTemplate, HumanMessagePromptTemplate, @@ -15,22 +15,29 @@ from langchain_community.agent_toolkits.sql.prompt import ( SQL_FUNCTIONS_SUFFIX, SQL_PREFIX, - SQL_SUFFIX, ) from langchain_community.agent_toolkits.sql.toolkit import SQLDatabaseToolkit -from langchain_community.tools import BaseTool +from langchain_community.tools.sql_database.tool import ( + InfoSQLDatabaseTool, + ListSQLDatabaseTool, +) if TYPE_CHECKING: from langchain.agents.agent import AgentExecutor from langchain.agents.agent_types import AgentType + from langchain_core.callbacks import BaseCallbackManager + from langchain_core.language_models import BaseLanguageModel + from langchain_core.tools import BaseTool + + from langchain_community.utilities.sql_database import SQLDatabase def create_sql_agent( llm: BaseLanguageModel, - toolkit: SQLDatabaseToolkit, - agent_type: Optional[AgentType] = None, + toolkit: Optional[SQLDatabaseToolkit] = None, + agent_type: Optional[Union[AgentType, Literal["openai-tools"]]] = None, callback_manager: Optional[BaseCallbackManager] = None, - prefix: str = SQL_PREFIX, + prefix: Optional[str] = None, suffix: Optional[str] = None, format_instructions: Optional[str] = None, input_variables: Optional[List[str]] = None, @@ -41,62 +48,165 @@ def create_sql_agent( verbose: bool = False, agent_executor_kwargs: Optional[Dict[str, Any]] = None, extra_tools: Sequence[BaseTool] = (), + *, + db: Optional[SQLDatabase] = None, + prompt: Optional[BasePromptTemplate] = None, **kwargs: Any, ) -> AgentExecutor: - """Construct an SQL agent from an LLM and tools.""" - from langchain.agents.agent import AgentExecutor, BaseSingleActionAgent + """Construct a SQL agent from an LLM and toolkit or database. + + Args: + llm: Language model to use for the agent. + toolkit: SQLDatabaseToolkit for the agent to use. Must provide exactly one of + 'toolkit' or 'db'. Specify 'toolkit' if you want to use a different model + for the agent and the toolkit. + agent_type: One of "openai-tools", "openai-functions", or + "zero-shot-react-description". Defaults to "zero-shot-react-description". + "openai-tools" is recommended over "openai-functions". + callback_manager: DEPRECATED. Pass "callbacks" key into 'agent_executor_kwargs' + instead to pass constructor callbacks to AgentExecutor. + prefix: Prompt prefix string. Must contain variables "top_k" and "dialect". + suffix: Prompt suffix string. Default depends on agent type. + format_instructions: Formatting instructions to pass to + ZeroShotAgent.create_prompt() when 'agent_type' is + "zero-shot-react-description". Otherwise ignored. + input_variables: DEPRECATED. Input variables to explicitly specify as part of + ZeroShotAgent.create_prompt() when 'agent_type' is + "zero-shot-react-description". Otherwise ignored. + top_k: Number of rows to query for by default. + max_iterations: Passed to AgentExecutor init. + max_execution_time: Passed to AgentExecutor init. + early_stopping_method: Passed to AgentExecutor init. + verbose: AgentExecutor verbosity. + agent_executor_kwargs: Arbitrary additional AgentExecutor args. + extra_tools: Additional tools to give to agent on top of the ones that come with + SQLDatabaseToolkit. + db: SQLDatabase from which to create a SQLDatabaseToolkit. Toolkit is created + using 'db' and 'llm'. Must provide exactly one of 'db' or 'toolkit'. + prompt: Complete agent prompt. prompt and {prefix, suffix, format_instructions, + input_variables} are mutually exclusive. + **kwargs: DEPRECATED. Not used, kept for backwards compatibility. + + Returns: + An AgentExecutor with the specified agent_type agent. + + Example: + + .. code-block:: python + + from langchain_openai import ChatOpenAI + from langchain_community.agent_toolkits import create_sql_agent + from langchain_community.utilities import SQLDatabase + + db = SQLDatabase.from_uri("sqlite:///Chinook.db") + llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0) + agent_executor = create_sql_agent(llm, db=db, agent_type="openai-tools", verbose=True) + + """ # noqa: E501 + from langchain.agents import ( + create_openai_functions_agent, + create_openai_tools_agent, + create_react_agent, + ) + from langchain.agents.agent import ( + AgentExecutor, + RunnableAgent, + RunnableMultiActionAgent, + ) from langchain.agents.agent_types import AgentType - from langchain.agents.mrkl.base import ZeroShotAgent - from langchain.agents.openai_functions_agent.base import OpenAIFunctionsAgent - from langchain.chains.llm import LLMChain + if toolkit is None and db is None: + raise ValueError( + "Must provide exactly one of 'toolkit' or 'db'. Received neither." + ) + if toolkit and db: + raise ValueError( + "Must provide exactly one of 'toolkit' or 'db'. Received both." + ) + if kwargs: + warnings.warn( + f"Received additional kwargs {kwargs} which are no longer supported." + ) + + toolkit = toolkit or SQLDatabaseToolkit(llm=llm, db=db) agent_type = agent_type or AgentType.ZERO_SHOT_REACT_DESCRIPTION tools = toolkit.get_tools() + list(extra_tools) - prefix = prefix.format(dialect=toolkit.dialect, top_k=top_k) - agent: BaseSingleActionAgent + if prompt is None: + prefix = prefix or SQL_PREFIX + prefix = prefix.format(dialect=toolkit.dialect, top_k=top_k) + else: + if "top_k" in prompt.input_variables: + prompt = prompt.partial(top_k=str(top_k)) + if "dialect" in prompt.input_variables: + prompt = prompt.partial(dialect=toolkit.dialect) + db_context = toolkit.get_context() + if "table_info" in prompt.input_variables: + prompt = prompt.partial(table_info=db_context["table_info"]) + tools = [ + tool for tool in tools if not isinstance(tool, InfoSQLDatabaseTool) + ] + if "table_names" in prompt.input_variables: + prompt = prompt.partial(table_names=db_context["table_names"]) + tools = [ + tool for tool in tools if not isinstance(tool, ListSQLDatabaseTool) + ] if agent_type == AgentType.ZERO_SHOT_REACT_DESCRIPTION: - prompt_params = ( - {"format_instructions": format_instructions} - if format_instructions is not None - else {} - ) - prompt = ZeroShotAgent.create_prompt( - tools, - prefix=prefix, - suffix=suffix or SQL_SUFFIX, - input_variables=input_variables, - **prompt_params, - ) - llm_chain = LLMChain( - llm=llm, - prompt=prompt, - callback_manager=callback_manager, + if prompt is None: + from langchain.agents.mrkl import prompt as react_prompt + + format_instructions = ( + format_instructions or react_prompt.FORMAT_INSTRUCTIONS + ) + template = "\n\n".join( + [ + react_prompt.PREFIX, + "{tools}", + format_instructions, + react_prompt.SUFFIX, + ] + ) + prompt = PromptTemplate.from_template(template) + agent = RunnableAgent( + runnable=create_react_agent(llm, tools, prompt), + input_keys_arg=["input"], + return_keys_arg=["output"], ) - tool_names = [tool.name for tool in tools] - agent = ZeroShotAgent(llm_chain=llm_chain, allowed_tools=tool_names, **kwargs) elif agent_type == AgentType.OPENAI_FUNCTIONS: - messages = [ - SystemMessage(content=prefix), - HumanMessagePromptTemplate.from_template("{input}"), - AIMessage(content=suffix or SQL_FUNCTIONS_SUFFIX), - MessagesPlaceholder(variable_name="agent_scratchpad"), - ] - input_variables = ["input", "agent_scratchpad"] - _prompt = ChatPromptTemplate(input_variables=input_variables, messages=messages) - - agent = OpenAIFunctionsAgent( - llm=llm, - prompt=_prompt, - tools=tools, - callback_manager=callback_manager, - **kwargs, + if prompt is None: + messages = [ + SystemMessage(content=prefix), + HumanMessagePromptTemplate.from_template("{input}"), + AIMessage(content=suffix or SQL_FUNCTIONS_SUFFIX), + MessagesPlaceholder(variable_name="agent_scratchpad"), + ] + prompt = ChatPromptTemplate.from_messages(messages) + agent = RunnableAgent( + runnable=create_openai_functions_agent(llm, tools, prompt), + input_keys_arg=["input"], + return_keys_arg=["output"], ) + elif agent_type == "openai-tools": + if prompt is None: + messages = [ + SystemMessage(content=prefix), + HumanMessagePromptTemplate.from_template("{input}"), + AIMessage(content=suffix or SQL_FUNCTIONS_SUFFIX), + MessagesPlaceholder(variable_name="agent_scratchpad"), + ] + prompt = ChatPromptTemplate.from_messages(messages) + agent = RunnableMultiActionAgent( + runnable=create_openai_tools_agent(llm, tools, prompt), + input_keys_arg=["input"], + return_keys_arg=["output"], + ) + else: raise ValueError(f"Agent type {agent_type} not supported at the moment.") - return AgentExecutor.from_agent_and_tools( + return AgentExecutor( + name="SQL Agent Executor", agent=agent, tools=tools, callback_manager=callback_manager, diff --git a/libs/community/langchain_community/agent_toolkits/sql/toolkit.py b/libs/community/langchain_community/agent_toolkits/sql/toolkit.py index 382fcbb485653..6944b557f20dd 100644 --- a/libs/community/langchain_community/agent_toolkits/sql/toolkit.py +++ b/libs/community/langchain_community/agent_toolkits/sql/toolkit.py @@ -69,3 +69,7 @@ def get_tools(self) -> List[BaseTool]: list_sql_database_tool, query_sql_checker_tool, ] + + def get_context(self) -> dict: + """Return db context that you may want in agent prompt.""" + return self.db.get_context() diff --git a/libs/community/langchain_community/callbacks/manager.py b/libs/community/langchain_community/callbacks/manager.py index 196afed3d0fe5..ec03a8234530c 100644 --- a/libs/community/langchain_community/callbacks/manager.py +++ b/libs/community/langchain_community/callbacks/manager.py @@ -11,6 +11,7 @@ from langchain_core.tracers.context import register_configure_hook from langchain_community.callbacks.openai_info import OpenAICallbackHandler +from langchain_community.callbacks.tracers.comet import CometTracer from langchain_community.callbacks.tracers.wandb import WandbTracer logger = logging.getLogger(__name__) @@ -21,11 +22,17 @@ wandb_tracing_callback_var: ContextVar[Optional[WandbTracer]] = ContextVar( # noqa: E501 "tracing_wandb_callback", default=None ) +comet_tracing_callback_var: ContextVar[Optional[CometTracer]] = ContextVar( # noqa: E501 + "tracing_comet_callback", default=None +) register_configure_hook(openai_callback_var, True) register_configure_hook( wandb_tracing_callback_var, True, WandbTracer, "LANGCHAIN_WANDB_TRACING" ) +register_configure_hook( + comet_tracing_callback_var, True, CometTracer, "LANGCHAIN_COMET_TRACING" +) @contextmanager diff --git a/libs/community/langchain_community/callbacks/mlflow_callback.py b/libs/community/langchain_community/callbacks/mlflow_callback.py index 6d93125d564de..4070d17d61cf9 100644 --- a/libs/community/langchain_community/callbacks/mlflow_callback.py +++ b/libs/community/langchain_community/callbacks/mlflow_callback.py @@ -1,3 +1,4 @@ +import logging import os import random import string @@ -5,10 +6,11 @@ import traceback from copy import deepcopy from pathlib import Path -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Optional, Sequence, Union from langchain_core.agents import AgentAction, AgentFinish from langchain_core.callbacks import BaseCallbackHandler +from langchain_core.documents import Document from langchain_core.outputs import LLMResult from langchain_core.utils import get_from_dict_or_env @@ -21,6 +23,8 @@ import_textstat, ) +logger = logging.getLogger(__name__) + def import_mlflow() -> Any: """Import the mlflow python package and raise an error if it is not installed.""" @@ -34,6 +38,47 @@ def import_mlflow() -> Any: return mlflow +def mlflow_callback_metrics() -> List[str]: + return [ + "step", + "starts", + "ends", + "errors", + "text_ctr", + "chain_starts", + "chain_ends", + "llm_starts", + "llm_ends", + "llm_streams", + "tool_starts", + "tool_ends", + "agent_ends", + "retriever_starts", + "retriever_ends", + ] + + +def get_text_complexity_metrics() -> List[str]: + return [ + "flesch_reading_ease", + "flesch_kincaid_grade", + "smog_index", + "coleman_liau_index", + "automated_readability_index", + "dale_chall_readability_score", + "difficult_words", + "linsear_write_formula", + "gunning_fog", + # "text_standard" + "fernandez_huerta", + "szigriszt_pazos", + "gutierrez_polini", + "crawford", + "gulpease_index", + "osman", + ] + + def analyze_text( text: str, nlp: Any = None, @@ -52,22 +97,7 @@ def analyze_text( textstat = import_textstat() spacy = import_spacy() text_complexity_metrics = { - "flesch_reading_ease": textstat.flesch_reading_ease(text), - "flesch_kincaid_grade": textstat.flesch_kincaid_grade(text), - "smog_index": textstat.smog_index(text), - "coleman_liau_index": textstat.coleman_liau_index(text), - "automated_readability_index": textstat.automated_readability_index(text), - "dale_chall_readability_score": textstat.dale_chall_readability_score(text), - "difficult_words": textstat.difficult_words(text), - "linsear_write_formula": textstat.linsear_write_formula(text), - "gunning_fog": textstat.gunning_fog(text), - # "text_standard": textstat.text_standard(text), - "fernandez_huerta": textstat.fernandez_huerta(text), - "szigriszt_pazos": textstat.szigriszt_pazos(text), - "gutierrez_polini": textstat.gutierrez_polini(text), - "crawford": textstat.crawford(text), - "gulpease_index": textstat.gulpease_index(text), - "osman": textstat.osman(text), + key: getattr(textstat, key)(text) for key in get_text_complexity_metrics() } resp.update({"text_complexity_metrics": text_complexity_metrics}) resp.update(text_complexity_metrics) @@ -140,58 +170,64 @@ def __init__(self, **kwargs: Any): ) self.mlflow.set_tracking_uri(tracking_uri) - # User can set other env variables described here - # > https://www.mlflow.org/docs/latest/tracking.html#logging-to-a-tracking-server - - experiment_name = get_from_dict_or_env( - kwargs, "experiment_name", "MLFLOW_EXPERIMENT_NAME" - ) - self.mlf_exp = self.mlflow.get_experiment_by_name(experiment_name) - if self.mlf_exp is not None: - self.mlf_expid = self.mlf_exp.experiment_id + if run_id := kwargs.get("run_id"): + self.mlf_expid = self.mlflow.get_run(run_id).info.experiment_id else: - self.mlf_expid = self.mlflow.create_experiment(experiment_name) - - self.start_run(kwargs["run_name"], kwargs["run_tags"]) + # User can set other env variables described here + # > https://www.mlflow.org/docs/latest/tracking.html#logging-to-a-tracking-server - def start_run(self, name: str, tags: Dict[str, str]) -> None: - """To start a new run, auto generates the random suffix for name""" - if name.endswith("-%"): - rname = "".join(random.choices(string.ascii_uppercase + string.digits, k=7)) - name = name.replace("%", rname) - self.run = self.mlflow.MlflowClient().create_run( - self.mlf_expid, run_name=name, tags=tags + experiment_name = get_from_dict_or_env( + kwargs, "experiment_name", "MLFLOW_EXPERIMENT_NAME" + ) + self.mlf_exp = self.mlflow.get_experiment_by_name(experiment_name) + if self.mlf_exp is not None: + self.mlf_expid = self.mlf_exp.experiment_id + else: + self.mlf_expid = self.mlflow.create_experiment(experiment_name) + + self.start_run( + kwargs["run_name"], kwargs["run_tags"], kwargs.get("run_id", None) ) + self.dir = kwargs.get("artifacts_dir", "") + + def start_run( + self, name: str, tags: Dict[str, str], run_id: Optional[str] = None + ) -> None: + """ + If run_id is provided, it will reuse the run with the given run_id. + Otherwise, it starts a new run, auto generates the random suffix for name. + """ + if run_id is None: + if name.endswith("-%"): + rname = "".join( + random.choices(string.ascii_uppercase + string.digits, k=7) + ) + name = name[:-1] + rname + run = self.mlflow.MlflowClient().create_run( + self.mlf_expid, run_name=name, tags=tags + ) + run_id = run.info.run_id + self.run_id = run_id def finish_run(self) -> None: """To finish the run.""" - with self.mlflow.start_run( - run_id=self.run.info.run_id, experiment_id=self.mlf_expid - ): - self.mlflow.end_run() + self.mlflow.end_run() def metric(self, key: str, value: float) -> None: """To log metric to mlflow server.""" - with self.mlflow.start_run( - run_id=self.run.info.run_id, experiment_id=self.mlf_expid - ): - self.mlflow.log_metric(key, value) + self.mlflow.log_metric(key, value, run_id=self.run_id) def metrics( self, data: Union[Dict[str, float], Dict[str, int]], step: Optional[int] = 0 ) -> None: """To log all metrics in the input dict.""" - with self.mlflow.start_run( - run_id=self.run.info.run_id, experiment_id=self.mlf_expid - ): - self.mlflow.log_metrics(data) + self.mlflow.log_metrics(data, run_id=self.run_id) def jsonf(self, data: Dict[str, Any], filename: str) -> None: """To log the input data as json file artifact.""" - with self.mlflow.start_run( - run_id=self.run.info.run_id, experiment_id=self.mlf_expid - ): - self.mlflow.log_dict(data, f"{filename}.json") + self.mlflow.log_dict( + data, os.path.join(self.dir, f"{filename}.json"), run_id=self.run_id + ) def table(self, name: str, dataframe) -> None: # type: ignore """To log the input pandas dataframe as a html table""" @@ -199,30 +235,22 @@ def table(self, name: str, dataframe) -> None: # type: ignore def html(self, html: str, filename: str) -> None: """To log the input html string as html file artifact.""" - with self.mlflow.start_run( - run_id=self.run.info.run_id, experiment_id=self.mlf_expid - ): - self.mlflow.log_text(html, f"{filename}.html") + self.mlflow.log_text( + html, os.path.join(self.dir, f"{filename}.html"), run_id=self.run_id + ) def text(self, text: str, filename: str) -> None: """To log the input text as text file artifact.""" - with self.mlflow.start_run( - run_id=self.run.info.run_id, experiment_id=self.mlf_expid - ): - self.mlflow.log_text(text, f"{filename}.txt") + self.mlflow.log_text( + text, os.path.join(self.dir, f"{filename}.txt"), run_id=self.run_id + ) def artifact(self, path: str) -> None: """To upload the file from given path as artifact.""" - with self.mlflow.start_run( - run_id=self.run.info.run_id, experiment_id=self.mlf_expid - ): - self.mlflow.log_artifact(path) + self.mlflow.log_artifact(path, run_id=self.run_id) def langchain_artifact(self, chain: Any) -> None: - with self.mlflow.start_run( - run_id=self.run.info.run_id, experiment_id=self.mlf_expid - ): - self.mlflow.langchain.log_model(chain, "langchain-model") + self.mlflow.langchain.log_model(chain, "langchain-model", run_id=self.run_id) class MlflowCallbackHandler(BaseMetadataCallbackHandler, BaseCallbackHandler): @@ -246,6 +274,8 @@ def __init__( experiment: Optional[str] = "langchain", tags: Optional[Dict] = None, tracking_uri: Optional[str] = None, + run_id: Optional[str] = None, + artifacts_dir: str = "", ) -> None: """Initialize callback handler.""" import_pandas() @@ -258,6 +288,8 @@ def __init__( self.experiment = experiment self.tags = tags or {} self.tracking_uri = tracking_uri + self.run_id = run_id + self.artifacts_dir = artifacts_dir self.temp_dir = tempfile.TemporaryDirectory() @@ -266,26 +298,21 @@ def __init__( experiment_name=self.experiment, run_name=self.name, run_tags=self.tags, + run_id=self.run_id, + artifacts_dir=self.artifacts_dir, ) self.action_records: list = [] - self.nlp = spacy.load("en_core_web_sm") - - self.metrics = { - "step": 0, - "starts": 0, - "ends": 0, - "errors": 0, - "text_ctr": 0, - "chain_starts": 0, - "chain_ends": 0, - "llm_starts": 0, - "llm_ends": 0, - "llm_streams": 0, - "tool_starts": 0, - "tool_ends": 0, - "agent_ends": 0, - } + try: + self.nlp = spacy.load("en_core_web_sm") + except OSError: + logger.warning( + "Run `python -m spacy download en_core_web_sm` " + "to download en_core_web_sm model for text visualization." + ) + self.nlp = None + + self.metrics = {key: 0 for key in mlflow_callback_metrics()} self.records: Dict[str, Any] = { "on_llm_start_records": [], @@ -298,6 +325,8 @@ def __init__( "on_text_records": [], "on_agent_finish_records": [], "on_agent_action_records": [], + "on_retriever_start_records": [], + "on_retriever_end_records": [], "action_records": [], } @@ -383,10 +412,14 @@ def on_llm_end(self, response: LLMResult, **kwargs: Any) -> None: self.records["on_llm_end_records"].append(generation_resp) self.records["action_records"].append(generation_resp) self.mlflg.jsonf(resp, f"llm_end_{llm_ends}_generation_{idx}") - dependency_tree = generation_resp["dependency_tree"] - entities = generation_resp["entities"] - self.mlflg.html(dependency_tree, "dep-" + hash_string(generation.text)) - self.mlflg.html(entities, "ent-" + hash_string(generation.text)) + if "dependency_tree" in generation_resp: + dependency_tree = generation_resp["dependency_tree"] + self.mlflg.html( + dependency_tree, "dep-" + hash_string(generation.text) + ) + if "entities" in generation_resp: + entities = generation_resp["entities"] + self.mlflg.html(entities, "ent-" + hash_string(generation.text)) def on_llm_error(self, error: BaseException, **kwargs: Any) -> None: """Run when LLM errors.""" @@ -410,14 +443,21 @@ def on_chain_start( self.mlflg.metrics(self.metrics, step=self.metrics["step"]) - chain_input = ",".join([f"{k}={v}" for k, v in inputs.items()]) + if isinstance(inputs, dict): + chain_input = ",".join([f"{k}={v}" for k, v in inputs.items()]) + elif isinstance(inputs, list): + chain_input = ",".join([str(input) for input in inputs]) + else: + chain_input = str(inputs) input_resp = deepcopy(resp) input_resp["inputs"] = chain_input self.records["on_chain_start_records"].append(input_resp) self.records["action_records"].append(input_resp) self.mlflg.jsonf(input_resp, f"chain_start_{chain_starts}") - def on_chain_end(self, outputs: Dict[str, Any], **kwargs: Any) -> None: + def on_chain_end( + self, outputs: Union[Dict[str, Any], str, List[str]], **kwargs: Any + ) -> None: """Run when chain ends running.""" self.metrics["step"] += 1 self.metrics["chain_ends"] += 1 @@ -426,7 +466,12 @@ def on_chain_end(self, outputs: Dict[str, Any], **kwargs: Any) -> None: chain_ends = self.metrics["chain_ends"] resp: Dict[str, Any] = {} - chain_output = ",".join([f"{k}={v}" for k, v in outputs.items()]) + if isinstance(outputs, dict): + chain_output = ",".join([f"{k}={v}" for k, v in outputs.items()]) + elif isinstance(outputs, list): + chain_output = ",".join(map(str, outputs)) + else: + chain_output = str(outputs) resp.update({"action": "on_chain_end", "outputs": chain_output}) resp.update(self.metrics) @@ -487,7 +532,7 @@ def on_tool_error(self, error: BaseException, **kwargs: Any) -> None: def on_text(self, text: str, **kwargs: Any) -> None: """ - Run when agent is ending. + Run when text is received. """ self.metrics["step"] += 1 self.metrics["text_ctr"] += 1 @@ -549,6 +594,69 @@ def on_agent_action(self, action: AgentAction, **kwargs: Any) -> Any: self.records["action_records"].append(resp) self.mlflg.jsonf(resp, f"agent_action_{tool_starts}") + def on_retriever_start( + self, + serialized: Dict[str, Any], + query: str, + **kwargs: Any, + ) -> Any: + """Run when Retriever starts running.""" + self.metrics["step"] += 1 + self.metrics["retriever_starts"] += 1 + self.metrics["starts"] += 1 + + retriever_starts = self.metrics["retriever_starts"] + + resp: Dict[str, Any] = {} + resp.update({"action": "on_retriever_start", "query": query}) + resp.update(flatten_dict(serialized)) + resp.update(self.metrics) + + self.mlflg.metrics(self.metrics, step=self.metrics["step"]) + + self.records["on_retriever_start_records"].append(resp) + self.records["action_records"].append(resp) + self.mlflg.jsonf(resp, f"retriever_start_{retriever_starts}") + + def on_retriever_end( + self, + documents: Sequence[Document], + **kwargs: Any, + ) -> Any: + """Run when Retriever ends running.""" + self.metrics["step"] += 1 + self.metrics["retriever_ends"] += 1 + self.metrics["ends"] += 1 + + retriever_ends = self.metrics["retriever_ends"] + + resp: Dict[str, Any] = {} + retriever_documents = [ + { + "page_content": doc.page_content, + "metadata": { + k: str(v) + if not isinstance(v, list) + else ",".join(str(x) for x in v) + for k, v in doc.metadata.items() + }, + } + for doc in documents + ] + resp.update({"action": "on_retriever_end", "documents": retriever_documents}) + resp.update(self.metrics) + + self.mlflg.metrics(self.metrics, step=self.metrics["step"]) + + self.records["on_retriever_end_records"].append(resp) + self.records["action_records"].append(resp) + self.mlflg.jsonf(resp, f"retriever_end_{retriever_ends}") + + def on_retriever_error(self, error: BaseException, **kwargs: Any) -> Any: + """Run when Retriever errors.""" + self.metrics["step"] += 1 + self.metrics["errors"] += 1 + def _create_session_analysis_df(self) -> Any: """Create a dataframe with all the information from the session.""" pd = import_pandas() @@ -570,39 +678,27 @@ def _create_session_analysis_df(self) -> Any: .dropna(axis=1) .rename({"step": "prompt_step"}, axis=1) ) - complexity_metrics_columns = [] - visualizations_columns = [] - - complexity_metrics_columns = [ - "flesch_reading_ease", - "flesch_kincaid_grade", - "smog_index", - "coleman_liau_index", - "automated_readability_index", - "dale_chall_readability_score", - "difficult_words", - "linsear_write_formula", - "gunning_fog", - # "text_standard", - "fernandez_huerta", - "szigriszt_pazos", - "gutierrez_polini", - "crawford", - "gulpease_index", - "osman", - ] + complexity_metrics_columns = get_text_complexity_metrics() + visualizations_columns = ( + ["dependency_tree", "entities"] if self.nlp is not None else [] + ) - visualizations_columns = ["dependency_tree", "entities"] + token_usage_columns = [ + "token_usage_total_tokens", + "token_usage_prompt_tokens", + "token_usage_completion_tokens", + ] + token_usage_columns = [ + x for x in token_usage_columns if x in on_llm_end_records_df.columns + ] llm_outputs_df = ( on_llm_end_records_df[ [ "step", "text", - "token_usage_total_tokens", - "token_usage_prompt_tokens", - "token_usage_completion_tokens", ] + + token_usage_columns + complexity_metrics_columns + visualizations_columns ] @@ -620,14 +716,18 @@ def _create_session_analysis_df(self) -> Any: ) return session_analysis_df + def _contain_llm_records(self): + return bool(self.records["on_llm_start_records"]) + def flush_tracker(self, langchain_asset: Any = None, finish: bool = False) -> None: pd = import_pandas() self.mlflg.table("action_records", pd.DataFrame(self.records["action_records"])) - session_analysis_df = self._create_session_analysis_df() - chat_html = session_analysis_df.pop("chat_html") - chat_html = chat_html.replace("\n", "", regex=True) - self.mlflg.table("session_analysis", pd.DataFrame(session_analysis_df)) - self.mlflg.html("".join(chat_html.tolist()), "chat_html") + if self._contain_llm_records(): + session_analysis_df = self._create_session_analysis_df() + chat_html = session_analysis_df.pop("chat_html") + chat_html = chat_html.replace("\n", "", regex=True) + self.mlflg.table("session_analysis", pd.DataFrame(session_analysis_df)) + self.mlflg.html("".join(chat_html.tolist()), "chat_html") if langchain_asset: # To avoid circular import error diff --git a/libs/community/langchain_community/callbacks/openai_info.py b/libs/community/langchain_community/callbacks/openai_info.py index 58cb6aab3b8ac..d18059c119341 100644 --- a/libs/community/langchain_community/callbacks/openai_info.py +++ b/libs/community/langchain_community/callbacks/openai_info.py @@ -68,10 +68,12 @@ "babbage-002-finetuned": 0.0016, "davinci-002-finetuned": 0.012, "gpt-3.5-turbo-0613-finetuned": 0.012, + "gpt-3.5-turbo-1106-finetuned": 0.012, # Fine Tuned output "babbage-002-finetuned-completion": 0.0016, "davinci-002-finetuned-completion": 0.012, "gpt-3.5-turbo-0613-finetuned-completion": 0.016, + "gpt-3.5-turbo-1106-finetuned-completion": 0.016, # Azure Fine Tuned input "babbage-002-azure-finetuned": 0.0004, "davinci-002-azure-finetuned": 0.002, diff --git a/libs/community/langchain_community/chat_message_histories/__init__.py b/libs/community/langchain_community/chat_message_histories/__init__.py index a45ecb7ead6e2..8803810c1bb7e 100644 --- a/libs/community/langchain_community/chat_message_histories/__init__.py +++ b/libs/community/langchain_community/chat_message_histories/__init__.py @@ -35,6 +35,7 @@ from langchain_community.chat_message_histories.streamlit import ( StreamlitChatMessageHistory, ) +from langchain_community.chat_message_histories.tidb import TiDBChatMessageHistory from langchain_community.chat_message_histories.upstash_redis import ( UpstashRedisChatMessageHistory, ) @@ -62,4 +63,5 @@ "ZepChatMessageHistory", "UpstashRedisChatMessageHistory", "Neo4jChatMessageHistory", + "TiDBChatMessageHistory", ] diff --git a/libs/community/langchain_community/chat_message_histories/tidb.py b/libs/community/langchain_community/chat_message_histories/tidb.py new file mode 100644 index 0000000000000..bfa36ad06ffa5 --- /dev/null +++ b/libs/community/langchain_community/chat_message_histories/tidb.py @@ -0,0 +1,148 @@ +import json +import logging +from datetime import datetime +from typing import List, Optional + +from langchain_core.chat_history import BaseChatMessageHistory +from langchain_core.messages import BaseMessage, message_to_dict, messages_from_dict +from sqlalchemy import create_engine, text +from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.orm import sessionmaker + +logger = logging.getLogger(__name__) + + +class TiDBChatMessageHistory(BaseChatMessageHistory): + """ + Represents a chat message history stored in a TiDB database. + """ + + def __init__( + self, + session_id: str, + connection_string: str, + table_name: str = "langchain_message_store", + earliest_time: Optional[datetime] = None, + ): + """ + Initializes a new instance of the TiDBChatMessageHistory class. + + Args: + session_id (str): The ID of the chat session. + connection_string (str): The connection string for the TiDB database. + format: mysql+pymysql://:@:4000/?ssl_ca=/etc/ssl/cert.pem&ssl_verify_cert=true&ssl_verify_identity=true + table_name (str, optional): the table name to store the chat messages. + Defaults to "langchain_message_store". + earliest_time (Optional[datetime], optional): The earliest time to retrieve messages from. + Defaults to None. + """ # noqa + + self.session_id = session_id + self.table_name = table_name + self.earliest_time = earliest_time + self.cache = [] + + # Set up SQLAlchemy engine and session + self.engine = create_engine(connection_string) + Session = sessionmaker(bind=self.engine) + self.session = Session() + + self._create_table_if_not_exists() + self._load_messages_to_cache() + + def _create_table_if_not_exists(self) -> None: + """ + Creates a table if it does not already exist in the database. + """ + + create_table_query = text( + f""" + CREATE TABLE IF NOT EXISTS {self.table_name} ( + id INT AUTO_INCREMENT PRIMARY KEY, + session_id VARCHAR(255) NOT NULL, + message JSON NOT NULL, + create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX session_idx (session_id) + );""" + ) + try: + self.session.execute(create_table_query) + self.session.commit() + except SQLAlchemyError as e: + logger.error(f"Error creating table: {e}") + self.session.rollback() + + def _load_messages_to_cache(self) -> None: + """ + Loads messages from the database into the cache. + + This method retrieves messages from the database table. The retrieved messages + are then stored in the cache for faster access. + + Raises: + SQLAlchemyError: If there is an error executing the database query. + + """ + time_condition = ( + f"AND create_time >= '{self.earliest_time}'" if self.earliest_time else "" + ) + query = text( + f""" + SELECT message FROM {self.table_name} + WHERE session_id = :session_id {time_condition} + ORDER BY id; + """ + ) + try: + result = self.session.execute(query, {"session_id": self.session_id}) + for record in result.fetchall(): + message_dict = json.loads(record[0]) + self.cache.append(messages_from_dict([message_dict])[0]) + except SQLAlchemyError as e: + logger.error(f"Error loading messages to cache: {e}") + + @property + def messages(self) -> List[BaseMessage]: + """returns all messages""" + if len(self.cache) == 0: + self.reload_cache() + return self.cache + + def add_message(self, message: BaseMessage) -> None: + """adds a message to the database and cache""" + query = text( + f"INSERT INTO {self.table_name} (session_id, message) VALUES (:session_id, :message);" # noqa + ) + try: + self.session.execute( + query, + { + "session_id": self.session_id, + "message": json.dumps(message_to_dict(message)), + }, + ) + self.session.commit() + self.cache.append(message) + except SQLAlchemyError as e: + logger.error(f"Error adding message: {e}") + self.session.rollback() + + def clear(self) -> None: + """clears all messages""" + query = text(f"DELETE FROM {self.table_name} WHERE session_id = :session_id;") + try: + self.session.execute(query, {"session_id": self.session_id}) + self.session.commit() + self.cache.clear() + except SQLAlchemyError as e: + logger.error(f"Error clearing messages: {e}") + self.session.rollback() + + def reload_cache(self) -> None: + """reloads messages from database to cache""" + self.cache.clear() + self._load_messages_to_cache() + + def __del__(self) -> None: + """closes the session""" + self.session.close() diff --git a/libs/community/langchain_community/chat_models/__init__.py b/libs/community/langchain_community/chat_models/__init__.py index aec1fcb8784b4..bc35a3c4ef6ae 100644 --- a/libs/community/langchain_community/chat_models/__init__.py +++ b/libs/community/langchain_community/chat_models/__init__.py @@ -25,6 +25,7 @@ from langchain_community.chat_models.bedrock import BedrockChat from langchain_community.chat_models.cohere import ChatCohere from langchain_community.chat_models.databricks import ChatDatabricks +from langchain_community.chat_models.deepinfra import ChatDeepInfra from langchain_community.chat_models.ernie import ErnieBotChat from langchain_community.chat_models.everlyai import ChatEverlyAI from langchain_community.chat_models.fake import FakeListChatModel @@ -47,6 +48,7 @@ from langchain_community.chat_models.openai import ChatOpenAI from langchain_community.chat_models.pai_eas_endpoint import PaiEasChatEndpoint from langchain_community.chat_models.promptlayer_openai import PromptLayerChatOpenAI +from langchain_community.chat_models.sparkllm import ChatSparkLLM from langchain_community.chat_models.tongyi import ChatTongyi from langchain_community.chat_models.vertexai import ChatVertexAI from langchain_community.chat_models.volcengine_maas import VolcEngineMaasChat @@ -61,6 +63,7 @@ "FakeListChatModel", "PromptLayerChatOpenAI", "ChatDatabricks", + "ChatDeepInfra", "ChatEverlyAI", "ChatAnthropic", "ChatCohere", @@ -86,6 +89,7 @@ "ChatBaichuan", "ChatHunyuan", "GigaChat", + "ChatSparkLLM", "VolcEngineMaasChat", "GPTRouter", "ChatZhipuAI", diff --git a/libs/community/langchain_community/chat_models/azureml_endpoint.py b/libs/community/langchain_community/chat_models/azureml_endpoint.py index 111f0502b1fc1..58192d6cdce70 100644 --- a/libs/community/langchain_community/chat_models/azureml_endpoint.py +++ b/libs/community/langchain_community/chat_models/azureml_endpoint.py @@ -1,8 +1,8 @@ import json from typing import Any, Dict, List, Optional, cast -from langchain_core.callbacks import CallbackManagerForLLMRun -from langchain_core.language_models.chat_models import SimpleChatModel +from langchain_core.callbacks.manager import CallbackManagerForLLMRun +from langchain_core.language_models.chat_models import BaseChatModel from langchain_core.messages import ( AIMessage, BaseMessage, @@ -10,16 +10,24 @@ HumanMessage, SystemMessage, ) -from langchain_core.pydantic_v1 import SecretStr, validator -from langchain_core.utils import convert_to_secret_str, get_from_dict_or_env +from langchain_core.outputs import ChatGeneration, ChatResult from langchain_community.llms.azureml_endpoint import ( - AzureMLEndpointClient, + AzureMLBaseEndpoint, + AzureMLEndpointApiType, ContentFormatterBase, ) class LlamaContentFormatter(ContentFormatterBase): + def __init__(self): + raise TypeError( + "`LlamaContentFormatter` is deprecated for chat models. Use " + "`LlamaChatContentFormatter` instead." + ) + + +class LlamaChatContentFormatter(ContentFormatterBase): """Content formatter for `LLaMA`.""" SUPPORTED_ROLES: List[str] = ["user", "assistant", "system"] @@ -45,7 +53,7 @@ def _convert_message_to_dict(message: BaseMessage) -> Dict: } elif ( isinstance(message, ChatMessage) - and message.role in LlamaContentFormatter.SUPPORTED_ROLES + and message.role in LlamaChatContentFormatter.SUPPORTED_ROLES ): return { "role": message.role, @@ -53,79 +61,96 @@ def _convert_message_to_dict(message: BaseMessage) -> Dict: } else: supported = ",".join( - [role for role in LlamaContentFormatter.SUPPORTED_ROLES] + [role for role in LlamaChatContentFormatter.SUPPORTED_ROLES] ) raise ValueError( f"""Received unsupported role. Supported roles for the LLaMa Foundation Model: {supported}""" ) - def _format_request_payload( - self, messages: List[BaseMessage], model_kwargs: Dict - ) -> bytes: + @property + def supported_api_types(self) -> List[AzureMLEndpointApiType]: + return [AzureMLEndpointApiType.realtime, AzureMLEndpointApiType.serverless] + + def format_request_payload( + self, + messages: List[BaseMessage], + model_kwargs: Dict, + api_type: AzureMLEndpointApiType, + ) -> str: + """Formats the request according to the chosen api""" chat_messages = [ - LlamaContentFormatter._convert_message_to_dict(message) + LlamaChatContentFormatter._convert_message_to_dict(message) for message in messages ] - prompt = json.dumps( - {"input_data": {"input_string": chat_messages, "parameters": model_kwargs}} - ) - return self.format_request_payload(prompt=prompt, model_kwargs=model_kwargs) - - def format_request_payload(self, prompt: str, model_kwargs: Dict) -> bytes: - """Formats the request according to the chosen api""" - return str.encode(prompt) + if api_type == AzureMLEndpointApiType.realtime: + request_payload = json.dumps( + { + "input_data": { + "input_string": chat_messages, + "parameters": model_kwargs, + } + } + ) + elif api_type == AzureMLEndpointApiType.serverless: + request_payload = json.dumps({"messages": chat_messages, **model_kwargs}) + else: + raise ValueError( + f"`api_type` {api_type} is not supported by this formatter" + ) + return str.encode(request_payload) - def format_response_payload(self, output: bytes) -> str: + def format_response_payload( + self, output: bytes, api_type: AzureMLEndpointApiType + ) -> ChatGeneration: """Formats response""" - return json.loads(output)["output"] + if api_type == AzureMLEndpointApiType.realtime: + try: + choice = json.loads(output)["output"] + except (KeyError, IndexError, TypeError) as e: + raise ValueError(self.format_error_msg.format(api_type=api_type)) from e + return ChatGeneration( + message=BaseMessage( + content=choice.strip(), + type="assistant", + ), + generation_info=None, + ) + if api_type == AzureMLEndpointApiType.serverless: + try: + choice = json.loads(output)["choices"][0] + if not isinstance(choice, dict): + raise TypeError( + "Endpoint response is not well formed for a chat " + "model. Expected `dict` but `{type(choice)}` was received." + ) + except (KeyError, IndexError, TypeError) as e: + raise ValueError(self.format_error_msg.format(api_type=api_type)) from e + return ChatGeneration( + message=BaseMessage( + content=choice["message"]["content"].strip(), + type=choice["message"]["role"], + ), + generation_info=dict( + finish_reason=choice.get("finish_reason"), + logprobs=choice.get("logprobs"), + ), + ) + raise ValueError(f"`api_type` {api_type} is not supported by this formatter") -class AzureMLChatOnlineEndpoint(SimpleChatModel): - """`AzureML` Chat models API. +class AzureMLChatOnlineEndpoint(BaseChatModel, AzureMLBaseEndpoint): + """Azure ML Online Endpoint chat models. Example: .. code-block:: python - - azure_chat = AzureMLChatOnlineEndpoint( + azure_llm = AzureMLOnlineEndpoint( endpoint_url="https://..inference.ml.azure.com/score", + endpoint_api_type=AzureMLApiType.realtime, endpoint_api_key="my-api-key", - content_formatter=content_formatter, + content_formatter=chat_content_formatter, ) - """ - - endpoint_url: str = "" - """URL of pre-existing Endpoint. Should be passed to constructor or specified as - env var `AZUREML_ENDPOINT_URL`.""" - - endpoint_api_key: SecretStr = convert_to_secret_str("") - """Authentication Key for Endpoint. Should be passed to constructor or specified as - env var `AZUREML_ENDPOINT_API_KEY`.""" - - http_client: Any = None #: :meta private: - - content_formatter: Any = None - """The content formatter that provides an input and output - transform function to handle formats between the LLM and - the endpoint""" - - model_kwargs: Optional[dict] = None - """Keyword arguments to pass to the model.""" - - @validator("http_client", always=True, allow_reuse=True) - @classmethod - def validate_client(cls, field_value: Any, values: Dict) -> AzureMLEndpointClient: - """Validate that api key and python package exist in environment.""" - values["endpoint_api_key"] = convert_to_secret_str( - get_from_dict_or_env(values, "endpoint_api_key", "AZUREML_ENDPOINT_API_KEY") - ) - endpoint_url = get_from_dict_or_env( - values, "endpoint_url", "AZUREML_ENDPOINT_URL" - ) - http_client = AzureMLEndpointClient( - endpoint_url, values["endpoint_api_key"].get_secret_value() - ) - return http_client + """ # noqa: E501 @property def _identifying_params(self) -> Dict[str, Any]: @@ -140,13 +165,13 @@ def _llm_type(self) -> str: """Return type of llm.""" return "azureml_chat_endpoint" - def _call( + def _generate( self, messages: List[BaseMessage], stop: Optional[List[str]] = None, run_manager: Optional[CallbackManagerForLLMRun] = None, **kwargs: Any, - ) -> str: + ) -> ChatResult: """Call out to an AzureML Managed Online endpoint. Args: messages: The messages in the conversation with the chat model. @@ -158,12 +183,17 @@ def _call( response = azureml_model("Tell me a joke.") """ _model_kwargs = self.model_kwargs or {} + _model_kwargs.update(kwargs) + if stop: + _model_kwargs["stop"] = stop - request_payload = self.content_formatter._format_request_payload( - messages, _model_kwargs + request_payload = self.content_formatter.format_request_payload( + messages, _model_kwargs, self.endpoint_api_type + ) + response_payload = self.http_client.call( + body=request_payload, run_manager=run_manager ) - response_payload = self.http_client.call(request_payload, **kwargs) - generated_text = self.content_formatter.format_response_payload( - response_payload + generations = self.content_formatter.format_response_payload( + response_payload, self.endpoint_api_type ) - return generated_text + return ChatResult(generations=[generations]) diff --git a/libs/community/langchain_community/chat_models/baichuan.py b/libs/community/langchain_community/chat_models/baichuan.py index 14cf4a57e2ee4..d95412739be6b 100644 --- a/libs/community/langchain_community/chat_models/baichuan.py +++ b/libs/community/langchain_community/chat_models/baichuan.py @@ -1,7 +1,5 @@ -import hashlib import json import logging -import time from typing import Any, Dict, Iterator, List, Mapping, Optional, Type import requests @@ -30,7 +28,7 @@ logger = logging.getLogger(__name__) -DEFAULT_API_BASE = "https://api.baichuan-ai.com/v1" +DEFAULT_API_BASE = "https://api.baichuan-ai.com/v1/chat/completions" def _convert_message_to_dict(message: BaseMessage) -> dict: @@ -73,14 +71,6 @@ def _convert_delta_to_message_chunk( return default_class(content=content) -# signature generation -def _signature(secret_key: SecretStr, payload: Dict[str, Any], timestamp: int) -> str: - input_str = secret_key.get_secret_value() + json.dumps(payload) + str(timestamp) - md5 = hashlib.md5() - md5.update(input_str.encode("utf-8")) - return md5.hexdigest() - - class ChatBaichuan(BaseChatModel): """Baichuan chat models API by Baichuan Intelligent Technology. @@ -91,7 +81,6 @@ class ChatBaichuan(BaseChatModel): def lc_secrets(self) -> Dict[str, str]: return { "baichuan_api_key": "BAICHUAN_API_KEY", - "baichuan_secret_key": "BAICHUAN_SECRET_KEY", } @property @@ -103,14 +92,14 @@ def lc_serializable(self) -> bool: baichuan_api_key: Optional[SecretStr] = None """Baichuan API Key""" baichuan_secret_key: Optional[SecretStr] = None - """Baichuan Secret Key""" + """[DEPRECATED, keeping it for for backward compatibility] Baichuan Secret Key""" streaming: bool = False """Whether to stream the results or not.""" request_timeout: int = 60 """request timeout for chat http requests""" - - model = "Baichuan2-53B" - """model name of Baichuan, default is `Baichuan2-53B`.""" + model = "Baichuan2-Turbo-192K" + """model name of Baichuan, default is `Baichuan2-Turbo-192K`, + other options include `Baichuan2-Turbo`""" temperature: float = 0.3 """What sampling temperature to use.""" top_k: int = 5 @@ -168,13 +157,6 @@ def validate_environment(cls, values: Dict) -> Dict: "BAICHUAN_API_KEY", ) ) - values["baichuan_secret_key"] = convert_to_secret_str( - get_from_dict_or_env( - values, - "baichuan_secret_key", - "BAICHUAN_SECRET_KEY", - ) - ) return values @@ -187,6 +169,7 @@ def _default_params(self) -> Dict[str, Any]: "top_p": self.top_p, "top_k": self.top_k, "with_search_enhance": self.with_search_enhance, + "stream": self.streaming, } return {**normal_params, **self.model_kwargs} @@ -205,12 +188,9 @@ def _generate( return generate_from_stream(stream_iter) res = self._chat(messages, **kwargs) - + if res.status_code != 200: + raise ValueError(f"Error from Baichuan api response: {res}") response = res.json() - - if response.get("code") != 0: - raise ValueError(f"Error from Baichuan api response: {response}") - return self._create_chat_result(response) def _stream( @@ -221,43 +201,49 @@ def _stream( **kwargs: Any, ) -> Iterator[ChatGenerationChunk]: res = self._chat(messages, **kwargs) - + if res.status_code != 200: + raise ValueError(f"Error from Baichuan api response: {res}") default_chunk_class = AIMessageChunk for chunk in res.iter_lines(): + chunk = chunk.decode("utf-8").strip("\r\n") + parts = chunk.split("data: ", 1) + chunk = parts[1] if len(parts) > 1 else None + if chunk is None: + continue + if chunk == "[DONE]": + break response = json.loads(chunk) - if response.get("code") != 0: - raise ValueError(f"Error from Baichuan api response: {response}") - - data = response.get("data") - for m in data.get("messages"): - chunk = _convert_delta_to_message_chunk(m, default_chunk_class) + for m in response.get("choices"): + chunk = _convert_delta_to_message_chunk( + m.get("delta"), default_chunk_class + ) default_chunk_class = chunk.__class__ yield ChatGenerationChunk(message=chunk) if run_manager: run_manager.on_llm_new_token(chunk.content) def _chat(self, messages: List[BaseMessage], **kwargs: Any) -> requests.Response: - if self.baichuan_secret_key is None: - raise ValueError("Baichuan secret key is not set.") - parameters = {**self._default_params, **kwargs} model = parameters.pop("model") headers = parameters.pop("headers", {}) + temperature = parameters.pop("temperature", 0.3) + top_k = parameters.pop("top_k", 5) + top_p = parameters.pop("top_p", 0.85) + with_search_enhance = parameters.pop("with_search_enhance", False) + stream = parameters.pop("stream", False) payload = { "model": model, "messages": [_convert_message_to_dict(m) for m in messages], - "parameters": parameters, + "top_k": top_k, + "top_p": top_p, + "temperature": temperature, + "with_search_enhance": with_search_enhance, + "stream": stream, } - timestamp = int(time.time()) - url = self.baichuan_api_base - if self.streaming: - url = f"{url}/stream" - url = f"{url}/chat" - api_key = "" if self.baichuan_api_key: api_key = self.baichuan_api_key.get_secret_value() @@ -268,13 +254,6 @@ def _chat(self, messages: List[BaseMessage], **kwargs: Any) -> requests.Response headers={ "Content-Type": "application/json", "Authorization": f"Bearer {api_key}", - "X-BC-Timestamp": str(timestamp), - "X-BC-Signature": _signature( - secret_key=self.baichuan_secret_key, - payload=payload, - timestamp=timestamp, - ), - "X-BC-Sign-Algo": "MD5", **headers, }, json=payload, @@ -284,8 +263,8 @@ def _chat(self, messages: List[BaseMessage], **kwargs: Any) -> requests.Response def _create_chat_result(self, response: Mapping[str, Any]) -> ChatResult: generations = [] - for m in response["data"]["messages"]: - message = _convert_dict_to_message(m) + for c in response["choices"]: + message = _convert_dict_to_message(c["message"]) gen = ChatGeneration(message=message) generations.append(gen) diff --git a/libs/community/langchain_community/chat_models/bedrock.py b/libs/community/langchain_community/chat_models/bedrock.py index 49b7acad19a02..5538372272b7b 100644 --- a/libs/community/langchain_community/chat_models/bedrock.py +++ b/libs/community/langchain_community/chat_models/bedrock.py @@ -32,6 +32,12 @@ def convert_messages_to_prompt( prompt = convert_messages_to_prompt_anthropic(messages=messages) elif provider == "meta": prompt = convert_messages_to_prompt_llama(messages=messages) + elif provider == "amazon": + prompt = convert_messages_to_prompt_anthropic( + messages=messages, + human_prompt="\n\nUser:", + ai_prompt="\n\nBot:", + ) else: raise NotImplementedError( f"Provider {provider} model does not support chat." diff --git a/libs/community/langchain_community/chat_models/deepinfra.py b/libs/community/langchain_community/chat_models/deepinfra.py new file mode 100644 index 0000000000000..7b8a40d3c119f --- /dev/null +++ b/libs/community/langchain_community/chat_models/deepinfra.py @@ -0,0 +1,451 @@ +"""deepinfra.com chat models wrapper""" +from __future__ import annotations + +import json +import logging +from typing import ( + Any, + AsyncIterator, + Callable, + Dict, + Iterator, + List, + Mapping, + Optional, + Tuple, + Type, + Union, +) + +import aiohttp +import requests +from langchain_core.callbacks.manager import ( + AsyncCallbackManagerForLLMRun, + CallbackManagerForLLMRun, +) +from langchain_core.language_models.chat_models import ( + BaseChatModel, + agenerate_from_stream, + generate_from_stream, +) +from langchain_core.language_models.llms import create_base_retry_decorator +from langchain_core.messages import ( + AIMessage, + AIMessageChunk, + BaseMessage, + BaseMessageChunk, + ChatMessage, + ChatMessageChunk, + FunctionMessage, + FunctionMessageChunk, + HumanMessage, + HumanMessageChunk, + SystemMessage, + SystemMessageChunk, +) +from langchain_core.outputs import ( + ChatGeneration, + ChatGenerationChunk, + ChatResult, +) +from langchain_core.pydantic_v1 import Field, root_validator +from langchain_core.utils import get_from_dict_or_env + +# from langchain.llms.base import create_base_retry_decorator +from langchain_community.utilities.requests import Requests + +logger = logging.getLogger(__name__) + + +class ChatDeepInfraException(Exception): + pass + + +def _create_retry_decorator( + llm: ChatDeepInfra, + run_manager: Optional[ + Union[AsyncCallbackManagerForLLMRun, CallbackManagerForLLMRun] + ] = None, +) -> Callable[[Any], Any]: + """Returns a tenacity retry decorator, preconfigured to handle PaLM exceptions""" + return create_base_retry_decorator( + error_types=[requests.exceptions.ConnectTimeout, ChatDeepInfraException], + max_retries=llm.max_retries, + run_manager=run_manager, + ) + + +def _convert_dict_to_message(_dict: Mapping[str, Any]) -> BaseMessage: + role = _dict["role"] + if role == "user": + return HumanMessage(content=_dict["content"]) + elif role == "assistant": + # Fix for azure + # Also OpenAI returns None for tool invocations + content = _dict.get("content", "") or "" + if _dict.get("function_call"): + additional_kwargs = {"function_call": dict(_dict["function_call"])} + else: + additional_kwargs = {} + return AIMessage(content=content, additional_kwargs=additional_kwargs) + elif role == "system": + return SystemMessage(content=_dict["content"]) + elif role == "function": + return FunctionMessage(content=_dict["content"], name=_dict["name"]) + else: + return ChatMessage(content=_dict["content"], role=role) + + +def _convert_delta_to_message_chunk( + _dict: Mapping[str, Any], default_class: Type[BaseMessageChunk] +) -> BaseMessageChunk: + role = _dict.get("role") + content = _dict.get("content") or "" + if _dict.get("function_call"): + additional_kwargs = {"function_call": dict(_dict["function_call"])} + else: + additional_kwargs = {} + + if role == "user" or default_class == HumanMessageChunk: + return HumanMessageChunk(content=content) + elif role == "assistant" or default_class == AIMessageChunk: + return AIMessageChunk(content=content, additional_kwargs=additional_kwargs) + elif role == "system" or default_class == SystemMessageChunk: + return SystemMessageChunk(content=content) + elif role == "function" or default_class == FunctionMessageChunk: + return FunctionMessageChunk(content=content, name=_dict["name"]) + elif role or default_class == ChatMessageChunk: + return ChatMessageChunk(content=content, role=role) + else: + return default_class(content=content) + + +def _convert_message_to_dict(message: BaseMessage) -> dict: + if isinstance(message, ChatMessage): + message_dict = {"role": message.role, "content": message.content} + elif isinstance(message, HumanMessage): + message_dict = {"role": "user", "content": message.content} + elif isinstance(message, AIMessage): + message_dict = {"role": "assistant", "content": message.content} + if "function_call" in message.additional_kwargs: + message_dict["function_call"] = message.additional_kwargs["function_call"] + elif isinstance(message, SystemMessage): + message_dict = {"role": "system", "content": message.content} + elif isinstance(message, FunctionMessage): + message_dict = { + "role": "function", + "content": message.content, + "name": message.name, + } + else: + raise ValueError(f"Got unknown type {message}") + if "name" in message.additional_kwargs: + message_dict["name"] = message.additional_kwargs["name"] + return message_dict + + +class ChatDeepInfra(BaseChatModel): + """A chat model that uses the DeepInfra API.""" + + # client: Any #: :meta private: + model_name: str = Field(default="meta-llama/Llama-2-70b-chat-hf", alias="model") + """Model name to use.""" + deepinfra_api_token: Optional[str] = None + request_timeout: Optional[float] = Field(default=None, alias="timeout") + temperature: Optional[float] = 1 + model_kwargs: Dict[str, Any] = Field(default_factory=dict) + """Run inference with this temperature. Must by in the closed + interval [0.0, 1.0].""" + top_p: Optional[float] = None + """Decode using nucleus sampling: consider the smallest set of tokens whose + probability sum is at least top_p. Must be in the closed interval [0.0, 1.0].""" + top_k: Optional[int] = None + """Decode using top-k sampling: consider the set of top_k most probable tokens. + Must be positive.""" + 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_tokens: int = 256 + streaming: bool = False + max_retries: int = 1 + + @property + def _default_params(self) -> Dict[str, Any]: + """Get the default parameters for calling OpenAI API.""" + return { + "model": self.model_name, + "max_tokens": self.max_tokens, + "stream": self.streaming, + "n": self.n, + "temperature": self.temperature, + "request_timeout": self.request_timeout, + **self.model_kwargs, + } + + @property + def _client_params(self) -> Dict[str, Any]: + """Get the parameters used for the openai client.""" + return {**self._default_params} + + def completion_with_retry( + self, run_manager: Optional[CallbackManagerForLLMRun] = None, **kwargs: Any + ) -> Any: + """Use tenacity to retry the completion call.""" + retry_decorator = _create_retry_decorator(self, run_manager=run_manager) + + @retry_decorator + def _completion_with_retry(**kwargs: Any) -> Any: + try: + request_timeout = kwargs.pop("request_timeout") + request = Requests(headers=self._headers()) + response = request.post( + url=self._url(), data=self._body(kwargs), timeout=request_timeout + ) + self._handle_status(response.status_code, response.text) + return response + except Exception as e: + # import pdb; pdb.set_trace() + print("EX", e) + raise + + return _completion_with_retry(**kwargs) + + async def acompletion_with_retry( + self, + run_manager: Optional[AsyncCallbackManagerForLLMRun] = None, + **kwargs: Any, + ) -> Any: + """Use tenacity to retry the async completion call.""" + retry_decorator = _create_retry_decorator(self, run_manager=run_manager) + + @retry_decorator + async def _completion_with_retry(**kwargs: Any) -> Any: + try: + request_timeout = kwargs.pop("request_timeout") + request = Requests(headers=self._headers()) + async with request.apost( + url=self._url(), data=self._body(kwargs), timeout=request_timeout + ) as response: + self._handle_status(response.status, response.text) + return await response.json() + except Exception as e: + print("EX", e) + raise + + return await _completion_with_retry(**kwargs) + + @root_validator() + def validate_environment(cls, values: Dict) -> Dict: + """Validate api key, python package exists, temperature, top_p, and top_k.""" + # For compatibility with LiteLLM + api_key = get_from_dict_or_env( + values, + "deepinfra_api_key", + "DEEPINFRA_API_KEY", + default="", + ) + values["deepinfra_api_token"] = get_from_dict_or_env( + values, + "deepinfra_api_token", + "DEEPINFRA_API_TOKEN", + default=api_key, + ) + + if values["temperature"] is not None and not 0 <= values["temperature"] <= 1: + raise ValueError("temperature must be in the range [0.0, 1.0]") + + if values["top_p"] is not None and not 0 <= values["top_p"] <= 1: + raise ValueError("top_p must be in the range [0.0, 1.0]") + + if values["top_k"] is not None and values["top_k"] <= 0: + raise ValueError("top_k must be positive") + + return values + + def _generate( + self, + messages: List[BaseMessage], + stop: Optional[List[str]] = None, + run_manager: Optional[CallbackManagerForLLMRun] = None, + stream: Optional[bool] = None, + **kwargs: Any, + ) -> ChatResult: + should_stream = stream if stream is not None else self.streaming + if should_stream: + stream_iter = self._stream( + messages, stop=stop, run_manager=run_manager, **kwargs + ) + return generate_from_stream(stream_iter) + + message_dicts, params = self._create_message_dicts(messages, stop) + params = {**params, **kwargs} + response = self.completion_with_retry( + messages=message_dicts, run_manager=run_manager, **params + ) + return self._create_chat_result(response.json()) + + def _create_chat_result(self, response: Mapping[str, Any]) -> ChatResult: + generations = [] + for res in response["choices"]: + message = _convert_dict_to_message(res["message"]) + gen = ChatGeneration( + message=message, + generation_info=dict(finish_reason=res.get("finish_reason")), + ) + generations.append(gen) + token_usage = response.get("usage", {}) + llm_output = {"token_usage": token_usage, "model": self.model_name} + res = ChatResult(generations=generations, llm_output=llm_output) + return res + + def _create_message_dicts( + self, messages: List[BaseMessage], stop: Optional[List[str]] + ) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]: + params = self._client_params + if stop is not None: + if "stop" in params: + raise ValueError("`stop` found in both the input and default params.") + params["stop"] = stop + message_dicts = [_convert_message_to_dict(m) for m in messages] + return message_dicts, params + + def _stream( + self, + messages: List[BaseMessage], + stop: Optional[List[str]] = None, + run_manager: Optional[CallbackManagerForLLMRun] = None, + **kwargs: Any, + ) -> Iterator[ChatGenerationChunk]: + message_dicts, params = self._create_message_dicts(messages, stop) + params = {**params, **kwargs, "stream": True} + + response = self.completion_with_retry( + messages=message_dicts, run_manager=run_manager, **params + ) + for line in _parse_stream(response.iter_lines()): + chunk = _handle_sse_line(line) + if chunk: + yield ChatGenerationChunk(message=chunk, generation_info=None) + if run_manager: + run_manager.on_llm_new_token(chunk.content) # type: ignore[arg-type] + + async def _astream( + self, + messages: List[BaseMessage], + stop: Optional[List[str]] = None, + run_manager: Optional[AsyncCallbackManagerForLLMRun] = None, + **kwargs: Any, + ) -> AsyncIterator[ChatGenerationChunk]: + message_dicts, params = self._create_message_dicts(messages, stop) + params = {"messages": message_dicts, "stream": True, **params, **kwargs} + + request_timeout = params.pop("request_timeout") + request = Requests(headers=self._headers()) + async with request.apost( + url=self._url(), data=self._body(params), timeout=request_timeout + ) as response: + async for line in _parse_stream_async(response.content): + chunk = _handle_sse_line(line) + if chunk: + yield ChatGenerationChunk(message=chunk, generation_info=None) + if run_manager: + await run_manager.on_llm_new_token(chunk.content) # type: ignore[arg-type] + + async def _agenerate( + self, + messages: List[BaseMessage], + stop: Optional[List[str]] = None, + run_manager: Optional[AsyncCallbackManagerForLLMRun] = None, + stream: Optional[bool] = None, + **kwargs: Any, + ) -> ChatResult: + should_stream = stream if stream is not None else self.streaming + if should_stream: + stream_iter = self._astream( + messages, stop=stop, run_manager=run_manager, **kwargs + ) + return await agenerate_from_stream(stream_iter) + + message_dicts, params = self._create_message_dicts(messages, stop) + params = {"messages": message_dicts, **params, **kwargs} + + res = await self.acompletion_with_retry(run_manager=run_manager, **params) + return self._create_chat_result(res) + + @property + def _identifying_params(self) -> Dict[str, Any]: + """Get the identifying parameters.""" + return { + "model": self.model_name, + "temperature": self.temperature, + "top_p": self.top_p, + "top_k": self.top_k, + "n": self.n, + } + + @property + def _llm_type(self) -> str: + return "deepinfra-chat" + + def _handle_status(self, code: int, text: Any) -> None: + if code >= 500: + raise ChatDeepInfraException(f"DeepInfra Server: Error {code}") + elif code >= 400: + raise ValueError(f"DeepInfra received an invalid payload: {text}") + elif code != 200: + raise Exception( + f"DeepInfra returned an unexpected response with status " + f"{code}: {text}" + ) + + def _url(self) -> str: + return "https://stage.api.deepinfra.com/v1/openai/chat/completions" + + def _headers(self) -> Dict: + return { + "Authorization": f"bearer {self.deepinfra_api_token}", + "Content-Type": "application/json", + } + + def _body(self, kwargs: Any) -> Dict: + return kwargs + + +def _parse_stream(rbody: Iterator[bytes]) -> Iterator[str]: + for line in rbody: + _line = _parse_stream_helper(line) + if _line is not None: + yield _line + + +async def _parse_stream_async(rbody: aiohttp.StreamReader) -> AsyncIterator[str]: + async for line in rbody: + _line = _parse_stream_helper(line) + if _line is not None: + yield _line + + +def _parse_stream_helper(line: bytes) -> Optional[str]: + if line and line.startswith(b"data:"): + if line.startswith(b"data: "): + # SSE event may be valid when it contain whitespace + line = line[len(b"data: ") :] + else: + line = line[len(b"data:") :] + if line.strip() == b"[DONE]": + # return here will cause GeneratorExit exception in urllib3 + # and it will close http connection with TCP Reset + return None + else: + return line.decode("utf-8") + return None + + +def _handle_sse_line(line: str) -> Optional[BaseMessageChunk]: + try: + obj = json.loads(line) + default_chunk_class = AIMessageChunk + delta = obj.get("choices", [{}])[0].get("delta", {}) + return _convert_delta_to_message_chunk(delta, default_chunk_class) + except Exception: + return None diff --git a/libs/community/langchain_community/chat_models/ernie.py b/libs/community/langchain_community/chat_models/ernie.py index 8d69669afc2d4..9954ef4272fc9 100644 --- a/libs/community/langchain_community/chat_models/ernie.py +++ b/libs/community/langchain_community/chat_models/ernie.py @@ -3,6 +3,7 @@ from typing import Any, Dict, List, Mapping, Optional import requests +from langchain_core._api.deprecation import deprecated from langchain_core.callbacks import CallbackManagerForLLMRun from langchain_core.language_models.chat_models import BaseChatModel from langchain_core.messages import ( @@ -30,6 +31,10 @@ def _convert_message_to_dict(message: BaseMessage) -> dict: return message_dict +@deprecated( + since="0.0.13", + alternative="langchain_community.chat_models.QianfanChatEndpoint", +) class ErnieBotChat(BaseChatModel): """`ERNIE-Bot` large language model. diff --git a/libs/community/langchain_community/chat_models/konko.py b/libs/community/langchain_community/chat_models/konko.py index 9fe24a50694f5..8492a5f8c56f3 100644 --- a/libs/community/langchain_community/chat_models/konko.py +++ b/libs/community/langchain_community/chat_models/konko.py @@ -3,12 +3,12 @@ import logging import os +import warnings from typing import ( Any, Dict, Iterator, List, - Mapping, Optional, Set, Tuple, @@ -19,20 +19,20 @@ from langchain_core.callbacks import ( CallbackManagerForLLMRun, ) -from langchain_core.language_models.chat_models import ( - BaseChatModel, - generate_from_stream, -) from langchain_core.messages import AIMessageChunk, BaseMessage -from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, ChatResult +from langchain_core.outputs import ChatGenerationChunk, ChatResult from langchain_core.pydantic_v1 import Field, SecretStr, root_validator from langchain_core.utils import convert_to_secret_str, get_from_dict_or_env from langchain_community.adapters.openai import ( - convert_dict_to_message, convert_message_to_dict, ) -from langchain_community.chat_models.openai import _convert_delta_to_message_chunk +from langchain_community.chat_models.openai import ( + ChatOpenAI, + _convert_delta_to_message_chunk, + generate_from_stream, +) +from langchain_community.utils.openai import is_openai_v1 DEFAULT_API_BASE = "https://api.konko.ai/v1" DEFAULT_MODEL = "meta-llama/Llama-2-13b-chat-hf" @@ -40,7 +40,7 @@ logger = logging.getLogger(__name__) -class ChatKonko(BaseChatModel): +class ChatKonko(ChatOpenAI): """`ChatKonko` Chat large language models API. To use, you should have the ``konko`` python package installed, and the @@ -72,10 +72,8 @@ def is_lc_serializable(cls) -> bool: """What sampling temperature to use.""" model_kwargs: Dict[str, Any] = Field(default_factory=dict) """Holds any model parameters valid for `create` call not explicitly specified.""" - openai_api_key: Optional[SecretStr] = None - konko_api_key: Optional[SecretStr] = None - request_timeout: Optional[Union[float, Tuple[float, float]]] = None - """Timeout for requests to Konko completion API.""" + openai_api_key: Optional[str] = None + konko_api_key: Optional[str] = None max_retries: int = 6 """Maximum number of retries to make when generating.""" streaming: bool = False @@ -100,13 +98,23 @@ def validate_environment(cls, values: Dict) -> Dict: "Please install it with `pip install konko`." ) try: - values["client"] = konko.ChatCompletion + if is_openai_v1(): + values["client"] = konko.chat.completions + else: + values["client"] = konko.ChatCompletion except AttributeError: raise ValueError( "`konko` has no `ChatCompletion` attribute, this is likely " "due to an old version of the konko package. Try upgrading it " "with `pip install --upgrade konko`." ) + + if not hasattr(konko, "_is_legacy_openai"): + warnings.warn( + "You are using an older version of the 'konko' package. " + "Please consider upgrading to access new features." + ) + if values["n"] < 1: raise ValueError("n must be at least 1.") if values["n"] > 1 and values["streaming"]: @@ -118,7 +126,6 @@ def _default_params(self) -> Dict[str, Any]: """Get the default parameters for calling Konko API.""" return { "model": self.model, - "request_timeout": self.request_timeout, "max_tokens": self.max_tokens, "stream": self.streaming, "n": self.n, @@ -182,20 +189,6 @@ def _completion_with_retry(**kwargs: Any) -> Any: return _completion_with_retry(**kwargs) - def _combine_llm_outputs(self, llm_outputs: List[Optional[dict]]) -> dict: - overall_token_usage: dict = {} - for output in llm_outputs: - if output is None: - # Happens in streaming - continue - token_usage = output["token_usage"] - for k, v in token_usage.items(): - if k in overall_token_usage: - overall_token_usage[k] += v - else: - overall_token_usage[k] = v - return {"token_usage": overall_token_usage, "model_name": self.model} - def _stream( self, messages: List[BaseMessage], @@ -259,19 +252,6 @@ def _create_message_dicts( message_dicts = [convert_message_to_dict(m) for m in messages] return message_dicts, params - def _create_chat_result(self, response: Mapping[str, Any]) -> ChatResult: - generations = [] - for res in response["choices"]: - message = convert_dict_to_message(res["message"]) - gen = ChatGeneration( - message=message, - generation_info=dict(finish_reason=res.get("finish_reason")), - ) - generations.append(gen) - token_usage = response.get("usage", {}) - llm_output = {"token_usage": token_usage, "model_name": self.model} - return ChatResult(generations=generations, llm_output=llm_output) - @property def _identifying_params(self) -> Dict[str, Any]: """Get the identifying parameters.""" diff --git a/libs/community/langchain_community/chat_models/litellm.py b/libs/community/langchain_community/chat_models/litellm.py index cb6a2107d34eb..39f7284c3b5ac 100644 --- a/libs/community/langchain_community/chat_models/litellm.py +++ b/libs/community/langchain_community/chat_models/litellm.py @@ -246,8 +246,8 @@ def validate_environment(cls, values: Dict) -> Dict: import litellm except ImportError: raise ChatLiteLLMException( - "Could not import google.generativeai python package. " - "Please install it with `pip install google-generativeai`" + "Could not import litellm python package. " + "Please install it with `pip install litellm`" ) values["openai_api_key"] = get_from_dict_or_env( diff --git a/libs/community/langchain_community/chat_models/sparkllm.py b/libs/community/langchain_community/chat_models/sparkllm.py new file mode 100644 index 0000000000000..7e84c2e98c2e5 --- /dev/null +++ b/libs/community/langchain_community/chat_models/sparkllm.py @@ -0,0 +1,473 @@ +import base64 +import hashlib +import hmac +import json +import logging +import queue +import threading +from datetime import datetime +from queue import Queue +from time import mktime +from typing import Any, Dict, Generator, Iterator, List, Mapping, Optional, Type +from urllib.parse import urlencode, urlparse, urlunparse +from wsgiref.handlers import format_date_time + +from langchain_core.callbacks import ( + CallbackManagerForLLMRun, +) +from langchain_core.language_models.chat_models import ( + BaseChatModel, + generate_from_stream, +) +from langchain_core.messages import ( + AIMessage, + AIMessageChunk, + BaseMessage, + BaseMessageChunk, + ChatMessage, + ChatMessageChunk, + HumanMessage, + HumanMessageChunk, + SystemMessage, +) +from langchain_core.outputs import ( + ChatGeneration, + ChatGenerationChunk, + ChatResult, +) +from langchain_core.pydantic_v1 import Field, root_validator +from langchain_core.utils import ( + get_from_dict_or_env, + get_pydantic_field_names, +) + +logger = logging.getLogger(__name__) + + +def _convert_message_to_dict(message: BaseMessage) -> dict: + if isinstance(message, ChatMessage): + message_dict = {"role": "user", "content": message.content} + elif isinstance(message, HumanMessage): + message_dict = {"role": "user", "content": message.content} + elif isinstance(message, AIMessage): + message_dict = {"role": "assistant", "content": message.content} + elif isinstance(message, SystemMessage): + message_dict = {"role": "system", "content": message.content} + else: + raise ValueError(f"Got unknown type {message}") + + return message_dict + + +def _convert_dict_to_message(_dict: Mapping[str, Any]) -> BaseMessage: + msg_role = _dict["role"] + msg_content = _dict["content"] + if msg_role == "user": + return HumanMessage(content=msg_content) + elif msg_role == "assistant": + content = msg_content or "" + return AIMessage(content=content) + elif msg_role == "system": + return SystemMessage(content=msg_content) + else: + return ChatMessage(content=msg_content, role=msg_role) + + +def _convert_delta_to_message_chunk( + _dict: Mapping[str, Any], default_class: Type[BaseMessageChunk] +) -> BaseMessageChunk: + msg_role = _dict["role"] + msg_content = _dict.get("content", "") + if msg_role == "user" or default_class == HumanMessageChunk: + return HumanMessageChunk(content=msg_content) + elif msg_role == "assistant" or default_class == AIMessageChunk: + return AIMessageChunk(content=msg_content) + elif msg_role or default_class == ChatMessageChunk: + return ChatMessageChunk(content=msg_content, role=msg_role) + else: + return default_class(content=msg_content) + + +class ChatSparkLLM(BaseChatModel): + """Wrapper around iFlyTek's Spark large language model. + + To use, you should pass `app_id`, `api_key`, `api_secret` + as a named parameter to the constructor OR set environment + variables ``IFLYTEK_SPARK_APP_ID``, ``IFLYTEK_SPARK_API_KEY`` and + ``IFLYTEK_SPARK_API_SECRET`` + + Example: + .. code-block:: python + + client = ChatSparkLLM( + spark_app_id="", + spark_api_key="", + spark_api_secret="" + ) + """ + + @classmethod + def is_lc_serializable(cls) -> bool: + """Return whether this model can be serialized by Langchain.""" + return False + + @property + def lc_secrets(self) -> Dict[str, str]: + return { + "spark_app_id": "IFLYTEK_SPARK_APP_ID", + "spark_api_key": "IFLYTEK_SPARK_API_KEY", + "spark_api_secret": "IFLYTEK_SPARK_API_SECRET", + "spark_api_url": "IFLYTEK_SPARK_API_URL", + "spark_llm_domain": "IFLYTEK_SPARK_LLM_DOMAIN", + } + + client: Any = None #: :meta private: + spark_app_id: Optional[str] = None + spark_api_key: Optional[str] = None + spark_api_secret: Optional[str] = None + spark_api_url: Optional[str] = None + spark_llm_domain: Optional[str] = None + spark_user_id: str = "lc_user" + streaming: bool = False + request_timeout: int = 30 + temperature: float = 0.5 + top_k: int = 4 + model_kwargs: Dict[str, Any] = Field(default_factory=dict) + + @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: + values["spark_app_id"] = get_from_dict_or_env( + values, + "spark_app_id", + "IFLYTEK_SPARK_APP_ID", + ) + values["spark_api_key"] = get_from_dict_or_env( + values, + "spark_api_key", + "IFLYTEK_SPARK_API_KEY", + ) + values["spark_api_secret"] = get_from_dict_or_env( + values, + "spark_api_secret", + "IFLYTEK_SPARK_API_SECRET", + ) + values["spark_app_url"] = get_from_dict_or_env( + values, + "spark_app_url", + "IFLYTEK_SPARK_APP_URL", + "wss://spark-api.xf-yun.com/v3.1/chat", + ) + values["spark_llm_domain"] = get_from_dict_or_env( + values, + "spark_llm_domain", + "IFLYTEK_SPARK_LLM_DOMAIN", + "generalv3", + ) + # put extra params into model_kwargs + values["model_kwargs"]["temperature"] = values["temperature"] or cls.temperature + values["model_kwargs"]["top_k"] = values["top_k"] or cls.top_k + + values["client"] = _SparkLLMClient( + app_id=values["spark_app_id"], + api_key=values["spark_api_key"], + api_secret=values["spark_api_secret"], + api_url=values["spark_api_url"], + spark_domain=values["spark_llm_domain"], + model_kwargs=values["model_kwargs"], + ) + return values + + def _stream( + self, + messages: List[BaseMessage], + stop: Optional[List[str]] = None, + run_manager: Optional[CallbackManagerForLLMRun] = None, + **kwargs: Any, + ) -> Iterator[ChatGenerationChunk]: + default_chunk_class = AIMessageChunk + + self.client.arun( + [_convert_message_to_dict(m) for m in messages], + self.spark_user_id, + self.model_kwargs, + self.streaming, + ) + for content in self.client.subscribe(timeout=self.request_timeout): + if "data" not in content: + continue + delta = content["data"] + chunk = _convert_delta_to_message_chunk(delta, default_chunk_class) + yield ChatGenerationChunk(message=chunk) + if run_manager: + run_manager.on_llm_new_token(str(chunk.content)) + + def _generate( + self, + messages: List[BaseMessage], + stop: Optional[List[str]] = None, + run_manager: Optional[CallbackManagerForLLMRun] = None, + **kwargs: Any, + ) -> ChatResult: + if self.streaming: + stream_iter = self._stream( + messages=messages, stop=stop, run_manager=run_manager, **kwargs + ) + return generate_from_stream(stream_iter) + + self.client.arun( + [_convert_message_to_dict(m) for m in messages], + self.spark_user_id, + self.model_kwargs, + False, + ) + completion = {} + llm_output = {} + for content in self.client.subscribe(timeout=self.request_timeout): + if "usage" in content: + llm_output["token_usage"] = content["usage"] + if "data" not in content: + continue + completion = content["data"] + message = _convert_dict_to_message(completion) + generations = [ChatGeneration(message=message)] + return ChatResult(generations=generations, llm_output=llm_output) + + @property + def _llm_type(self) -> str: + return "spark-llm-chat" + + +class _SparkLLMClient: + """ + Use websocket-client to call the SparkLLM interface provided by Xfyun, + which is the iFlyTek's open platform for AI capabilities + """ + + def __init__( + self, + app_id: str, + api_key: str, + api_secret: str, + api_url: Optional[str] = None, + spark_domain: Optional[str] = None, + model_kwargs: Optional[dict] = None, + ): + try: + import websocket + + self.websocket_client = websocket + except ImportError: + raise ImportError( + "Could not import websocket client python package. " + "Please install it with `pip install websocket-client`." + ) + + self.api_url = ( + "wss://spark-api.xf-yun.com/v3.1/chat" if not api_url else api_url + ) + self.app_id = app_id + self.ws_url = _SparkLLMClient._create_url( + self.api_url, + api_key, + api_secret, + ) + self.model_kwargs = model_kwargs + self.spark_domain = spark_domain or "generalv3" + self.queue: Queue[Dict] = Queue() + self.blocking_message = {"content": "", "role": "assistant"} + + @staticmethod + def _create_url(api_url: str, api_key: str, api_secret: str) -> str: + """ + Generate a request url with an api key and an api secret. + """ + # generate timestamp by RFC1123 + date = format_date_time(mktime(datetime.now().timetuple())) + + # urlparse + parsed_url = urlparse(api_url) + host = parsed_url.netloc + path = parsed_url.path + + signature_origin = f"host: {host}\ndate: {date}\nGET {path} HTTP/1.1" + + # encrypt using hmac-sha256 + signature_sha = hmac.new( + api_secret.encode("utf-8"), + signature_origin.encode("utf-8"), + digestmod=hashlib.sha256, + ).digest() + + signature_sha_base64 = base64.b64encode(signature_sha).decode(encoding="utf-8") + + authorization_origin = f'api_key="{api_key}", algorithm="hmac-sha256", \ + headers="host date request-line", signature="{signature_sha_base64}"' + authorization = base64.b64encode(authorization_origin.encode("utf-8")).decode( + encoding="utf-8" + ) + + # generate url + params_dict = {"authorization": authorization, "date": date, "host": host} + encoded_params = urlencode(params_dict) + url = urlunparse( + ( + parsed_url.scheme, + parsed_url.netloc, + parsed_url.path, + parsed_url.params, + encoded_params, + parsed_url.fragment, + ) + ) + return url + + def run( + self, + messages: List[Dict], + user_id: str, + model_kwargs: Optional[dict] = None, + streaming: bool = False, + ) -> None: + self.websocket_client.enableTrace(False) + ws = self.websocket_client.WebSocketApp( + self.ws_url, + on_message=self.on_message, + on_error=self.on_error, + on_close=self.on_close, + on_open=self.on_open, + ) + ws.messages = messages + ws.user_id = user_id + ws.model_kwargs = self.model_kwargs if model_kwargs is None else model_kwargs + ws.streaming = streaming + ws.run_forever() + + def arun( + self, + messages: List[Dict], + user_id: str, + model_kwargs: Optional[dict] = None, + streaming: bool = False, + ) -> threading.Thread: + ws_thread = threading.Thread( + target=self.run, + args=( + messages, + user_id, + model_kwargs, + streaming, + ), + ) + ws_thread.start() + return ws_thread + + def on_error(self, ws: Any, error: Optional[Any]) -> None: + self.queue.put({"error": error}) + ws.close() + + def on_close(self, ws: Any, close_status_code: int, close_reason: str) -> None: + logger.debug( + { + "log": { + "close_status_code": close_status_code, + "close_reason": close_reason, + } + } + ) + self.queue.put({"done": True}) + + def on_open(self, ws: Any) -> None: + self.blocking_message = {"content": "", "role": "assistant"} + data = json.dumps( + self.gen_params( + messages=ws.messages, user_id=ws.user_id, model_kwargs=ws.model_kwargs + ) + ) + ws.send(data) + + def on_message(self, ws: Any, message: str) -> None: + data = json.loads(message) + code = data["header"]["code"] + if code != 0: + self.queue.put( + {"error": f"Code: {code}, Error: {data['header']['message']}"} + ) + ws.close() + else: + choices = data["payload"]["choices"] + status = choices["status"] + content = choices["text"][0]["content"] + if ws.streaming: + self.queue.put({"data": choices["text"][0]}) + else: + self.blocking_message["content"] += content + if status == 2: + if not ws.streaming: + self.queue.put({"data": self.blocking_message}) + usage_data = ( + data.get("payload", {}).get("usage", {}).get("text", {}) + if data + else {} + ) + self.queue.put({"usage": usage_data}) + ws.close() + + def gen_params( + self, messages: list, user_id: str, model_kwargs: Optional[dict] = None + ) -> dict: + data: Dict = { + "header": {"app_id": self.app_id, "uid": user_id}, + "parameter": {"chat": {"domain": self.spark_domain}}, + "payload": {"message": {"text": messages}}, + } + + if model_kwargs: + data["parameter"]["chat"].update(model_kwargs) + logger.debug(f"Spark Request Parameters: {data}") + return data + + def subscribe(self, timeout: Optional[int] = 30) -> Generator[Dict, None, None]: + while True: + try: + content = self.queue.get(timeout=timeout) + except queue.Empty as _: + raise TimeoutError( + f"SparkLLMClient wait LLM api response timeout {timeout} seconds" + ) + if "error" in content: + raise ConnectionError(content["error"]) + if "usage" in content: + yield content + continue + if "done" in content: + break + if "data" not in content: + break + yield content diff --git a/libs/community/langchain_community/chat_models/tongyi.py b/libs/community/langchain_community/chat_models/tongyi.py index be4373a50563b..004ffdd3b12bf 100644 --- a/libs/community/langchain_community/chat_models/tongyi.py +++ b/libs/community/langchain_community/chat_models/tongyi.py @@ -315,9 +315,7 @@ async def _agenerate( ) resp = await asyncio.get_running_loop().run_in_executor( None, - functools.partial( - self.completion_with_retry, **{"run_manager": run_manager, **params} - ), + functools.partial(self.completion_with_retry, **params), ) generations.append( ChatGeneration(**self._chat_generation_from_qwen_resp(resp)) diff --git a/libs/community/langchain_community/document_loaders/__init__.py b/libs/community/langchain_community/document_loaders/__init__.py index bf2ac63bce111..d4c5436525e94 100644 --- a/libs/community/langchain_community/document_loaders/__init__.py +++ b/libs/community/langchain_community/document_loaders/__init__.py @@ -180,6 +180,7 @@ from langchain_community.document_loaders.spreedly import SpreedlyLoader from langchain_community.document_loaders.srt import SRTLoader from langchain_community.document_loaders.stripe import StripeLoader +from langchain_community.document_loaders.surrealdb import SurrealDBLoader from langchain_community.document_loaders.telegram import ( TelegramChatApiLoader, TelegramChatFileLoader, @@ -206,6 +207,7 @@ from langchain_community.document_loaders.url import UnstructuredURLLoader from langchain_community.document_loaders.url_playwright import PlaywrightURLLoader from langchain_community.document_loaders.url_selenium import SeleniumURLLoader +from langchain_community.document_loaders.vsdx import VsdxLoader from langchain_community.document_loaders.weather import WeatherDataLoader from langchain_community.document_loaders.web_base import WebBaseLoader from langchain_community.document_loaders.whatsapp_chat import WhatsAppChatLoader @@ -360,6 +362,7 @@ "SnowflakeLoader", "SpreedlyLoader", "StripeLoader", + "SurrealDBLoader", "TelegramChatApiLoader", "TelegramChatFileLoader", "TelegramChatLoader", @@ -392,6 +395,7 @@ "UnstructuredURLLoader", "UnstructuredWordDocumentLoader", "UnstructuredXMLLoader", + "VsdxLoader", "WeatherDataLoader", "WebBaseLoader", "WhatsAppChatLoader", diff --git a/libs/community/langchain_community/document_loaders/cassandra.py b/libs/community/langchain_community/document_loaders/cassandra.py new file mode 100644 index 0000000000000..8983f3c56b6a0 --- /dev/null +++ b/libs/community/langchain_community/document_loaders/cassandra.py @@ -0,0 +1,123 @@ +import json +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Iterator, + List, + Optional, + Sequence, + Union, +) + +from langchain_core.documents import Document + +from langchain_community.document_loaders.base import BaseLoader + + +def default_page_content_mapper(row: Any) -> str: + if hasattr(row, "_asdict"): + return json.dumps(row._asdict()) + return json.dumps(row) + + +_NOT_SET = object() + +if TYPE_CHECKING: + from cassandra.cluster import Session + from cassandra.pool import Host + from cassandra.query import Statement + + +class CassandraLoader(BaseLoader): + def __init__( + self, + table: Optional[str] = None, + session: Optional["Session"] = None, + keyspace: Optional[str] = None, + query: Optional[Union[str, "Statement"]] = None, + page_content_mapper: Callable[[Any], str] = default_page_content_mapper, + metadata_mapper: Callable[[Any], dict] = lambda _: {}, + *, + query_parameters: Union[dict, Sequence] = None, + query_timeout: Optional[float] = _NOT_SET, + query_trace: bool = False, + query_custom_payload: dict = None, + query_execution_profile: Any = _NOT_SET, + query_paging_state: Any = None, + query_host: "Host" = None, + query_execute_as: str = None, + ) -> None: + """ + Document Loader for Apache Cassandra. + + Args: + table: The table to load the data from. + (do not use together with the query parameter) + session: The cassandra driver session. + If not provided, the cassio resolved session will be used. + keyspace: The keyspace of the table. + If not provided, the cassio resolved keyspace will be used. + query: The query used to load the data. + (do not use together with the table parameter) + page_content_mapper: a function to convert a row to string page content. + query_parameters: The query parameters used when calling session.execute . + query_timeout: The query timeout used when calling session.execute . + query_custom_payload: The query custom_payload used when calling + session.execute . + query_execution_profile: The query execution_profile used when calling + session.execute . + query_host: The query host used when calling session.execute . + query_execute_as: The query execute_as used when calling session.execute . + """ + if query and table: + raise ValueError("Cannot specify both query and table.") + + if not query and not table: + raise ValueError("Must specify query or table.") + + if not session or (table and not keyspace): + try: + from cassio.config import check_resolve_keyspace, check_resolve_session + except (ImportError, ModuleNotFoundError): + raise ImportError( + "Could not import a recent cassio package." + "Please install it with `pip install --upgrade cassio`." + ) + + if table: + _keyspace = keyspace or check_resolve_keyspace(keyspace) + self.query = f"SELECT * FROM {_keyspace}.{table};" + self.metadata = {"table": table, "keyspace": _keyspace} + else: + self.query = query + self.metadata = {} + + self.session = session or check_resolve_session(session) + self.page_content_mapper = page_content_mapper + self.metadata_mapper = metadata_mapper + + self.query_kwargs = { + "parameters": query_parameters, + "trace": query_trace, + "custom_payload": query_custom_payload, + "paging_state": query_paging_state, + "host": query_host, + "execute_as": query_execute_as, + } + if query_timeout is not _NOT_SET: + self.query_kwargs["timeout"] = query_timeout + + if query_execution_profile is not _NOT_SET: + self.query_kwargs["execution_profile"] = query_execution_profile + + def load(self) -> List[Document]: + return list(self.lazy_load()) + + def lazy_load(self) -> Iterator[Document]: + for row in self.session.execute(self.query, **self.query_kwargs): + metadata = self.metadata.copy() + metadata.update(self.metadata_mapper(row)) + yield Document( + page_content=self.page_content_mapper(row), metadata=metadata + ) diff --git a/libs/community/langchain_community/document_loaders/parsers/__init__.py b/libs/community/langchain_community/document_loaders/parsers/__init__.py index 9d01c3df2f5b7..8b635e9a68bd0 100644 --- a/libs/community/langchain_community/document_loaders/parsers/__init__.py +++ b/libs/community/langchain_community/document_loaders/parsers/__init__.py @@ -13,6 +13,7 @@ PyPDFium2Parser, PyPDFParser, ) +from langchain_community.document_loaders.parsers.vsdx import VsdxParser __all__ = [ "AzureAIDocumentIntelligenceParser", @@ -26,4 +27,5 @@ "PyMuPDFParser", "PyPDFium2Parser", "PyPDFParser", + "VsdxParser", ] diff --git a/libs/community/langchain_community/document_loaders/parsers/audio.py b/libs/community/langchain_community/document_loaders/parsers/audio.py index ab54c67ed3751..3b96f9860c536 100644 --- a/libs/community/langchain_community/document_loaders/parsers/audio.py +++ b/libs/community/langchain_community/document_loaders/parsers/audio.py @@ -64,7 +64,7 @@ def lazy_parse(self, blob: Blob) -> Iterator[Document]: file_obj.name = f"part_{split_number}.mp3" # Transcribe - print(f"Transcribing part {split_number+1}!") + print(f"Transcribing part {split_number + 1}!") attempts = 0 while attempts < 3: try: @@ -116,6 +116,8 @@ def __init__( self, device: str = "0", lang_model: Optional[str] = None, + batch_size: int = 8, + chunk_length: int = 30, forced_decoder_ids: Optional[Tuple[Dict]] = None, ): """Initialize the parser. @@ -126,6 +128,10 @@ def __init__( Defaults to None. forced_decoder_ids: id states for decoder in a multilanguage model. Defaults to None. + batch_size: batch size used for decoding + Defaults to 8. + chunk_length: chunk length used during inference. + Defaults to 30s. """ try: from transformers import pipeline @@ -141,47 +147,37 @@ def __init__( "torch package not found, please install it with " "`pip install torch`" ) - # set device, cpu by default check if there is a GPU available + # Determine the device to use if device == "cpu": self.device = "cpu" - if lang_model is not None: - self.lang_model = lang_model - print("WARNING! Model override. Using model: ", self.lang_model) - else: - # unless overridden, use the small base model on cpu - self.lang_model = "openai/whisper-base" else: - if torch.cuda.is_available(): - self.device = "cuda:0" - # check GPU memory and select automatically the model - mem = torch.cuda.get_device_properties(self.device).total_memory / ( - 1024**2 - ) - if mem < 5000: - rec_model = "openai/whisper-base" - elif mem < 7000: - rec_model = "openai/whisper-small" - elif mem < 12000: - rec_model = "openai/whisper-medium" - else: - rec_model = "openai/whisper-large" - - # check if model is overridden - if lang_model is not None: - self.lang_model = lang_model - print("WARNING! Model override. Might not fit in your GPU") - else: - self.lang_model = rec_model + self.device = "cuda:0" if torch.cuda.is_available() else "cpu" + + if self.device == "cpu": + default_model = "openai/whisper-base" + self.lang_model = lang_model if lang_model else default_model + else: + # Set the language model based on the device and available memory + mem = torch.cuda.get_device_properties(self.device).total_memory / (1024**2) + if mem < 5000: + rec_model = "openai/whisper-base" + elif mem < 7000: + rec_model = "openai/whisper-small" + elif mem < 12000: + rec_model = "openai/whisper-medium" else: - "cpu" + rec_model = "openai/whisper-large" + self.lang_model = lang_model if lang_model else rec_model print("Using the following model: ", self.lang_model) + self.batch_size = batch_size + # load model for inference self.pipe = pipeline( "automatic-speech-recognition", model=self.lang_model, - chunk_length_s=30, + chunk_length_s=chunk_length, device=self.device, ) if forced_decoder_ids is not None: @@ -224,7 +220,7 @@ def lazy_parse(self, blob: Blob) -> Iterator[Document]: y, sr = librosa.load(file_obj, sr=16000) - prediction = self.pipe(y.copy(), batch_size=8)["text"] + prediction = self.pipe(y.copy(), batch_size=self.batch_size)["text"] yield Document( page_content=prediction, diff --git a/libs/community/langchain_community/document_loaders/parsers/grobid.py b/libs/community/langchain_community/document_loaders/parsers/grobid.py index 8eb9974479c9d..f73f91150c2a2 100644 --- a/libs/community/langchain_community/document_loaders/parsers/grobid.py +++ b/libs/community/langchain_community/document_loaders/parsers/grobid.py @@ -59,19 +59,20 @@ def process_xml( for i, sentence in enumerate(paragraph.find_all("s")): paragraph_text.append(sentence.text) sbboxes = [] - for bbox in sentence.get("coords").split(";"): - box = bbox.split(",") - sbboxes.append( - { - "page": box[0], - "x": box[1], - "y": box[2], - "h": box[3], - "w": box[4], - } - ) - chunk_bboxes.append(sbboxes) - if segment_sentences is True: + if sentence.get("coords") is not None: + for bbox in sentence.get("coords").split(";"): + box = bbox.split(",") + sbboxes.append( + { + "page": box[0], + "x": box[1], + "y": box[2], + "h": box[3], + "w": box[4], + } + ) + chunk_bboxes.append(sbboxes) + if (segment_sentences is True) and (len(sbboxes) > 0): fpage, lpage = sbboxes[0]["page"], sbboxes[-1]["page"] sentence_dict = { "text": sentence.text, diff --git a/libs/community/langchain_community/document_loaders/parsers/language/language_parser.py b/libs/community/langchain_community/document_loaders/parsers/language/language_parser.py index c53dd493fdeed..b3f6534327f55 100644 --- a/libs/community/langchain_community/document_loaders/parsers/language/language_parser.py +++ b/libs/community/langchain_community/document_loaders/parsers/language/language_parser.py @@ -97,6 +97,8 @@ def __init__(self, language: Optional[Language] = None, parser_threshold: int = language: If None (default), it will try to infer language from source. parser_threshold: Minimum lines needed to activate parsing (0 by default). """ + if language and language not in LANGUAGE_SEGMENTERS: + raise Exception(f"No parser available for {language}") self.language = language self.parser_threshold = parser_threshold diff --git a/libs/community/langchain_community/document_loaders/parsers/vsdx.py b/libs/community/langchain_community/document_loaders/parsers/vsdx.py new file mode 100644 index 0000000000000..109521e48cc9f --- /dev/null +++ b/libs/community/langchain_community/document_loaders/parsers/vsdx.py @@ -0,0 +1,205 @@ +import json +import re +import zipfile +from abc import ABC +from pathlib import Path +from typing import Iterator, List, Set, Tuple + +from langchain_community.docstore.document import Document +from langchain_community.document_loaders.base import BaseBlobParser +from langchain_community.document_loaders.blob_loaders import Blob + + +class VsdxParser(BaseBlobParser, ABC): + def parse(self, blob: Blob) -> Iterator[Document]: + """Parse a vsdx file.""" + return self.lazy_parse(blob) + + def lazy_parse(self, blob: Blob) -> Iterator[Document]: + """Retrieve the contents of pages from a .vsdx file + and insert them into documents, one document per page.""" + + with blob.as_bytes_io() as pdf_file_obj: + with zipfile.ZipFile(pdf_file_obj, "r") as zfile: + pages = self.get_pages_content(zfile, blob.source) + + yield from [ + Document( + page_content=page_content, + metadata={ + "source": blob.source, + "page": page_number, + "page_name": page_name, + }, + ) + for page_number, page_name, page_content in pages + ] + + def get_pages_content( + self, zfile: zipfile.ZipFile, source: str + ) -> List[Tuple[int, str, str]]: + """Get the content of the pages of a vsdx file. + + Attributes: + zfile (zipfile.ZipFile): The vsdx file under zip format. + source (str): The path of the vsdx file. + + Returns: + list[tuple[int, str, str]]: A list of tuples containing the page number, + the name of the page and the content of the page + for each page of the vsdx file. + """ + + try: + import xmltodict + except ImportError: + raise ImportError( + "The xmltodict library is required to parse vsdx files. " + "Please install it with `pip install xmltodict`." + ) + + if "visio/pages/pages.xml" not in zfile.namelist(): + print("WARNING - No pages.xml file found in {}".format(source)) + return + if "visio/pages/_rels/pages.xml.rels" not in zfile.namelist(): + print("WARNING - No pages.xml.rels file found in {}".format(source)) + return + if "docProps/app.xml" not in zfile.namelist(): + print("WARNING - No app.xml file found in {}".format(source)) + return + + pagesxml_content: dict = xmltodict.parse(zfile.read("visio/pages/pages.xml")) + appxml_content: dict = xmltodict.parse(zfile.read("docProps/app.xml")) + pagesxmlrels_content: dict = xmltodict.parse( + zfile.read("visio/pages/_rels/pages.xml.rels") + ) + + if isinstance(pagesxml_content["Pages"]["Page"], list): + disordered_names: List[str] = [ + rel["@Name"].strip() for rel in pagesxml_content["Pages"]["Page"] + ] + else: + disordered_names: List[str] = [ + pagesxml_content["Pages"]["Page"]["@Name"].strip() + ] + if isinstance(pagesxmlrels_content["Relationships"]["Relationship"], list): + disordered_paths: List[str] = [ + "visio/pages/" + rel["@Target"] + for rel in pagesxmlrels_content["Relationships"]["Relationship"] + ] + else: + disordered_paths: List[str] = [ + "visio/pages/" + + pagesxmlrels_content["Relationships"]["Relationship"]["@Target"] + ] + ordered_names: List[str] = appxml_content["Properties"]["TitlesOfParts"][ + "vt:vector" + ]["vt:lpstr"][: len(disordered_names)] + ordered_names = [name.strip() for name in ordered_names] + ordered_paths = [ + disordered_paths[disordered_names.index(name.strip())] + for name in ordered_names + ] + + # Pages out of order and without content of their relationships + disordered_pages = [] + for path in ordered_paths: + content = zfile.read(path) + string_content = json.dumps(xmltodict.parse(content)) + + samples = re.findall( + r'"#text"\s*:\s*"([^\\"]*(?:\\.[^\\"]*)*)"', string_content + ) + if len(samples) > 0: + page_content = "\n".join(samples) + map_symboles = { + "\\n": "\n", + "\\t": "\t", + "\\u2013": "-", + "\\u2019": "'", + "\\u00e9r": "é", + "\\u00f4me": "ô", + } + for key, value in map_symboles.items(): + page_content = page_content.replace(key, value) + + disordered_pages.append({"page": path, "page_content": page_content}) + + # Direct relationships of each page in a dict format + pagexml_rels = [ + { + "path": page_path, + "content": xmltodict.parse( + zfile.read(f"visio/pages/_rels/{Path(page_path).stem}.xml.rels") + ), + } + for page_path in ordered_paths + if f"visio/pages/_rels/{Path(page_path).stem}.xml.rels" in zfile.namelist() + ] + + # Pages in order and with content of their relationships (direct and indirect) + ordered_pages: List[Tuple[int, str, str]] = [] + for page_number, (path, page_name) in enumerate( + zip(ordered_paths, ordered_names) + ): + relationships = self.get_relationships( + path, zfile, ordered_paths, pagexml_rels + ) + page_content = "\n".join( + [ + page_["page_content"] + for page_ in disordered_pages + if page_["page"] in relationships + ] + + [ + page_["page_content"] + for page_ in disordered_pages + if page_["page"] == path + ] + ) + ordered_pages.append((page_number, page_name, page_content)) + + return ordered_pages + + def get_relationships( + self, + page: str, + zfile: zipfile.ZipFile, + filelist: List[str], + pagexml_rels: List[dict], + ) -> Set[str]: + """Get the relationships of a page and the relationships of its relationships, + etc... recursively. + Pages are based on other pages (ex: background page), + so we need to get all the relationships to get all the content of a single page. + """ + + name_path = Path(page).name + parent_path = Path(page).parent + rels_path = parent_path / f"_rels/{name_path}.rels" + + if str(rels_path) not in zfile.namelist(): + return set() + + pagexml_rels_content = next( + page_["content"] for page_ in pagexml_rels if page_["path"] == page + ) + + if isinstance(pagexml_rels_content["Relationships"]["Relationship"], list): + targets = [ + rel["@Target"] + for rel in pagexml_rels_content["Relationships"]["Relationship"] + ] + else: + targets = [pagexml_rels_content["Relationships"]["Relationship"]["@Target"]] + + relationships = set( + [str(parent_path / target) for target in targets] + ).intersection(filelist) + + for rel in relationships: + relationships = relationships | self.get_relationships( + rel, zfile, filelist, pagexml_rels + ) + + return relationships diff --git a/libs/community/langchain_community/document_loaders/surrealdb.py b/libs/community/langchain_community/document_loaders/surrealdb.py new file mode 100644 index 0000000000000..3a96a14a1adbf --- /dev/null +++ b/libs/community/langchain_community/document_loaders/surrealdb.py @@ -0,0 +1,95 @@ +import asyncio +import json +import logging +from typing import Any, Dict, List, Optional + +from langchain_core.documents import Document + +from langchain_community.document_loaders.base import BaseLoader + +logger = logging.getLogger(__name__) + + +class SurrealDBLoader(BaseLoader): + """Load SurrealDB documents.""" + + def __init__( + self, + filter_criteria: Optional[Dict] = None, + **kwargs: Any, + ) -> None: + try: + from surrealdb import Surreal + except ImportError as e: + raise ImportError( + """Cannot import from surrealdb. + please install with `pip install surrealdb`.""" + ) from e + + self.dburl = kwargs.pop("dburl", "ws://localhost:8000/rpc") + + if self.dburl[0:2] == "ws": + self.sdb = Surreal(self.dburl) + else: + raise ValueError("Only websocket connections are supported at this time.") + + self.filter_criteria = filter_criteria or {} + + if "table" in self.filter_criteria: + raise ValueError( + "key `table` is not a valid criteria for `filter_criteria` argument." + ) + + self.ns = kwargs.pop("ns", "langchain") + self.db = kwargs.pop("db", "database") + self.table = kwargs.pop("table", "documents") + self.sdb = Surreal(self.dburl) + self.kwargs = kwargs + + async def initialize(self) -> None: + """ + Initialize connection to surrealdb database + and authenticate if credentials are provided + """ + await self.sdb.connect() + if "db_user" in self.kwargs and "db_pass" in self.kwargs: + user = self.kwargs.get("db_user") + password = self.kwargs.get("db_pass") + await self.sdb.signin({"user": user, "pass": password}) + + await self.sdb.use(self.ns, self.db) + + def load(self) -> List[Document]: + async def _load() -> List[Document]: + await self.initialize() + return await self.aload() + + return asyncio.run(_load()) + + async def aload(self) -> List[Document]: + """Load data into Document objects.""" + + query = "SELECT * FROM type::table($table)" + if self.filter_criteria is not None and len(self.filter_criteria) > 0: + query += " WHERE " + for idx, key in enumerate(self.filter_criteria): + query += f""" {"AND" if idx > 0 else ""} {key} = ${key}""" + + metadata = { + "ns": self.ns, + "db": self.db, + "table": self.table, + } + results = await self.sdb.query( + query, {"table": self.table, **self.filter_criteria} + ) + + return [ + ( + Document( + page_content=json.dumps(result), + metadata={"id": result["id"], **result["metadata"], **metadata}, + ) + ) + for result in results[0]["result"] + ] diff --git a/libs/community/langchain_community/document_loaders/unstructured.py b/libs/community/langchain_community/document_loaders/unstructured.py index 9d8223ff86072..b7ee7717056ed 100644 --- a/libs/community/langchain_community/document_loaders/unstructured.py +++ b/libs/community/langchain_community/document_loaders/unstructured.py @@ -170,7 +170,13 @@ def __init__( def _get_elements(self) -> List: from unstructured.partition.auto import partition - return partition(filename=self.file_path, **self.unstructured_kwargs) + if isinstance(self.file_path, list): + elements = [] + for file in self.file_path: + elements.extend(partition(filename=file, **self.unstructured_kwargs)) + return elements + else: + return partition(filename=self.file_path, **self.unstructured_kwargs) def _get_metadata(self) -> dict: return {"source": self.file_path} diff --git a/libs/community/langchain_community/document_loaders/vsdx.py b/libs/community/langchain_community/document_loaders/vsdx.py new file mode 100644 index 0000000000000..e0929e4019279 --- /dev/null +++ b/libs/community/langchain_community/document_loaders/vsdx.py @@ -0,0 +1,53 @@ +import os +import tempfile +from abc import ABC +from typing import List +from urllib.parse import urlparse + +import requests + +from langchain_community.docstore.document import Document +from langchain_community.document_loaders.base import BaseLoader +from langchain_community.document_loaders.blob_loaders import Blob +from langchain_community.document_loaders.parsers import VsdxParser + + +class VsdxLoader(BaseLoader, ABC): + def __init__(self, file_path: str): + """Initialize with file path.""" + self.file_path = file_path + if "~" in self.file_path: + self.file_path = os.path.expanduser(self.file_path) + + # If the file is a web path, download it to a temporary file, and use that + if not os.path.isfile(self.file_path) and self._is_valid_url(self.file_path): + r = requests.get(self.file_path) + + if r.status_code != 200: + raise ValueError( + "Check the url of your file; returned status code %s" + % r.status_code + ) + + self.web_path = self.file_path + self.temp_file = tempfile.NamedTemporaryFile() + self.temp_file.write(r.content) + self.file_path = self.temp_file.name + elif not os.path.isfile(self.file_path): + raise ValueError("File path %s is not a valid file or url" % self.file_path) + + self.parser = VsdxParser() + + def __del__(self) -> None: + if hasattr(self, "temp_file"): + self.temp_file.close() + + @staticmethod + def _is_valid_url(url: str) -> bool: + """Check if the url is valid.""" + parsed = urlparse(url) + return bool(parsed.netloc) and bool(parsed.scheme) + + def load(self) -> List[Document]: + blob = Blob.from_path(self.file_path) + return list(self.parser.parse(blob)) diff --git a/libs/community/langchain_community/document_transformers/doctran_text_extract.py b/libs/community/langchain_community/document_transformers/doctran_text_extract.py index eee109193eefc..e942eafdde85d 100644 --- a/libs/community/langchain_community/document_transformers/doctran_text_extract.py +++ b/libs/community/langchain_community/document_transformers/doctran_text_extract.py @@ -66,7 +66,27 @@ def __init__( async def atransform_documents( self, documents: Sequence[Document], **kwargs: Any ) -> Sequence[Document]: - raise NotImplementedError + """Extracts properties from text documents using doctran.""" + try: + from doctran import Doctran, ExtractProperty + + doctran = Doctran( + openai_api_key=self.openai_api_key, openai_model=self.openai_api_model + ) + except ImportError: + raise ImportError( + "Install doctran to use this parser. (pip install doctran)" + ) + properties = [ExtractProperty(**property) for property in self.properties] + for d in documents: + doctran_doc = ( + doctran.parse(content=d.page_content) + .extract(properties=properties) + .execute() + ) + + d.metadata["extracted_properties"] = doctran_doc.extracted_properties + return documents def transform_documents( self, documents: Sequence[Document], **kwargs: Any diff --git a/libs/community/langchain_community/embeddings/__init__.py b/libs/community/langchain_community/embeddings/__init__.py index 9b9deba027c19..f39787b3556d1 100644 --- a/libs/community/langchain_community/embeddings/__init__.py +++ b/libs/community/langchain_community/embeddings/__init__.py @@ -57,7 +57,10 @@ from langchain_community.embeddings.llm_rails import LLMRailsEmbeddings from langchain_community.embeddings.localai import LocalAIEmbeddings from langchain_community.embeddings.minimax import MiniMaxEmbeddings -from langchain_community.embeddings.mlflow import MlflowEmbeddings +from langchain_community.embeddings.mlflow import ( + MlflowCohereEmbeddings, + MlflowEmbeddings, +) from langchain_community.embeddings.mlflow_gateway import MlflowAIGatewayEmbeddings from langchain_community.embeddings.modelscope_hub import ModelScopeEmbeddings from langchain_community.embeddings.mosaicml import MosaicMLInstructorEmbeddings @@ -102,6 +105,7 @@ "LLMRailsEmbeddings", "HuggingFaceHubEmbeddings", "MlflowEmbeddings", + "MlflowCohereEmbeddings", "MlflowAIGatewayEmbeddings", "ModelScopeEmbeddings", "TensorflowHubEmbeddings", diff --git a/libs/community/langchain_community/embeddings/bedrock.py b/libs/community/langchain_community/embeddings/bedrock.py index 529809fb91163..7ab94df4dcb92 100644 --- a/libs/community/langchain_community/embeddings/bedrock.py +++ b/libs/community/langchain_community/embeddings/bedrock.py @@ -3,6 +3,7 @@ import os from typing import Any, Dict, List, Optional +import numpy as np from langchain_core.embeddings import Embeddings from langchain_core.pydantic_v1 import BaseModel, Extra, root_validator from langchain_core.runnables.config import run_in_executor @@ -64,6 +65,9 @@ class BedrockEmbeddings(BaseModel, Embeddings): endpoint_url: Optional[str] = None """Needed if you don't want to default to us-east-1 endpoint""" + normalize: bool = False + """Whether the embeddings should be normalized to unit vectors""" + class Config: """Configuration for this pydantic object.""" @@ -145,6 +149,12 @@ def _embedding_func(self, text: str) -> List[float]: except Exception as e: raise ValueError(f"Error raised by inference endpoint: {e}") + def _normalize_vector(self, embeddings: List[float]) -> List[float]: + """Normalize the embedding to a unit vector.""" + emb = np.array(embeddings) + norm_emb = emb / np.linalg.norm(emb) + return norm_emb.tolist() + def embed_documents(self, texts: List[str]) -> List[List[float]]: """Compute doc embeddings using a Bedrock model. @@ -157,7 +167,12 @@ def embed_documents(self, texts: List[str]) -> List[List[float]]: results = [] for text in texts: response = self._embedding_func(text) + + if self.normalize: + response = self._normalize_vector(response) + results.append(response) + return results def embed_query(self, text: str) -> List[float]: @@ -169,7 +184,12 @@ def embed_query(self, text: str) -> List[float]: Returns: Embeddings for the text. """ - return self._embedding_func(text) + embedding = self._embedding_func(text) + + if self.normalize: + return self._normalize_vector(embedding) + + return embedding async def aembed_query(self, text: str) -> List[float]: """Asynchronous compute query embeddings using a Bedrock model. diff --git a/libs/community/langchain_community/embeddings/ernie.py b/libs/community/langchain_community/embeddings/ernie.py index 5467e4c027814..b6bff742bd1e4 100644 --- a/libs/community/langchain_community/embeddings/ernie.py +++ b/libs/community/langchain_community/embeddings/ernie.py @@ -4,6 +4,7 @@ from typing import Dict, List, Optional import requests +from langchain_core._api.deprecation import deprecated from langchain_core.embeddings import Embeddings from langchain_core.pydantic_v1 import BaseModel, root_validator from langchain_core.runnables.config import run_in_executor @@ -12,6 +13,10 @@ logger = logging.getLogger(__name__) +@deprecated( + since="0.0.13", + alternative="langchain_community.embeddings.QianfanEmbeddingsEndpoint", +) class ErnieEmbeddings(BaseModel, Embeddings): """`Ernie Embeddings V1` embedding models.""" diff --git a/libs/community/langchain_community/embeddings/huggingface.py b/libs/community/langchain_community/embeddings/huggingface.py index 84a568866f178..068aafa1fe1db 100644 --- a/libs/community/langchain_community/embeddings/huggingface.py +++ b/libs/community/langchain_community/embeddings/huggingface.py @@ -2,7 +2,7 @@ import requests from langchain_core.embeddings import Embeddings -from langchain_core.pydantic_v1 import BaseModel, Extra, Field +from langchain_core.pydantic_v1 import BaseModel, Extra, Field, SecretStr DEFAULT_MODEL_NAME = "sentence-transformers/all-mpnet-base-v2" DEFAULT_INSTRUCT_MODEL = "hkunlp/instructor-large" @@ -275,7 +275,7 @@ class HuggingFaceInferenceAPIEmbeddings(BaseModel, Embeddings): Requires a HuggingFace Inference API key and a model name. """ - api_key: str + api_key: SecretStr """Your API key for the HuggingFace Inference API.""" model_name: str = "sentence-transformers/all-MiniLM-L6-v2" """The name of the model to use for text embeddings.""" @@ -297,7 +297,7 @@ def _default_api_url(self) -> str: @property def _headers(self) -> dict: - return {"Authorization": f"Bearer {self.api_key}"} + return {"Authorization": f"Bearer {self.api_key.get_secret_value()}"} def embed_documents(self, texts: List[str]) -> List[List[float]]: """Get the embeddings for a list of texts. diff --git a/libs/community/langchain_community/embeddings/huggingface_hub.py b/libs/community/langchain_community/embeddings/huggingface_hub.py index 465f8c69256a2..70424ac0b2181 100644 --- a/libs/community/langchain_community/embeddings/huggingface_hub.py +++ b/libs/community/langchain_community/embeddings/huggingface_hub.py @@ -144,5 +144,5 @@ async def aembed_query(self, text: str) -> List[float]: Returns: Embeddings for the text. """ - response = await self.aembed_documents([text])[0] + response = (await self.aembed_documents([text]))[0] return response diff --git a/libs/community/langchain_community/embeddings/mlflow.py b/libs/community/langchain_community/embeddings/mlflow.py index 0ae46bcffdb79..6b24dacb02575 100644 --- a/libs/community/langchain_community/embeddings/mlflow.py +++ b/libs/community/langchain_community/embeddings/mlflow.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Any, Iterator, List +from typing import Any, Dict, Iterator, List from urllib.parse import urlparse from langchain_core.embeddings import Embeddings @@ -34,6 +34,10 @@ class MlflowEmbeddings(Embeddings, BaseModel): target_uri: str """The target URI to use.""" _client: Any = PrivateAttr() + """The parameters to use for queries.""" + query_params: Dict[str, str] = {} + """The parameters to use for documents.""" + documents_params: Dict[str, str] = {} def __init__(self, **kwargs: Any): super().__init__(**kwargs) @@ -63,12 +67,22 @@ def _validate_uri(self) -> None: f"The scheme must be one of {allowed}." ) - def embed_documents(self, texts: List[str]) -> List[List[float]]: + def embed(self, texts: List[str], params: Dict[str, str]) -> List[List[float]]: embeddings: List[List[float]] = [] for txt in _chunk(texts, 20): - resp = self._client.predict(endpoint=self.endpoint, inputs={"input": txt}) + resp = self._client.predict( + endpoint=self.endpoint, inputs={"input": txt, **params} + ) embeddings.extend(r["embedding"] for r in resp["data"]) return embeddings + def embed_documents(self, texts: List[str]) -> List[List[float]]: + return self.embed(texts, params=self.documents_params) + def embed_query(self, text: str) -> List[float]: - return self.embed_documents([text])[0] + return self.embed([text], params=self.query_params)[0] + + +class MlflowCohereEmbeddings(MlflowEmbeddings): + query_params: Dict[str, str] = {"input_type": "search_query"} + documents_params: Dict[str, str] = {"input_type": "search_document"} diff --git a/libs/community/langchain_community/embeddings/ollama.py b/libs/community/langchain_community/embeddings/ollama.py index 9b9830fb0a0aa..f1c28f1124e7b 100644 --- a/libs/community/langchain_community/embeddings/ollama.py +++ b/libs/community/langchain_community/embeddings/ollama.py @@ -97,7 +97,7 @@ class OllamaEmbeddings(BaseModel, Embeddings): will give more diverse answers, while a lower value (e.g. 10) will be more conservative. (Default: 40)""" - top_p: Optional[int] = None + top_p: Optional[float] = None """Works together with top-k. A higher value (e.g., 0.95) will lead to more diverse text, while a lower value (e.g., 0.5) will generate more focused and conservative text. (Default: 0.9)""" diff --git a/libs/community/langchain_community/embeddings/yandex.py b/libs/community/langchain_community/embeddings/yandex.py index d96fb46869ab1..4022eb1199bb3 100644 --- a/libs/community/langchain_community/embeddings/yandex.py +++ b/libs/community/langchain_community/embeddings/yandex.py @@ -5,8 +5,8 @@ from typing import Any, Callable, Dict, List from langchain_core.embeddings import Embeddings -from langchain_core.pydantic_v1 import BaseModel, root_validator -from langchain_core.utils import get_from_dict_or_env +from langchain_core.pydantic_v1 import BaseModel, SecretStr, root_validator +from langchain_core.utils import convert_to_secret_str, get_from_dict_or_env from tenacity import ( before_sleep_log, retry, @@ -41,18 +41,16 @@ class YandexGPTEmbeddings(BaseModel, Embeddings): embeddings = YandexGPTEmbeddings(iam_token="t1.9eu...", model_uri="emb:///text-search-query/latest") """ - iam_token: str = "" + iam_token: SecretStr = "" """Yandex Cloud IAM token for service account with the `ai.languageModels.user` role""" - api_key: str = "" + api_key: SecretStr = "" """Yandex Cloud Api Key for service account with the `ai.languageModels.user` role""" model_uri: str = "" """Model uri to use.""" folder_id: str = "" """Yandex Cloud folder ID""" - model_uri: str = "" - """Model uri to use.""" model_name: str = "text-search-query" """Model name to use.""" model_version: str = "latest" @@ -66,23 +64,27 @@ class YandexGPTEmbeddings(BaseModel, Embeddings): def validate_environment(cls, values: Dict) -> Dict: """Validate that iam token exists in environment.""" - iam_token = get_from_dict_or_env(values, "iam_token", "YC_IAM_TOKEN", "") + iam_token = convert_to_secret_str( + get_from_dict_or_env(values, "iam_token", "YC_IAM_TOKEN", "") + ) values["iam_token"] = iam_token - api_key = get_from_dict_or_env(values, "api_key", "YC_API_KEY", "") + api_key = convert_to_secret_str( + get_from_dict_or_env(values, "api_key", "YC_API_KEY", "") + ) values["api_key"] = api_key folder_id = get_from_dict_or_env(values, "folder_id", "YC_FOLDER_ID", "") values["folder_id"] = folder_id - if api_key == "" and iam_token == "": + if api_key.get_secret_value() == "" and iam_token.get_secret_value() == "": raise ValueError("Either 'YC_API_KEY' or 'YC_IAM_TOKEN' must be provided.") if values["iam_token"]: values["_grpc_metadata"] = [ - ("authorization", f"Bearer {values['iam_token']}") + ("authorization", f"Bearer {values['iam_token'].get_secret_value()}") ] if values["folder_id"]: values["_grpc_metadata"].append(("x-folder-id", values["folder_id"])) else: values["_grpc_metadata"] = ( - ("authorization", f"Api-Key {values['api_key']}"), + ("authorization", f"Api-Key {values['api_key'].get_secret_value()}"), ) if values["model_uri"] == "" and values["folder_id"] == "": raise ValueError("Either 'model_uri' or 'folder_id' must be provided.") diff --git a/libs/community/langchain_community/graphs/__init__.py b/libs/community/langchain_community/graphs/__init__.py index 7de3bdbc7bda4..4037f1572143f 100644 --- a/libs/community/langchain_community/graphs/__init__.py +++ b/libs/community/langchain_community/graphs/__init__.py @@ -10,6 +10,7 @@ from langchain_community.graphs.neptune_graph import NeptuneGraph from langchain_community.graphs.networkx_graph import NetworkxEntityGraph from langchain_community.graphs.rdf_graph import RdfGraph +from langchain_community.graphs.tigergraph_graph import TigerGraph __all__ = [ "MemgraphGraph", @@ -22,4 +23,5 @@ "RdfGraph", "ArangoGraph", "FalkorDBGraph", + "TigerGraph", ] diff --git a/libs/community/langchain_community/graphs/memgraph_graph.py b/libs/community/langchain_community/graphs/memgraph_graph.py index 2df4612a2c09b..34e9f7145bb22 100644 --- a/libs/community/langchain_community/graphs/memgraph_graph.py +++ b/libs/community/langchain_community/graphs/memgraph_graph.py @@ -1,12 +1,6 @@ from langchain_community.graphs.neo4j_graph import Neo4jGraph SCHEMA_QUERY = """ -CALL llm_util.schema("prompt_ready") -YIELD * -RETURN * -""" - -RAW_SCHEMA_QUERY = """ CALL llm_util.schema("raw") YIELD * RETURN * @@ -39,10 +33,39 @@ def refresh_schema(self) -> None: Refreshes the Memgraph graph schema information. """ - db_schema = self.query(SCHEMA_QUERY)[0].get("schema") - assert db_schema is not None - self.schema = db_schema - - db_structured_schema = self.query(RAW_SCHEMA_QUERY)[0].get("schema") + db_structured_schema = self.query(SCHEMA_QUERY)[0].get("schema") assert db_structured_schema is not None self.structured_schema = db_structured_schema + + # Format node properties + formatted_node_props = [] + + for node_name, properties in db_structured_schema["node_props"].items(): + formatted_node_props.append( + f"Node name: '{node_name}', Node properties: {properties}" + ) + + # Format relationship properties + formatted_rel_props = [] + for rel_name, properties in db_structured_schema["rel_props"].items(): + formatted_rel_props.append( + f"Relationship name: '{rel_name}', " + f"Relationship properties: {properties}" + ) + + # Format relationships + formatted_rels = [ + f"(:{rel['start']})-[:{rel['type']}]->(:{rel['end']})" + for rel in db_structured_schema["relationships"] + ] + + self.schema = "\n".join( + [ + "Node properties are the following:", + *formatted_node_props, + "Relationship properties are the following:", + *formatted_rel_props, + "The relationships are the following:", + *formatted_rels, + ] + ) diff --git a/libs/community/langchain_community/graphs/neo4j_graph.py b/libs/community/langchain_community/graphs/neo4j_graph.py index af416b29bc541..b8970c06ecce5 100644 --- a/libs/community/langchain_community/graphs/neo4j_graph.py +++ b/libs/community/langchain_community/graphs/neo4j_graph.py @@ -31,8 +31,50 @@ """ +def value_sanitize(d: Dict[str, Any]) -> Dict[str, Any]: + """ + Sanitizes the input dictionary by removing embedding-like values, + lists with more than 128 elements, that are mostly irrelevant for + generating answers in a LLM context. These properties, if left in + results, can occupy significant context space and detract from + the LLM's performance by introducing unnecessary noise and cost. + """ + LIST_LIMIT = 128 + # Create a new dictionary to avoid changing size during iteration + new_dict = {} + for key, value in d.items(): + if isinstance(value, dict): + # Recurse to handle nested dictionaries + new_dict[key] = value_sanitize(value) + elif isinstance(value, list): + # check if it has less than LIST_LIMIT values + if len(value) < LIST_LIMIT: + # if value is a list, check if it contains dictionaries to clean + cleaned_list = [] + for item in value: + if isinstance(item, dict): + cleaned_list.append(value_sanitize(item)) + else: + cleaned_list.append(item) + new_dict[key] = cleaned_list + else: + new_dict[key] = value + return new_dict + + class Neo4jGraph(GraphStore): - """Neo4j wrapper for graph operations. + """Provides a connection to a Neo4j database for various graph operations. + Parameters: + url (Optional[str]): The URL of the Neo4j database server. + username (Optional[str]): The username for database authentication. + password (Optional[str]): The password for database authentication. + database (str): The name of the database to connect to. Default is 'neo4j'. + timeout (Optional[float]): The timeout for transactions in seconds. + Useful for terminating long-running queries. + By default, there is no timeout set. + sanitize (bool): A flag to indicate whether to remove lists with + more than 128 elements from results. Useful for removing + embedding-like properties from database responses. Default is False. *Security note*: Make sure that the database connection uses credentials that are narrowly-scoped to only include necessary permissions. @@ -52,6 +94,8 @@ def __init__( username: Optional[str] = None, password: Optional[str] = None, database: str = "neo4j", + timeout: Optional[float] = None, + sanitize: bool = False, ) -> None: """Create a new Neo4j graph wrapper instance.""" try: @@ -69,6 +113,8 @@ def __init__( self._driver = neo4j.GraphDatabase.driver(url, auth=(username, password)) self._database = database + self.timeout = timeout + self.sanitize = sanitize self.schema: str = "" self.structured_schema: Dict[str, Any] = {} # Verify connection @@ -106,12 +152,16 @@ def get_structured_schema(self) -> Dict[str, Any]: def query(self, query: str, params: dict = {}) -> List[Dict[str, Any]]: """Query Neo4j database.""" + from neo4j import Query from neo4j.exceptions import CypherSyntaxError with self._driver.session(database=self._database) as session: try: - data = session.run(query, params) - return [r.data() for r in data] + data = session.run(Query(text=query, timeout=self.timeout), params) + json_data = [r.data() for r in data] + if self.sanitize: + json_data = [value_sanitize(el) for el in json_data] + return json_data except CypherSyntaxError as e: raise ValueError(f"Generated Cypher Statement is not valid\n{e}") diff --git a/libs/community/langchain_community/graphs/tigergraph_graph.py b/libs/community/langchain_community/graphs/tigergraph_graph.py new file mode 100644 index 0000000000000..cff2f4e2ce7fa --- /dev/null +++ b/libs/community/langchain_community/graphs/tigergraph_graph.py @@ -0,0 +1,94 @@ +from typing import Any, Dict, List, Optional + +from langchain_community.graphs.graph_store import GraphStore + + +class TigerGraph(GraphStore): + """TigerGraph wrapper for graph operations. + + *Security note*: Make sure that the database connection uses credentials + that are narrowly-scoped to only include necessary permissions. + Failure to do so may result in data corruption or loss, since the calling + code may attempt commands that would result in deletion, mutation + of data if appropriately prompted or reading sensitive data if such + data is present in the database. + The best way to guard against such negative outcomes is to (as appropriate) + limit the permissions granted to the credentials used with this tool. + + See https://python.langchain.com/docs/security for more information. + """ + + def __init__(self, conn: Any) -> None: + """Create a new TigerGraph graph wrapper instance.""" + self.set_connection(conn) + self.set_schema() + + @property + def conn(self) -> Any: + return self._conn + + @property + def schema(self) -> Dict[str, Any]: + return self._schema + + def get_schema(self) -> str: + if self._schema: + return str(self._schema) + else: + self.set_schema() + return str(self._schema) + + def set_connection(self, conn: Any) -> None: + from pyTigerGraph import TigerGraphConnection + + if not isinstance(conn, TigerGraphConnection): + msg = "**conn** parameter must inherit from TigerGraphConnection" + raise TypeError(msg) + + if conn.ai.nlqs_host is None: + msg = """**conn** parameter does not have nlqs_host parameter defined. + Define hostname of NLQS service.""" + raise ConnectionError(msg) + + self._conn: TigerGraphConnection = conn + self.set_schema() + + def set_schema(self, schema: Optional[Dict[str, Any]] = None) -> None: + """ + Set the schema of the TigerGraph Database. + Auto-generates Schema if **schema** is None. + """ + self._schema = self.generate_schema() if schema is None else schema + + def generate_schema( + self, + ) -> Dict[str, List[Dict[str, Any]]]: + """ + Generates the schema of the TigerGraph Database and returns it + User can specify a **sample_ratio** (0 to 1) to determine the + ratio of documents/edges used (in relation to the Collection size) + to render each Collection Schema. + """ + return self._conn.getSchema(force=True) + + def refresh_schema(self): + self.generate_schema() + + def query(self, query: str) -> Dict[str, Any]: + """Query the TigerGraph database.""" + answer = self._conn.ai.query(query) + return answer + + def register_query( + self, + function_header: str, + description: str, + docstring: str, + param_types: dict = {}, + ) -> List[str]: + """ + Wrapper function to register a custom GSQL query to the TigerGraph NLQS. + """ + return self._conn.ai.registerCustomQuery( + function_header, description, docstring, param_types + ) diff --git a/libs/community/langchain_community/llms/__init__.py b/libs/community/langchain_community/llms/__init__.py index d13d099dc1eca..13d3607918eeb 100644 --- a/libs/community/langchain_community/llms/__init__.py +++ b/libs/community/langchain_community/llms/__init__.py @@ -270,6 +270,12 @@ def _import_koboldai() -> Any: return KoboldApiLLM +def _import_konko() -> Any: + from langchain_community.llms.konko import Konko + + return Konko + + def _import_llamacpp() -> Any: from langchain_community.llms.llamacpp import LlamaCpp @@ -639,6 +645,8 @@ def __getattr__(name: str) -> Any: return _import_javelin_ai_gateway() elif name == "KoboldApiLLM": return _import_koboldai() + elif name == "Konko": + return _import_konko() elif name == "LlamaCpp": return _import_llamacpp() elif name == "ManifestWrapper": @@ -780,6 +788,7 @@ def __getattr__(name: str) -> Any: "HuggingFaceTextGenInference", "HumanInputLLM", "KoboldApiLLM", + "Konko", "LlamaCpp", "TextGen", "ManifestWrapper", @@ -868,6 +877,7 @@ def get_type_to_cls_dict() -> Dict[str, Callable[[], Type[BaseLLM]]]: "huggingface_textgen_inference": _import_huggingface_text_gen_inference, "human-input": _import_human, "koboldai": _import_koboldai, + "konko": _import_konko, "llamacpp": _import_llamacpp, "textgen": _import_textgen, "minimax": _import_minimax, diff --git a/libs/community/langchain_community/llms/azureml_endpoint.py b/libs/community/langchain_community/llms/azureml_endpoint.py index c9e73df6c6345..3480a801f96f6 100644 --- a/libs/community/langchain_community/llms/azureml_endpoint.py +++ b/libs/community/langchain_community/llms/azureml_endpoint.py @@ -2,12 +2,14 @@ import urllib.request import warnings from abc import abstractmethod +from enum import Enum from typing import Any, Dict, List, Mapping, Optional -from langchain_core.callbacks import CallbackManagerForLLMRun -from langchain_core.language_models.llms import LLM -from langchain_core.pydantic_v1 import BaseModel, validator -from langchain_core.utils import get_from_dict_or_env +from langchain_core.callbacks.manager import CallbackManagerForLLMRun +from langchain_core.language_models.llms import BaseLLM +from langchain_core.outputs import Generation, LLMResult +from langchain_core.pydantic_v1 import BaseModel, SecretStr, root_validator, validator +from langchain_core.utils import convert_to_secret_str, get_from_dict_or_env class AzureMLEndpointClient(object): @@ -26,7 +28,12 @@ def __init__( self.endpoint_api_key = endpoint_api_key self.deployment_name = deployment_name - def call(self, body: bytes, **kwargs: Any) -> bytes: + def call( + self, + body: bytes, + run_manager: Optional[CallbackManagerForLLMRun] = None, + **kwargs: Any, + ) -> bytes: """call.""" # The azureml-model-deployment header will force the request to go to a @@ -45,6 +52,16 @@ def call(self, body: bytes, **kwargs: Any) -> bytes: return result +class AzureMLEndpointApiType(str, Enum): + """Azure ML endpoints API types. Use `realtime` for models deployed in hosted + infrastructure, or `serverless` for models deployed as a service with a + pay-as-you-go billing or PTU. + """ + + realtime = "realtime" + serverless = "serverless" + + class ContentFormatterBase: """Transform request and response of AzureML endpoint to match with required schema. @@ -61,7 +78,8 @@ class ContentFormatter(ContentFormatterBase): def format_request_payload( self, prompt: str, - model_kwargs: Dict + model_kwargs: Dict, + api_type: AzureMLEndpointApiType, ) -> bytes: input_str = json.dumps( { @@ -71,7 +89,9 @@ def format_request_payload( ) return str.encode(input_str) - def format_response_payload(self, output: str) -> str: + def format_response_payload( + self, output: str, api_type: AzureMLEndpointApiType + ) -> str: response_json = json.loads(output) return response_json[0]["0"] """ @@ -81,6 +101,12 @@ def format_response_payload(self, output: str) -> str: accepts: Optional[str] = "application/json" """The MIME type of the response data returned from the endpoint""" + format_error_msg: Optional[str] = ( + "Error while formatting response payload for chat model of type " + " `{api_type}`. Are you using the right formatter for the deployed " + " model and endpoint type?" + ) + @staticmethod def escape_special_characters(prompt: str) -> str: """Escapes any special characters in `prompt`""" @@ -100,15 +126,32 @@ def escape_special_characters(prompt: str) -> str: return prompt + @property + def supported_api_types(self) -> List[AzureMLEndpointApiType]: + """Supported APIs for the given formatter. Azure ML supports + deploying models using different hosting methods. Each method may have + a different API structure.""" + + return [AzureMLEndpointApiType.realtime] + @abstractmethod - def format_request_payload(self, prompt: str, model_kwargs: Dict) -> bytes: + def format_request_payload( + self, + prompt: str, + model_kwargs: Dict, + api_type: AzureMLEndpointApiType = AzureMLEndpointApiType.realtime, + ) -> bytes: """Formats the request body according to the input schema of the model. Returns bytes or seekable file like object in the format specified in the content_type request header. """ @abstractmethod - def format_response_payload(self, output: bytes) -> str: + def format_response_payload( + self, + output: bytes, + api_type: AzureMLEndpointApiType = AzureMLEndpointApiType.realtime, + ) -> Generation: """Formats the response body according to the output schema of the model. Returns the data type that is received from the response. @@ -118,15 +161,27 @@ def format_response_payload(self, output: bytes) -> str: class GPT2ContentFormatter(ContentFormatterBase): """Content handler for GPT2""" - def format_request_payload(self, prompt: str, model_kwargs: Dict) -> bytes: + @property + def supported_api_types(self) -> List[AzureMLEndpointApiType]: + return [AzureMLEndpointApiType.realtime] + + def format_request_payload( + self, prompt: str, model_kwargs: Dict, api_type: AzureMLEndpointApiType + ) -> bytes: prompt = ContentFormatterBase.escape_special_characters(prompt) request_payload = json.dumps( {"inputs": {"input_string": [f'"{prompt}"']}, "parameters": model_kwargs} ) return str.encode(request_payload) - def format_response_payload(self, output: bytes) -> str: - return json.loads(output)[0]["0"] + def format_response_payload( + self, output: bytes, api_type: AzureMLEndpointApiType + ) -> Generation: + try: + choice = json.loads(output)[0]["0"] + except (KeyError, IndexError, TypeError) as e: + raise ValueError(self.format_error_msg.format(api_type=api_type)) from e + return Generation(text=choice) class OSSContentFormatter(GPT2ContentFormatter): @@ -148,21 +203,39 @@ def __init__(self) -> None: class HFContentFormatter(ContentFormatterBase): """Content handler for LLMs from the HuggingFace catalog.""" - def format_request_payload(self, prompt: str, model_kwargs: Dict) -> bytes: + @property + def supported_api_types(self) -> List[AzureMLEndpointApiType]: + return [AzureMLEndpointApiType.realtime] + + def format_request_payload( + self, prompt: str, model_kwargs: Dict, api_type: AzureMLEndpointApiType + ) -> bytes: ContentFormatterBase.escape_special_characters(prompt) request_payload = json.dumps( {"inputs": [f'"{prompt}"'], "parameters": model_kwargs} ) return str.encode(request_payload) - def format_response_payload(self, output: bytes) -> str: - return json.loads(output)[0]["generated_text"] + def format_response_payload( + self, output: bytes, api_type: AzureMLEndpointApiType + ) -> Generation: + try: + choice = json.loads(output)[0]["0"]["generated_text"] + except (KeyError, IndexError, TypeError) as e: + raise ValueError(self.format_error_msg.format(api_type=api_type)) from e + return Generation(text=choice) class DollyContentFormatter(ContentFormatterBase): """Content handler for the Dolly-v2-12b model""" - def format_request_payload(self, prompt: str, model_kwargs: Dict) -> bytes: + @property + def supported_api_types(self) -> List[AzureMLEndpointApiType]: + return [AzureMLEndpointApiType.realtime] + + def format_request_payload( + self, prompt: str, model_kwargs: Dict, api_type: AzureMLEndpointApiType + ) -> bytes: prompt = ContentFormatterBase.escape_special_characters(prompt) request_payload = json.dumps( { @@ -172,49 +245,88 @@ def format_request_payload(self, prompt: str, model_kwargs: Dict) -> bytes: ) return str.encode(request_payload) - def format_response_payload(self, output: bytes) -> str: - return json.loads(output)[0] + def format_response_payload( + self, output: bytes, api_type: AzureMLEndpointApiType + ) -> Generation: + try: + choice = json.loads(output)[0] + except (KeyError, IndexError, TypeError) as e: + raise ValueError(self.format_error_msg.format(api_type=api_type)) from e + return Generation(text=choice) class LlamaContentFormatter(ContentFormatterBase): """Content formatter for LLaMa""" - def format_request_payload(self, prompt: str, model_kwargs: Dict) -> bytes: + @property + def supported_api_types(self) -> List[AzureMLEndpointApiType]: + return [AzureMLEndpointApiType.realtime, AzureMLEndpointApiType.serverless] + + def format_request_payload( + self, prompt: str, model_kwargs: Dict, api_type: AzureMLEndpointApiType + ) -> bytes: """Formats the request according to the chosen api""" prompt = ContentFormatterBase.escape_special_characters(prompt) - request_payload = json.dumps( - { - "input_data": { - "input_string": [f'"{prompt}"'], - "parameters": model_kwargs, + if api_type == AzureMLEndpointApiType.realtime: + request_payload = json.dumps( + { + "input_data": { + "input_string": [f'"{prompt}"'], + "parameters": model_kwargs, + } } - } - ) + ) + elif api_type == AzureMLEndpointApiType.serverless: + request_payload = json.dumps({"prompt": prompt, **model_kwargs}) + else: + raise ValueError( + f"`api_type` {api_type} is not supported by this formatter" + ) return str.encode(request_payload) - def format_response_payload(self, output: bytes) -> str: + def format_response_payload( + self, output: bytes, api_type: AzureMLEndpointApiType + ) -> Generation: """Formats response""" - return json.loads(output)[0]["0"] - + if api_type == AzureMLEndpointApiType.realtime: + try: + choice = json.loads(output)[0]["0"] + except (KeyError, IndexError, TypeError) as e: + raise ValueError(self.format_error_msg.format(api_type=api_type)) from e + return Generation(text=choice) + if api_type == AzureMLEndpointApiType.serverless: + try: + choice = json.loads(output)["choices"][0] + if not isinstance(choice, dict): + raise TypeError( + "Endpoint response is not well formed for a chat " + "model. Expected `dict` but `{type(choice)}` was " + "received." + ) + except (KeyError, IndexError, TypeError) as e: + raise ValueError(self.format_error_msg.format(api_type=api_type)) from e + return Generation( + text=choice["text"].strip(), + generation_info=dict( + finish_reason=choice.get("finish_reason"), + logprobs=choice.get("logprobs"), + ), + ) + raise ValueError(f"`api_type` {api_type} is not supported by this formatter") -class AzureMLOnlineEndpoint(LLM, BaseModel): - """Azure ML Online Endpoint models. - Example: - .. code-block:: python - - azure_llm = AzureMLOnlineEndpoint( - endpoint_url="https://..inference.ml.azure.com/score", - endpoint_api_key="my-api-key", - content_formatter=content_formatter, - ) - """ # noqa: E501 +class AzureMLBaseEndpoint(BaseModel): + """Azure ML Online Endpoint models.""" endpoint_url: str = "" """URL of pre-existing Endpoint. Should be passed to constructor or specified as env var `AZUREML_ENDPOINT_URL`.""" - endpoint_api_key: str = "" + endpoint_api_type: AzureMLEndpointApiType = AzureMLEndpointApiType.realtime + """Type of the endpoint being consumed. Possible values are `serverless` for + pay-as-you-go and `realtime` for real-time endpoints. """ + + endpoint_api_key: SecretStr = convert_to_secret_str("") """Authentication Key for Endpoint. Should be passed to constructor or specified as env var `AZUREML_ENDPOINT_API_KEY`.""" @@ -232,22 +344,106 @@ class AzureMLOnlineEndpoint(LLM, BaseModel): model_kwargs: Optional[dict] = None """Keyword arguments to pass to the model.""" - @validator("http_client", always=True, allow_reuse=True) - @classmethod - def validate_client(cls, field_value: Any, values: Dict) -> AzureMLEndpointClient: - """Validate that api key and python package exists in environment.""" - endpoint_key = get_from_dict_or_env( - values, "endpoint_api_key", "AZUREML_ENDPOINT_API_KEY" + @root_validator(pre=True) + def validate_environ(cls, values: Dict) -> Dict: + values["endpoint_api_key"] = convert_to_secret_str( + get_from_dict_or_env(values, "endpoint_api_key", "AZUREML_ENDPOINT_API_KEY") ) - endpoint_url = get_from_dict_or_env( + values["endpoint_url"] = get_from_dict_or_env( values, "endpoint_url", "AZUREML_ENDPOINT_URL" ) - deployment_name = get_from_dict_or_env( + values["deployment_name"] = get_from_dict_or_env( values, "deployment_name", "AZUREML_DEPLOYMENT_NAME", "" ) - http_client = AzureMLEndpointClient(endpoint_url, endpoint_key, deployment_name) + values["endpoint_api_type"] = get_from_dict_or_env( + values, + "endpoint_api_type", + "AZUREML_ENDPOINT_API_TYPE", + AzureMLEndpointApiType.realtime, + ) + + return values + + @validator("content_formatter") + def validate_content_formatter( + cls, field_value: Any, values: Dict + ) -> ContentFormatterBase: + """Validate that content formatter is supported by endpoint type.""" + endpoint_api_type = values.get("endpoint_api_type") + if endpoint_api_type not in field_value.supported_api_types: + raise ValueError( + f"Content formatter f{type(field_value)} is not supported by this " + f"endpoint. Supported types are {field_value.supported_api_types} " + f"but endpoint is {endpoint_api_type}." + ) + return field_value + + @validator("endpoint_url") + def validate_endpoint_url(cls, field_value: Any) -> str: + """Validate that endpoint url is complete.""" + if field_value.endswith("/"): + field_value = field_value[:-1] + if field_value.endswith("inference.ml.azure.com"): + raise ValueError( + "`endpoint_url` should contain the full invocation URL including " + "`/score` for `endpoint_api_type='realtime'` or `/v1/completions` " + "or `/v1/chat/completions` for `endpoint_api_type='serverless'`" + ) + return field_value + + @validator("endpoint_api_type") + def validate_endpoint_api_type( + cls, field_value: Any, values: Dict + ) -> AzureMLEndpointApiType: + """Validate that endpoint api type is compatible with the URL format.""" + endpoint_url = values.get("endpoint_url") + if field_value == AzureMLEndpointApiType.realtime and not endpoint_url.endswith( + "/score" + ): + raise ValueError( + "Endpoints of type `realtime` should follow the format " + "`https://..inference.ml.azure.com/score`." + " If your endpoint URL ends with `/v1/completions` or" + "`/v1/chat/completions`, use `endpoint_api_type='serverless'` instead." + ) + if field_value == AzureMLEndpointApiType.serverless and not ( + endpoint_url.endswith("/v1/completions") + or endpoint_url.endswith("/v1/chat/completions") + ): + raise ValueError( + "Endpoints of type `serverless` should follow the format " + "`https://..inference.ml.azure.com/v1/chat/completions`" + " or `https://..inference.ml.azure.com/v1/chat/completions`" + ) + + return field_value + + @validator("http_client", always=True) + def validate_client(cls, field_value: Any, values: Dict) -> AzureMLEndpointClient: + """Validate that api key and python package exists in environment.""" + endpoint_url = values.get("endpoint_url") + endpoint_key = values.get("endpoint_api_key") + deployment_name = values.get("deployment_name") + + http_client = AzureMLEndpointClient( + endpoint_url, endpoint_key.get_secret_value(), deployment_name + ) return http_client + +class AzureMLOnlineEndpoint(BaseLLM, AzureMLBaseEndpoint): + """Azure ML Online Endpoint models. + + Example: + .. code-block:: python + azure_llm = AzureMLOnlineEndpoint( + endpoint_url="https://..inference.ml.azure.com/score", + endpoint_api_type=AzureMLApiType.realtime, + endpoint_api_key="my-api-key", + content_formatter=content_formatter, + ) + """ # noqa: E501 + @property def _identifying_params(self) -> Mapping[str, Any]: """Get the identifying parameters.""" @@ -262,16 +458,17 @@ def _llm_type(self) -> str: """Return type of llm.""" return "azureml_endpoint" - def _call( + def _generate( self, - prompt: str, + prompts: List[str], stop: Optional[List[str]] = None, run_manager: Optional[CallbackManagerForLLMRun] = None, **kwargs: Any, - ) -> str: - """Call out to an AzureML Managed Online endpoint. + ) -> LLMResult: + """Run the LLM on the given prompts. + Args: - prompt: The prompt to pass into the model. + prompts: The prompt to pass into the model. stop: Optional list of stop words to use when generating. Returns: The string generated by the model. @@ -280,12 +477,21 @@ def _call( response = azureml_model("Tell me a joke.") """ _model_kwargs = self.model_kwargs or {} + _model_kwargs.update(kwargs) + if stop: + _model_kwargs["stop"] = stop + generations = [] + + for prompt in prompts: + request_payload = self.content_formatter.format_request_payload( + prompt, _model_kwargs, self.endpoint_api_type + ) + response_payload = self.http_client.call( + body=request_payload, run_manager=run_manager + ) + generated_text = self.content_formatter.format_response_payload( + response_payload, self.endpoint_api_type + ) + generations.append([generated_text]) - request_payload = self.content_formatter.format_request_payload( - prompt, _model_kwargs - ) - response_payload = self.http_client.call(request_payload, **kwargs) - generated_text = self.content_formatter.format_response_payload( - response_payload - ) - return generated_text + return LLMResult(generations=generations) diff --git a/libs/community/langchain_community/llms/bedrock.py b/libs/community/langchain_community/llms/bedrock.py index ddc15d5ef1cbc..5ec60e84967c3 100644 --- a/libs/community/langchain_community/llms/bedrock.py +++ b/libs/community/langchain_community/llms/bedrock.py @@ -1,11 +1,25 @@ from __future__ import annotations +import asyncio import json import warnings from abc import ABC -from typing import TYPE_CHECKING, Any, Dict, Iterator, List, Mapping, Optional +from typing import ( + TYPE_CHECKING, + Any, + AsyncGenerator, + AsyncIterator, + Dict, + Iterator, + List, + Mapping, + Optional, +) -from langchain_core.callbacks import CallbackManagerForLLMRun +from langchain_core.callbacks import ( + AsyncCallbackManagerForLLMRun, + CallbackManagerForLLMRun, +) from langchain_core.language_models.llms import LLM from langchain_core.outputs import GenerationChunk from langchain_core.pydantic_v1 import BaseModel, Extra, Field, root_validator @@ -128,26 +142,56 @@ def prepare_output_stream( if not stream: return - if provider not in cls.provider_to_output_key_map: + output_key = cls.provider_to_output_key_map.get(provider, None) + + if not output_key: raise ValueError( f"Unknown streaming response output key for provider: {provider}" ) for event in stream: chunk = event.get("chunk") - if chunk: - chunk_obj = json.loads(chunk.get("bytes").decode()) - if provider == "cohere" and ( - chunk_obj["is_finished"] - or chunk_obj[cls.provider_to_output_key_map[provider]] - == "" - ): - return - - # chunk obj format varies with provider - yield GenerationChunk( - text=chunk_obj[cls.provider_to_output_key_map[provider]] - ) + if not chunk: + continue + + chunk_obj = json.loads(chunk.get("bytes").decode()) + + if provider == "cohere" and ( + chunk_obj["is_finished"] or chunk_obj[output_key] == "" + ): + return + + yield GenerationChunk(text=chunk_obj[output_key]) + + @classmethod + async def aprepare_output_stream( + cls, provider: str, response: Any, stop: Optional[List[str]] = None + ) -> AsyncIterator[GenerationChunk]: + stream = response.get("body") + + if not stream: + return + + output_key = cls.provider_to_output_key_map.get(provider, None) + + if not output_key: + raise ValueError( + f"Unknown streaming response output key for provider: {provider}" + ) + + for event in stream: + chunk = event.get("chunk") + if not chunk: + continue + + chunk_obj = json.loads(chunk.get("bytes").decode()) + + if provider == "cohere" and ( + chunk_obj["is_finished"] or chunk_obj[output_key] == "" + ): + return + + yield GenerationChunk(text=chunk_obj[output_key]) class BedrockBase(BaseModel, ABC): @@ -272,10 +316,12 @@ def _prepare_input_and_invoke( try: response = self.client.invoke_model( - body=body, modelId=self.model_id, accept=accept, contentType=contentType + body=body, + modelId=self.model_id, + accept=accept, + contentType=contentType, ) text = LLMInputOutputAdapter.prepare_output(provider, response) - except Exception as e: raise ValueError(f"Error raised by bedrock service: {e}").with_traceback( e.__traceback__ @@ -330,6 +376,51 @@ def _prepare_input_and_invoke_stream( if run_manager is not None: run_manager.on_llm_new_token(chunk.text, chunk=chunk) + async def _aprepare_input_and_invoke_stream( + self, + prompt: str, + stop: Optional[List[str]] = None, + run_manager: Optional[AsyncCallbackManagerForLLMRun] = None, + **kwargs: Any, + ) -> AsyncIterator[GenerationChunk]: + _model_kwargs = self.model_kwargs or {} + provider = self._get_provider() + + if stop: + if provider not in self.provider_stop_sequence_key_name_map: + raise ValueError( + f"Stop sequence key name for {provider} is not supported." + ) + _model_kwargs[self.provider_stop_sequence_key_name_map.get(provider)] = stop + + if provider == "cohere": + _model_kwargs["stream"] = True + + params = {**_model_kwargs, **kwargs} + input_body = LLMInputOutputAdapter.prepare_input(provider, prompt, params) + body = json.dumps(input_body) + + response = await asyncio.get_running_loop().run_in_executor( + None, + lambda: self.client.invoke_model_with_response_stream( + body=body, + modelId=self.model_id, + accept="application/json", + contentType="application/json", + ), + ) + + async for chunk in LLMInputOutputAdapter.aprepare_output_stream( + provider, response, stop + ): + yield chunk + if run_manager is not None and asyncio.iscoroutinefunction( + run_manager.on_llm_new_token + ): + await run_manager.on_llm_new_token(chunk.text, chunk=chunk) + elif run_manager is not None: + run_manager.on_llm_new_token(chunk.text, chunk=chunk) + class Bedrock(LLM, BedrockBase): """Bedrock models. @@ -447,6 +538,65 @@ def _call( return self._prepare_input_and_invoke(prompt=prompt, stop=stop, **kwargs) + async def _astream( + self, + prompt: str, + stop: Optional[List[str]] = None, + run_manager: Optional[AsyncCallbackManagerForLLMRun] = None, + **kwargs: Any, + ) -> AsyncGenerator[GenerationChunk, None]: + """Call out to Bedrock service with streaming. + + Args: + prompt (str): The prompt to pass into the model + stop (Optional[List[str]], optional): Stop sequences. These will + override any stop sequences in the `model_kwargs` attribute. + Defaults to None. + run_manager (Optional[CallbackManagerForLLMRun], optional): Callback + run managers used to process the output. Defaults to None. + + Yields: + AsyncGenerator[GenerationChunk, None]: Generator that asynchronously yields + the streamed responses. + """ + async for chunk in self._aprepare_input_and_invoke_stream( + prompt=prompt, stop=stop, run_manager=run_manager, **kwargs + ): + yield chunk + + async def _acall( + self, + prompt: str, + stop: Optional[List[str]] = None, + run_manager: Optional[AsyncCallbackManagerForLLMRun] = None, + **kwargs: Any, + ) -> str: + """Call out to Bedrock service model. + + Args: + prompt: The prompt to pass into the model. + stop: Optional list of stop words to use when generating. + + Returns: + The string generated by the model. + + Example: + .. code-block:: python + + response = await llm._acall("Tell me a joke.") + """ + + if not self.streaming: + raise ValueError("Streaming must be set to True for async operations. ") + + chunks = [ + chunk.text + async for chunk in self._astream( + prompt=prompt, stop=stop, run_manager=run_manager, **kwargs + ) + ] + return "".join(chunks) + def get_num_tokens(self, text: str) -> int: if self._model_is_anthropic: return get_num_tokens_anthropic(text) diff --git a/libs/community/langchain_community/llms/gpt4all.py b/libs/community/langchain_community/llms/gpt4all.py index 83dace226bb29..8b347ceb5c38b 100644 --- a/libs/community/langchain_community/llms/gpt4all.py +++ b/libs/community/langchain_community/llms/gpt4all.py @@ -111,6 +111,7 @@ def _model_param_names() -> Set[str]: "n_batch", "repeat_penalty", "repeat_last_n", + "streaming", } def _default_params(self) -> Dict[str, Any]: @@ -123,6 +124,7 @@ def _default_params(self) -> Dict[str, Any]: "n_batch": self.n_batch, "repeat_penalty": self.repeat_penalty, "repeat_last_n": self.repeat_last_n, + "streaming": self.streaming, } @root_validator() diff --git a/libs/community/langchain_community/llms/huggingface_endpoint.py b/libs/community/langchain_community/llms/huggingface_endpoint.py index d429e2fd93557..c14b2e24a8050 100644 --- a/libs/community/langchain_community/llms/huggingface_endpoint.py +++ b/libs/community/langchain_community/llms/huggingface_endpoint.py @@ -8,7 +8,12 @@ from langchain_community.llms.utils import enforce_stop_tokens -VALID_TASKS = ("text2text-generation", "text-generation", "summarization") +VALID_TASKS = ( + "text2text-generation", + "text-generation", + "summarization", + "conversational", +) class HuggingFaceEndpoint(LLM): @@ -144,6 +149,8 @@ def _call( text = generated_text[0]["generated_text"] elif self.task == "summarization": text = generated_text[0]["summary_text"] + elif self.task == "conversational": + text = generated_text["response"][1] else: raise ValueError( f"Got invalid task {self.task}, " diff --git a/libs/community/langchain_community/llms/huggingface_hub.py b/libs/community/langchain_community/llms/huggingface_hub.py index 32facc244b0cd..f432727773121 100644 --- a/libs/community/langchain_community/llms/huggingface_hub.py +++ b/libs/community/langchain_community/llms/huggingface_hub.py @@ -1,3 +1,4 @@ +import json from typing import Any, Dict, List, Mapping, Optional from langchain_core.callbacks import CallbackManagerForLLMRun @@ -7,8 +8,15 @@ from langchain_community.llms.utils import enforce_stop_tokens -DEFAULT_REPO_ID = "gpt2" -VALID_TASKS = ("text2text-generation", "text-generation", "summarization") +# key: task +# value: key in the output dictionary +VALID_TASKS_DICT = { + "translation": "translation_text", + "summarization": "summary_text", + "conversational": "generated_text", + "text-generation": "generated_text", + "text2text-generation": "generated_text", +} class HuggingFaceHub(LLM): @@ -18,7 +26,8 @@ class HuggingFaceHub(LLM): environment variable ``HUGGINGFACEHUB_API_TOKEN`` set with your API token, or pass it as a named parameter to the constructor. - Only supports `text-generation`, `text2text-generation` and `summarization` for now. + Supports `text-generation`, `text2text-generation`, `conversational`, `translation`, + and `summarization`. Example: .. code-block:: python @@ -28,11 +37,13 @@ class HuggingFaceHub(LLM): """ client: Any #: :meta private: - repo_id: str = DEFAULT_REPO_ID - """Model name to use.""" + repo_id: Optional[str] = None + """Model name to use. + If not provided, the default model for the chosen task will be used.""" task: Optional[str] = None """Task to call the model with. - Should be a task that returns `generated_text` or `summary_text`.""" + Should be a task that returns `generated_text`, `summary_text`, + or `translation_text`.""" model_kwargs: Optional[dict] = None """Keyword arguments to pass to the model.""" @@ -50,18 +61,27 @@ def validate_environment(cls, values: Dict) -> Dict: values, "huggingfacehub_api_token", "HUGGINGFACEHUB_API_TOKEN" ) try: - from huggingface_hub.inference_api import InferenceApi + from huggingface_hub import HfApi, InferenceClient repo_id = values["repo_id"] - client = InferenceApi( - repo_id=repo_id, + client = InferenceClient( + model=repo_id, token=huggingfacehub_api_token, - task=values.get("task"), ) - if client.task not in VALID_TASKS: + if not values["task"]: + if not repo_id: + raise ValueError( + "Must specify either `repo_id` or `task`, or both." + ) + # Use the recommended task for the chosen model + model_info = HfApi(token=huggingfacehub_api_token).model_info( + repo_id=repo_id + ) + values["task"] = model_info.pipeline_tag + if values["task"] not in VALID_TASKS_DICT: raise ValueError( - f"Got invalid task {client.task}, " - f"currently only {VALID_TASKS} are supported" + f"Got invalid task {values['task']}, " + f"currently only {VALID_TASKS_DICT.keys()} are supported" ) values["client"] = client except ImportError: @@ -108,21 +128,20 @@ def _call( """ _model_kwargs = self.model_kwargs or {} params = {**_model_kwargs, **kwargs} - response = self.client(inputs=prompt, params=params) + + response = self.client.post( + json={"inputs": prompt, "params": params}, task=self.task + ) + response = json.loads(response.decode()) if "error" in response: raise ValueError(f"Error raised by inference API: {response['error']}") - if self.client.task == "text-generation": - # Text generation return includes the starter text. - text = response[0]["generated_text"][len(prompt) :] - elif self.client.task == "text2text-generation": - text = response[0]["generated_text"] - elif self.client.task == "summarization": - text = response[0]["summary_text"] + + response_key = VALID_TASKS_DICT[self.task] # type: ignore + if isinstance(response, list): + text = response[0][response_key] else: - raise ValueError( - f"Got invalid task {self.client.task}, " - f"currently only {VALID_TASKS} are supported" - ) + text = response[response_key] + if stop is not None: # This is a bit hacky, but I can't figure out a better way to enforce # stop tokens when making calls to huggingface_hub. diff --git a/libs/community/langchain_community/llms/konko.py b/libs/community/langchain_community/llms/konko.py new file mode 100644 index 0000000000000..7bcd471d4e0d7 --- /dev/null +++ b/libs/community/langchain_community/llms/konko.py @@ -0,0 +1,200 @@ +"""Wrapper around Konko AI's Completion API.""" +import logging +import warnings +from typing import Any, Dict, List, Optional + +from langchain_core.callbacks import ( + AsyncCallbackManagerForLLMRun, + CallbackManagerForLLMRun, +) +from langchain_core.language_models.llms import LLM +from langchain_core.pydantic_v1 import Extra, SecretStr, root_validator + +from langchain_community.utils.openai import is_openai_v1 + +logger = logging.getLogger(__name__) + + +class Konko(LLM): + """Wrapper around Konko AI models. + + To use, you'll need an API key. This can be passed in as init param + ``konko_api_key`` or set as environment variable ``KONKO_API_KEY``. + + Konko AI API reference: https://docs.konko.ai/reference/ + """ + + base_url: str = "https://api.konko.ai/v1/completions" + """Base inference API URL.""" + konko_api_key: SecretStr + """Konko AI API key.""" + model: str + """Model name. Available models listed here: + https://docs.konko.ai/reference/get_models + """ + temperature: Optional[float] = None + """Model temperature.""" + top_p: Optional[float] = None + """Used to dynamically adjust the number of choices for each predicted token based + on the cumulative probabilities. A value of 1 will always yield the same + output. A temperature less than 1 favors more correctness and is appropriate + for question answering or summarization. A value greater than 1 introduces more + randomness in the output. + """ + top_k: Optional[int] = None + """Used to limit the number of choices for the next predicted word or token. It + specifies the maximum number of tokens to consider at each step, based on their + probability of occurrence. This technique helps to speed up the generation + process and can improve the quality of the generated text by focusing on the + most likely options. + """ + max_tokens: Optional[int] = None + """The maximum number of tokens to generate.""" + repetition_penalty: Optional[float] = None + """A number that controls the diversity of generated text by reducing the + likelihood of repeated sequences. Higher values decrease repetition. + """ + logprobs: Optional[int] = None + """An integer that specifies how many top token log probabilities are included in + the response for each token generation step. + """ + + class Config: + """Configuration for this pydantic object.""" + + extra = Extra.forbid + + @root_validator(pre=True) + def validate_environment(cls, values: Dict[str, Any]) -> Dict[str, Any]: + """Validate that python package exists in environment.""" + try: + import konko + + except ImportError: + raise ValueError( + "Could not import konko python package. " + "Please install it with `pip install konko`." + ) + if not hasattr(konko, "_is_legacy_openai"): + warnings.warn( + "You are using an older version of the 'konko' package. " + "Please consider upgrading to access new features" + "including the completion endpoint." + ) + return values + + def construct_payload( + self, + prompt: str, + stop: Optional[List[str]] = None, + **kwargs: Any, + ) -> Dict[str, Any]: + stop_to_use = stop[0] if stop and len(stop) == 1 else stop + payload: Dict[str, Any] = { + **self.default_params, + "prompt": prompt, + "stop": stop_to_use, + **kwargs, + } + return {k: v for k, v in payload.items() if v is not None} + + @property + def _llm_type(self) -> str: + """Return type of model.""" + return "konko" + + @staticmethod + def get_user_agent() -> str: + from langchain_community import __version__ + + return f"langchain/{__version__}" + + @property + def default_params(self) -> Dict[str, Any]: + return { + "model": self.model, + "temperature": self.temperature, + "top_p": self.top_p, + "top_k": self.top_k, + "max_tokens": self.max_tokens, + "repetition_penalty": self.repetition_penalty, + } + + def _call( + self, + prompt: str, + stop: Optional[List[str]] = None, + run_manager: Optional[CallbackManagerForLLMRun] = None, + **kwargs: Any, + ) -> str: + """Call out to Konko's text generation endpoint. + + Args: + prompt: The prompt to pass into the model. + + Returns: + The string generated by the model.. + """ + import konko + + payload = self.construct_payload(prompt, stop, **kwargs) + + try: + if is_openai_v1(): + response = konko.completions.create(**payload) + else: + response = konko.Completion.create(**payload) + + except AttributeError: + raise ValueError( + "`konko` has no `Completion` attribute, this is likely " + "due to an old version of the konko package. Try upgrading it " + "with `pip install --upgrade konko`." + ) + + if is_openai_v1(): + output = response.choices[0].text + else: + output = response["choices"][0]["text"] + + return output + + async def _acall( + self, + prompt: str, + stop: Optional[List[str]] = None, + run_manager: Optional[AsyncCallbackManagerForLLMRun] = None, + **kwargs: Any, + ) -> str: + """Asynchronously call out to Konko's text generation endpoint. + + Args: + prompt: The prompt to pass into the model. + + Returns: + The string generated by the model. + """ + import konko + + payload = self.construct_payload(prompt, stop, **kwargs) + + try: + if is_openai_v1(): + client = konko.AsyncKonko() + response = await client.completions.create(**payload) + else: + response = await konko.Completion.acreate(**payload) + + except AttributeError: + raise ValueError( + "`konko` has no `Completion` attribute, this is likely " + "due to an old version of the konko package. Try upgrading it " + "with `pip install --upgrade konko`." + ) + + if is_openai_v1(): + output = response.choices[0].text + else: + output = response["choices"][0]["text"] + + return output diff --git a/libs/community/langchain_community/llms/nlpcloud.py b/libs/community/langchain_community/llms/nlpcloud.py index bdff6404290eb..a73087918ce78 100644 --- a/libs/community/langchain_community/llms/nlpcloud.py +++ b/libs/community/langchain_community/llms/nlpcloud.py @@ -38,7 +38,7 @@ class NLPCloud(LLM): """Whether or not to remove the end sequence token.""" bad_words: List[str] = [] """List of tokens not allowed to be generated.""" - top_p: int = 1 + top_p: float = 1.0 """Total probability mass of tokens to consider at each step.""" top_k: int = 50 """The number of highest probability tokens to keep for top-k filtering.""" diff --git a/libs/community/langchain_community/llms/ollama.py b/libs/community/langchain_community/llms/ollama.py index 4e7f1838f75d6..db8d66170483f 100644 --- a/libs/community/langchain_community/llms/ollama.py +++ b/libs/community/langchain_community/llms/ollama.py @@ -90,7 +90,7 @@ class _OllamaCommon(BaseLanguageModel): will give more diverse answers, while a lower value (e.g. 10) will be more conservative. (Default: 40)""" - top_p: Optional[int] = None + top_p: Optional[float] = None """Works together with top-k. A higher value (e.g., 0.95) will lead to more diverse text, while a lower value (e.g., 0.5) will generate more focused and conservative text. (Default: 0.9)""" @@ -190,8 +190,9 @@ def _create_stream( params = self._default_params - if "model" in kwargs: - params["model"] = kwargs["model"] + for key in self._default_params: + if key in kwargs: + params[key] = kwargs[key] if "options" in kwargs: params["options"] = kwargs["options"] @@ -199,7 +200,7 @@ def _create_stream( params["options"] = { **params["options"], "stop": stop, - **kwargs, + **{k: v for k, v in kwargs.items() if k not in self._default_params}, } if payload.get("messages"): @@ -253,8 +254,9 @@ async def _acreate_stream( params = self._default_params - if "model" in kwargs: - params["model"] = kwargs["model"] + for key in self._default_params: + if key in kwargs: + params[key] = kwargs[key] if "options" in kwargs: params["options"] = kwargs["options"] @@ -262,7 +264,7 @@ async def _acreate_stream( params["options"] = { **params["options"], "stop": stop, - **kwargs, + **{k: v for k, v in kwargs.items() if k not in self._default_params}, } if payload.get("messages"): @@ -466,7 +468,7 @@ async def _astream( run_manager: Optional[AsyncCallbackManagerForLLMRun] = None, **kwargs: Any, ) -> AsyncIterator[GenerationChunk]: - async for stream_resp in self._acreate_stream(prompt, stop, **kwargs): + async for stream_resp in self._acreate_generate_stream(prompt, stop, **kwargs): if stream_resp: chunk = _stream_response_to_generation_chunk(stream_resp) yield chunk diff --git a/libs/community/langchain_community/llms/watsonxllm.py b/libs/community/langchain_community/llms/watsonxllm.py index 380628f5ef31c..d60c728460059 100644 --- a/libs/community/langchain_community/llms/watsonxllm.py +++ b/libs/community/langchain_community/llms/watsonxllm.py @@ -249,6 +249,12 @@ def get_count_value(key: str, result: Dict[str, Any]) -> int: "input_token_count": input_token_count, } + def _get_chat_params(self, stop: Optional[List[str]] = None) -> Dict[str, Any]: + params: Dict[str, Any] = {**self.params} if self.params else None + if stop is not None: + params = (params or {}) | {"stop_sequences": stop} + return params + def _create_llm_result(self, response: List[dict]) -> LLMResult: """Create the LLMResult from the choices and prompts.""" generations = [] @@ -334,11 +340,7 @@ def _generate( response = watsonx_llm.generate(["What is a molecule"]) """ - if stop: - if self.params: - self.params.update({"stop_sequences": stop}) - else: - self.params = {"stop_sequences": stop} + params = self._get_chat_params(stop=stop) should_stream = stream if stream is not None else self.streaming if should_stream: if len(prompts) > 1: @@ -360,7 +362,7 @@ def _generate( return LLMResult(generations=[[generation]], llm_output=llm_output) return LLMResult(generations=[[generation]]) else: - response = self.watsonx_model.generate(prompt=prompts, params=self.params) + response = self.watsonx_model.generate(prompt=prompts, params=params) return self._create_llm_result(response) def _stream( @@ -384,13 +386,9 @@ def _stream( for chunk in response: print(chunk, end='') """ - if stop: - if self.params: - self.params.update({"stop_sequences": stop}) - else: - self.params = {"stop_sequences": stop} + params = self._get_chat_params(stop=stop) for stream_resp in self.watsonx_model.generate_text_stream( - prompt=prompt, raw_response=True, params=self.params + prompt=prompt, raw_response=True, params=params ): chunk = self._stream_response_to_generation_chunk(stream_resp) yield chunk diff --git a/libs/community/langchain_community/llms/yandex.py b/libs/community/langchain_community/llms/yandex.py index c07efe6831046..c96570357ce16 100644 --- a/libs/community/langchain_community/llms/yandex.py +++ b/libs/community/langchain_community/llms/yandex.py @@ -9,8 +9,8 @@ ) from langchain_core.language_models.llms import LLM from langchain_core.load.serializable import Serializable -from langchain_core.pydantic_v1 import root_validator -from langchain_core.utils import get_from_dict_or_env +from langchain_core.pydantic_v1 import SecretStr, root_validator +from langchain_core.utils import convert_to_secret_str, get_from_dict_or_env from tenacity import ( before_sleep_log, retry, @@ -25,10 +25,10 @@ class _BaseYandexGPT(Serializable): - iam_token: str = "" + iam_token: SecretStr = "" """Yandex Cloud IAM token for service or user account with the `ai.languageModels.user` role""" - api_key: str = "" + api_key: SecretStr = "" """Yandex Cloud Api Key for service account with the `ai.languageModels.user` role""" folder_id: str = "" @@ -72,24 +72,28 @@ def _identifying_params(self) -> Mapping[str, Any]: def validate_environment(cls, values: Dict) -> Dict: """Validate that iam token exists in environment.""" - iam_token = get_from_dict_or_env(values, "iam_token", "YC_IAM_TOKEN", "") + iam_token = convert_to_secret_str( + get_from_dict_or_env(values, "iam_token", "YC_IAM_TOKEN", "") + ) values["iam_token"] = iam_token - api_key = get_from_dict_or_env(values, "api_key", "YC_API_KEY", "") + api_key = convert_to_secret_str( + get_from_dict_or_env(values, "api_key", "YC_API_KEY", "") + ) values["api_key"] = api_key folder_id = get_from_dict_or_env(values, "folder_id", "YC_FOLDER_ID", "") values["folder_id"] = folder_id - if api_key == "" and iam_token == "": + if api_key.get_secret_value() == "" and iam_token.get_secret_value() == "": raise ValueError("Either 'YC_API_KEY' or 'YC_IAM_TOKEN' must be provided.") if values["iam_token"]: values["_grpc_metadata"] = [ - ("authorization", f"Bearer {values['iam_token']}") + ("authorization", f"Bearer {values['iam_token'].get_secret_value()}") ] if values["folder_id"]: values["_grpc_metadata"].append(("x-folder-id", values["folder_id"])) else: values["_grpc_metadata"] = ( - ("authorization", f"Api-Key {values['api_key']}"), + ("authorization", f"Api-Key {values['api_key'].get_secret_value()}"), ) if values["model_uri"] == "" and values["folder_id"] == "": raise ValueError("Either 'model_uri' or 'folder_id' must be provided.") diff --git a/libs/community/langchain_community/output_parsers/__init__.py b/libs/community/langchain_community/output_parsers/__init__.py new file mode 100644 index 0000000000000..62740af544177 --- /dev/null +++ b/libs/community/langchain_community/output_parsers/__init__.py @@ -0,0 +1,14 @@ +"""**OutputParser** classes parse the output of an LLM call. + +**Class hierarchy:** + +.. code-block:: + + BaseLLMOutputParser --> BaseOutputParser --> OutputParser # GuardrailsOutputParser + +**Main helpers:** + +.. code-block:: + + Serializable, Generation, PromptValue +""" # noqa: E501 diff --git a/libs/community/langchain_community/output_parsers/ernie_functions.py b/libs/community/langchain_community/output_parsers/ernie_functions.py new file mode 100644 index 0000000000000..223284649f3b4 --- /dev/null +++ b/libs/community/langchain_community/output_parsers/ernie_functions.py @@ -0,0 +1,179 @@ +import copy +import json +from typing import Any, Dict, List, Optional, Type, Union + +import jsonpatch +from langchain_core.exceptions import OutputParserException +from langchain_core.output_parsers import ( + BaseCumulativeTransformOutputParser, + BaseGenerationOutputParser, +) +from langchain_core.output_parsers.json import parse_partial_json +from langchain_core.outputs.chat_generation import ( + ChatGeneration, + Generation, +) +from langchain_core.pydantic_v1 import BaseModel, root_validator + + +class OutputFunctionsParser(BaseGenerationOutputParser[Any]): + """Parse an output that is one of sets of values.""" + + args_only: bool = True + """Whether to only return the arguments to the function call.""" + + def parse_result(self, result: List[Generation], *, partial: bool = False) -> Any: + generation = result[0] + if not isinstance(generation, ChatGeneration): + raise OutputParserException( + "This output parser can only be used with a chat generation." + ) + message = generation.message + try: + func_call = copy.deepcopy(message.additional_kwargs["function_call"]) + except KeyError as exc: + raise OutputParserException(f"Could not parse function call: {exc}") + + if self.args_only: + return func_call["arguments"] + return func_call + + +class JsonOutputFunctionsParser(BaseCumulativeTransformOutputParser[Any]): + """Parse an output as the Json object.""" + + strict: bool = False + """Whether to allow non-JSON-compliant strings. + + See: https://docs.python.org/3/library/json.html#encoders-and-decoders + + Useful when the parsed output may include unicode characters or new lines. + """ + + args_only: bool = True + """Whether to only return the arguments to the function call.""" + + @property + def _type(self) -> str: + return "json_functions" + + def _diff(self, prev: Optional[Any], next: Any) -> Any: + return jsonpatch.make_patch(prev, next).patch + + def parse_result(self, result: List[Generation], *, partial: bool = False) -> Any: + if len(result) != 1: + raise OutputParserException( + f"Expected exactly one result, but got {len(result)}" + ) + generation = result[0] + if not isinstance(generation, ChatGeneration): + raise OutputParserException( + "This output parser can only be used with a chat generation." + ) + message = generation.message + if "function_call" not in message.additional_kwargs: + return None + try: + function_call = message.additional_kwargs["function_call"] + except KeyError as exc: + if partial: + return None + else: + raise OutputParserException(f"Could not parse function call: {exc}") + try: + if partial: + if self.args_only: + return parse_partial_json( + function_call["arguments"], strict=self.strict + ) + else: + return { + **function_call, + "arguments": parse_partial_json( + function_call["arguments"], strict=self.strict + ), + } + else: + if self.args_only: + try: + return json.loads( + function_call["arguments"], strict=self.strict + ) + except (json.JSONDecodeError, TypeError) as exc: + raise OutputParserException( + f"Could not parse function call data: {exc}" + ) + else: + try: + return { + **function_call, + "arguments": json.loads( + function_call["arguments"], strict=self.strict + ), + } + except (json.JSONDecodeError, TypeError) as exc: + raise OutputParserException( + f"Could not parse function call data: {exc}" + ) + except KeyError: + return None + + # This method would be called by the default implementation of `parse_result` + # but we're overriding that method so it's not needed. + def parse(self, text: str) -> Any: + raise NotImplementedError() + + +class JsonKeyOutputFunctionsParser(JsonOutputFunctionsParser): + """Parse an output as the element of the Json object.""" + + key_name: str + """The name of the key to return.""" + + def parse_result(self, result: List[Generation], *, partial: bool = False) -> Any: + res = super().parse_result(result, partial=partial) + if partial and res is None: + return None + return res.get(self.key_name) if partial else res[self.key_name] + + +class PydanticOutputFunctionsParser(OutputFunctionsParser): + """Parse an output as a pydantic object.""" + + pydantic_schema: Union[Type[BaseModel], Dict[str, Type[BaseModel]]] + """The pydantic schema to parse the output with.""" + + @root_validator(pre=True) + def validate_schema(cls, values: Dict) -> Dict: + schema = values["pydantic_schema"] + if "args_only" not in values: + values["args_only"] = isinstance(schema, type) and issubclass( + schema, BaseModel + ) + elif values["args_only"] and isinstance(schema, Dict): + raise ValueError( + "If multiple pydantic schemas are provided then args_only should be" + " False." + ) + return values + + def parse_result(self, result: List[Generation], *, partial: bool = False) -> Any: + _result = super().parse_result(result) + if self.args_only: + pydantic_args = self.pydantic_schema.parse_raw(_result) # type: ignore + else: + fn_name = _result["name"] + _args = _result["arguments"] + pydantic_args = self.pydantic_schema[fn_name].parse_raw(_args) # type: ignore # noqa: E501 + return pydantic_args + + +class PydanticAttrOutputFunctionsParser(PydanticOutputFunctionsParser): + """Parse an output as an attribute of a pydantic object.""" + + attr_name: str + """The name of the attribute to return.""" + + def parse_result(self, result: List[Generation], *, partial: bool = False) -> Any: + result = super().parse_result(result) + return getattr(result, self.attr_name) diff --git a/libs/community/langchain_community/output_parsers/rail_parser.py b/libs/community/langchain_community/output_parsers/rail_parser.py new file mode 100644 index 0000000000000..f0cabc13eb553 --- /dev/null +++ b/libs/community/langchain_community/output_parsers/rail_parser.py @@ -0,0 +1,109 @@ +from __future__ import annotations + +from typing import Any, Callable, Dict, Optional + +from langchain_core.output_parsers import BaseOutputParser + + +class GuardrailsOutputParser(BaseOutputParser): + """Parse the output of an LLM call using Guardrails.""" + + guard: Any + """The Guardrails object.""" + api: Optional[Callable] + """The LLM API passed to Guardrails during parsing. An example is `openai.completions.create`.""" # noqa: E501 + args: Any + """Positional arguments to pass to the above LLM API callable.""" + kwargs: Any + """Keyword arguments to pass to the above LLM API callable.""" + + @property + def _type(self) -> str: + return "guardrails" + + @classmethod + def from_rail( + cls, + rail_file: str, + num_reasks: int = 1, + api: Optional[Callable] = None, + *args: Any, + **kwargs: Any, + ) -> GuardrailsOutputParser: + """Create a GuardrailsOutputParser from a rail file. + + Args: + rail_file: a rail file. + num_reasks: number of times to re-ask the question. + api: the API to use for the Guardrails object. + *args: The arguments to pass to the API + **kwargs: The keyword arguments to pass to the API. + + Returns: + GuardrailsOutputParser + """ + try: + from guardrails import Guard + except ImportError: + raise ImportError( + "guardrails-ai package not installed. " + "Install it by running `pip install guardrails-ai`." + ) + return cls( + guard=Guard.from_rail(rail_file, num_reasks=num_reasks), + api=api, + args=args, + kwargs=kwargs, + ) + + @classmethod + def from_rail_string( + cls, + rail_str: str, + num_reasks: int = 1, + api: Optional[Callable] = None, + *args: Any, + **kwargs: Any, + ) -> GuardrailsOutputParser: + try: + from guardrails import Guard + except ImportError: + raise ImportError( + "guardrails-ai package not installed. " + "Install it by running `pip install guardrails-ai`." + ) + return cls( + guard=Guard.from_rail_string(rail_str, num_reasks=num_reasks), + api=api, + args=args, + kwargs=kwargs, + ) + + @classmethod + def from_pydantic( + cls, + output_class: Any, + num_reasks: int = 1, + api: Optional[Callable] = None, + *args: Any, + **kwargs: Any, + ) -> GuardrailsOutputParser: + try: + from guardrails import Guard + except ImportError: + raise ImportError( + "guardrails-ai package not installed. " + "Install it by running `pip install guardrails-ai`." + ) + return cls( + guard=Guard.from_pydantic(output_class, "", num_reasks=num_reasks), + api=api, + args=args, + kwargs=kwargs, + ) + + def get_format_instructions(self) -> str: + return self.guard.raw_prompt.format_instructions + + def parse(self, text: str) -> Dict: + return self.guard.parse(text, llm_api=self.api, *self.args, **self.kwargs) diff --git a/libs/community/langchain_community/retrievers/bm25.py b/libs/community/langchain_community/retrievers/bm25.py index c0e0b248313fa..0ebaa2c0cd285 100644 --- a/libs/community/langchain_community/retrievers/bm25.py +++ b/libs/community/langchain_community/retrievers/bm25.py @@ -4,6 +4,7 @@ from langchain_core.callbacks import CallbackManagerForRetrieverRun from langchain_core.documents import Document +from langchain_core.pydantic_v1 import Field from langchain_core.retrievers import BaseRetriever @@ -16,7 +17,7 @@ class BM25Retriever(BaseRetriever): vectorizer: Any """ BM25 vectorizer.""" - docs: List[Document] + docs: List[Document] = Field(repr=False) """ List of documents.""" k: int = 4 """ Number of documents to return.""" diff --git a/libs/community/langchain_community/retrievers/cohere_rag_retriever.py b/libs/community/langchain_community/retrievers/cohere_rag_retriever.py index 39dcc30f3b451..91f4c3a0886f5 100644 --- a/libs/community/langchain_community/retrievers/cohere_rag_retriever.py +++ b/libs/community/langchain_community/retrievers/cohere_rag_retriever.py @@ -17,10 +17,14 @@ def _get_docs(response: Any) -> List[Document]: - docs = [ - Document(page_content=doc["snippet"], metadata=doc) - for doc in response.generation_info["documents"] - ] + docs = ( + [] + if "documents" not in response.generation_info + else [ + Document(page_content=doc["snippet"], metadata=doc) + for doc in response.generation_info["documents"] + ] + ) docs.append( Document( page_content=response.message.content, diff --git a/libs/community/langchain_community/storage/__init__.py b/libs/community/langchain_community/storage/__init__.py index 494591b03c713..5c28015e57437 100644 --- a/libs/community/langchain_community/storage/__init__.py +++ b/libs/community/langchain_community/storage/__init__.py @@ -11,6 +11,10 @@ AstraDBStore, ) from langchain_community.storage.redis import RedisStore +from langchain_community.storage.sql import ( + SQLDocStore, + SQLStrStore, +) from langchain_community.storage.upstash_redis import ( UpstashRedisByteStore, UpstashRedisStore, @@ -22,4 +26,6 @@ "RedisStore", "UpstashRedisByteStore", "UpstashRedisStore", + "SQLDocStore", + "SQLStrStore", ] diff --git a/libs/community/langchain_community/storage/sql.py b/libs/community/langchain_community/storage/sql.py new file mode 100644 index 0000000000000..7baaf64285936 --- /dev/null +++ b/libs/community/langchain_community/storage/sql.py @@ -0,0 +1,345 @@ +"""SQL storage that persists data in a SQL database +and supports data isolation using collections.""" +from __future__ import annotations + +import uuid +from typing import Any, Generic, Iterator, List, Optional, Sequence, Tuple, TypeVar + +import sqlalchemy +from sqlalchemy import JSON, UUID +from sqlalchemy.orm import Session, relationship + +try: + from sqlalchemy.orm import declarative_base +except ImportError: + from sqlalchemy.ext.declarative import declarative_base + +from langchain_core.documents import Document +from langchain_core.load import Serializable, dumps, loads +from langchain_core.stores import BaseStore + +V = TypeVar("V") + +ITERATOR_WINDOW_SIZE = 1000 + +Base = declarative_base() # type: Any + + +_LANGCHAIN_DEFAULT_COLLECTION_NAME = "langchain" + + +class BaseModel(Base): + """Base model for the SQL stores.""" + + __abstract__ = True + uuid = sqlalchemy.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + + +_classes: Any = None + + +def _get_storage_stores() -> Any: + global _classes + if _classes is not None: + return _classes + + class CollectionStore(BaseModel): + """Collection store.""" + + __tablename__ = "langchain_storage_collection" + + name = sqlalchemy.Column(sqlalchemy.String) + cmetadata = sqlalchemy.Column(JSON) + + items = relationship( + "ItemStore", + back_populates="collection", + passive_deletes=True, + ) + + @classmethod + def get_by_name( + cls, session: Session, name: str + ) -> Optional["CollectionStore"]: + # type: ignore + return session.query(cls).filter(cls.name == name).first() + + @classmethod + def get_or_create( + cls, + session: Session, + name: str, + cmetadata: Optional[dict] = None, + ) -> Tuple["CollectionStore", bool]: + """ + Get or create a collection. + Returns [Collection, bool] where the bool is True if the collection was created. + """ # noqa: E501 + created = False + collection = cls.get_by_name(session, name) + if collection: + return collection, created + + collection = cls(name=name, cmetadata=cmetadata) + session.add(collection) + session.commit() + created = True + return collection, created + + class ItemStore(BaseModel): + """Item store.""" + + __tablename__ = "langchain_storage_items" + + collection_id = sqlalchemy.Column( + UUID(as_uuid=True), + sqlalchemy.ForeignKey( + f"{CollectionStore.__tablename__}.uuid", + ondelete="CASCADE", + ), + ) + collection = relationship(CollectionStore, back_populates="items") + + content = sqlalchemy.Column(sqlalchemy.String, nullable=True) + + # custom_id : any user defined id + custom_id = sqlalchemy.Column(sqlalchemy.String, nullable=True) + + _classes = (ItemStore, CollectionStore) + + return _classes + + +class SQLBaseStore(BaseStore[str, V], Generic[V]): + """SQL storage + + Args: + connection_string: SQL connection string that will be passed to SQLAlchemy. + collection_name: The name of the collection to use. (default: langchain) + NOTE: Collections are useful to isolate your data in a given a database. + This is not the name of the table, but the name of the collection. + The tables will be created when initializing the store (if not exists) + So, make sure the user has the right permissions to create tables. + pre_delete_collection: If True, will delete the collection if it exists. + (default: False). Useful for testing. + engine_args: SQLAlchemy's create engine arguments. + + Example: + .. code-block:: python + + from langchain_community.storage import SQLDocStore + from langchain_community.embeddings.openai import OpenAIEmbeddings + + # example using an SQLDocStore to store Document objects for + # a ParentDocumentRetriever + CONNECTION_STRING = "postgresql+psycopg2://user:pass@localhost:5432/db" + COLLECTION_NAME = "state_of_the_union_test" + docstore = SQLDocStore( + collection_name=COLLECTION_NAME, + connection_string=CONNECTION_STRING, + ) + child_splitter = RecursiveCharacterTextSplitter(chunk_size=400) + vectorstore = ... + + retriever = ParentDocumentRetriever( + vectorstore=vectorstore, + docstore=docstore, + child_splitter=child_splitter, + ) + + # example using an SQLStrStore to store strings + # same example as in "InMemoryStore" but using SQL persistence + store = SQLDocStore( + collection_name=COLLECTION_NAME, + connection_string=CONNECTION_STRING, + ) + store.mset([('key1', 'value1'), ('key2', 'value2')]) + store.mget(['key1', 'key2']) + # ['value1', 'value2'] + store.mdelete(['key1']) + list(store.yield_keys()) + # ['key2'] + list(store.yield_keys(prefix='k')) + # ['key2'] + + # delete the COLLECTION_NAME collection + docstore.delete_collection() + """ + + def __init__( + self, + connection_string: str, + collection_name: str = _LANGCHAIN_DEFAULT_COLLECTION_NAME, + collection_metadata: Optional[dict] = None, + pre_delete_collection: bool = False, + connection: Optional[sqlalchemy.engine.Connection] = None, + engine_args: Optional[dict[str, Any]] = None, + ) -> None: + self.connection_string = connection_string + self.collection_name = collection_name + self.collection_metadata = collection_metadata + self.pre_delete_collection = pre_delete_collection + self.engine_args = engine_args or {} + # Create a connection if not provided, otherwise use the provided connection + self._conn = connection if connection else self.__connect() + self.__post_init__() + + def __post_init__( + self, + ) -> None: + """Initialize the store.""" + ItemStore, CollectionStore = _get_storage_stores() + self.CollectionStore = CollectionStore + self.ItemStore = ItemStore + self.__create_tables_if_not_exists() + self.__create_collection() + + def __connect(self) -> sqlalchemy.engine.Connection: + engine = sqlalchemy.create_engine(self.connection_string, **self.engine_args) + conn = engine.connect() + return conn + + def __create_tables_if_not_exists(self) -> None: + with self._conn.begin(): + Base.metadata.create_all(self._conn) + + def __create_collection(self) -> None: + if self.pre_delete_collection: + self.delete_collection() + with Session(self._conn) as session: + self.CollectionStore.get_or_create( + session, self.collection_name, cmetadata=self.collection_metadata + ) + + def delete_collection(self) -> None: + with Session(self._conn) as session: + collection = self.__get_collection(session) + if not collection: + return + session.delete(collection) + session.commit() + + def __get_collection(self, session: Session) -> Any: + return self.CollectionStore.get_by_name(session, self.collection_name) + + def __del__(self) -> None: + if self._conn: + self._conn.close() + + def __serialize_value(self, obj: V) -> str: + if isinstance(obj, Serializable): + return dumps(obj) + return obj + + def __deserialize_value(self, obj: V) -> str: + try: + return loads(obj) + except Exception: + return obj + + def mget(self, keys: Sequence[str]) -> List[Optional[V]]: + """Get the values associated with the given keys. + + Args: + keys (Sequence[str]): A sequence of keys. + + Returns: + A sequence of optional values associated with the keys. + If a key is not found, the corresponding value will be None. + """ + with Session(self._conn) as session: + collection = self.__get_collection(session) + + items = ( + session.query(self.ItemStore.content, self.ItemStore.custom_id) + .where( + sqlalchemy.and_( + self.ItemStore.custom_id.in_(keys), + self.ItemStore.collection_id == (collection.uuid), + ) + ) + .all() + ) + + ordered_values = {key: None for key in keys} + for item in items: + v = item[0] + val = self.__deserialize_value(v) if v is not None else v + k = item[1] + ordered_values[k] = val + + return [ordered_values[key] for key in keys] + + def mset(self, key_value_pairs: Sequence[Tuple[str, V]]) -> None: + """Set the values for the given keys. + + Args: + key_value_pairs (Sequence[Tuple[str, V]]): A sequence of key-value pairs. + + Returns: + None + """ + with Session(self._conn) as session: + collection = self.__get_collection(session) + if not collection: + raise ValueError("Collection not found") + for id, item in key_value_pairs: + content = self.__serialize_value(item) + item_store = self.ItemStore( + content=content, + custom_id=id, + collection_id=collection.uuid, + ) + session.add(item_store) + session.commit() + + def mdelete(self, keys: Sequence[str]) -> None: + """Delete the given keys and their associated values. + + Args: + keys (Sequence[str]): A sequence of keys to delete. + """ + with Session(self._conn) as session: + collection = self.__get_collection(session) + if not collection: + raise ValueError("Collection not found") + if keys is not None: + stmt = sqlalchemy.delete(self.ItemStore).where( + sqlalchemy.and_( + self.ItemStore.custom_id.in_(keys), + self.ItemStore.collection_id == (collection.uuid), + ) + ) + session.execute(stmt) + session.commit() + + def yield_keys(self, prefix: Optional[str] = None) -> Iterator[str]: + """Get an iterator over keys that match the given prefix. + + Args: + prefix (str, optional): The prefix to match. Defaults to None. + + Returns: + Iterator[str]: An iterator over keys that match the given prefix. + """ + with Session(self._conn) as session: + collection = self.__get_collection(session) + start = 0 + while True: + stop = start + ITERATOR_WINDOW_SIZE + query = session.query(self.ItemStore.custom_id).where( + self.ItemStore.collection_id == (collection.uuid) + ) + if prefix is not None: + query = query.filter(self.ItemStore.custom_id.startswith(prefix)) + items = query.slice(start, stop).all() + + if len(items) == 0: + break + for item in items: + yield item[0] + start += ITERATOR_WINDOW_SIZE + + +SQLDocStore = SQLBaseStore[Document] +SQLStrStore = SQLBaseStore[str] diff --git a/libs/community/langchain_community/tools/__init__.py b/libs/community/langchain_community/tools/__init__.py index 8bce838f88129..b00e02387103d 100644 --- a/libs/community/langchain_community/tools/__init__.py +++ b/libs/community/langchain_community/tools/__init__.py @@ -472,6 +472,12 @@ def _import_plugin() -> Any: return AIPluginTool +def _import_polygon_tool_PolygonLastQuote() -> Any: + from langchain_community.tools.polygon.last_quote import PolygonLastQuote + + return PolygonLastQuote + + def _import_powerbi_tool_InfoPowerBITool() -> Any: from langchain_community.tools.powerbi.tool import InfoPowerBITool @@ -907,6 +913,8 @@ def __getattr__(name: str) -> Any: return _import_playwright_NavigateTool() elif name == "AIPluginTool": return _import_plugin() + elif name == "PolygonLastQuote": + return _import_polygon_tool_PolygonLastQuote() elif name == "InfoPowerBITool": return _import_powerbi_tool_InfoPowerBITool() elif name == "ListPowerBITool": @@ -1085,6 +1093,7 @@ def __getattr__(name: str) -> Any: "OpenAPISpec", "OpenWeatherMapQueryRun", "PubmedQueryRun", + "PolygonLastQuote", "RedditSearchRun", "QueryCheckerTool", "QueryPowerBITool", diff --git a/libs/community/langchain_community/tools/amadeus/closest_airport.py b/libs/community/langchain_community/tools/amadeus/closest_airport.py index 4107cc500ba30..cf108f3b11d99 100644 --- a/libs/community/langchain_community/tools/amadeus/closest_airport.py +++ b/libs/community/langchain_community/tools/amadeus/closest_airport.py @@ -58,4 +58,4 @@ def _run( ' Location Identifier" ' ) - return self.llm.predict(content) + return self.llm.invoke(content) diff --git a/libs/community/langchain_community/tools/bing_search/tool.py b/libs/community/langchain_community/tools/bing_search/tool.py index 027f2750c9418..658b5011ca561 100644 --- a/libs/community/langchain_community/tools/bing_search/tool.py +++ b/libs/community/langchain_community/tools/bing_search/tool.py @@ -31,7 +31,7 @@ def _run( class BingSearchResults(BaseTool): """Tool that queries the Bing Search API and gets back json.""" - name: str = "Bing Search Results JSON" + name: str = "bing_search_results_json" description: str = ( "A wrapper around Bing Search. " "Useful for when you need to answer questions about current events. " diff --git a/libs/community/langchain_community/tools/polygon/__init__.py b/libs/community/langchain_community/tools/polygon/__init__.py new file mode 100644 index 0000000000000..acc8bc4ac70c7 --- /dev/null +++ b/libs/community/langchain_community/tools/polygon/__init__.py @@ -0,0 +1,7 @@ +"""Polygon IO tools.""" + +from langchain_community.tools.polygon.last_quote import PolygonLastQuote + +__all__ = [ + "PolygonLastQuote", +] diff --git a/libs/community/langchain_community/tools/polygon/last_quote.py b/libs/community/langchain_community/tools/polygon/last_quote.py new file mode 100644 index 0000000000000..55fe3d9301020 --- /dev/null +++ b/libs/community/langchain_community/tools/polygon/last_quote.py @@ -0,0 +1,34 @@ +from typing import Optional, Type + +from langchain_core.callbacks import CallbackManagerForToolRun +from langchain_core.pydantic_v1 import BaseModel +from langchain_core.tools import BaseTool + +from langchain_community.utilities.polygon import PolygonAPIWrapper + + +class Inputs(BaseModel): + query: str + + +class PolygonLastQuote(BaseTool): + """Tool that gets the last quote of a ticker from Polygon""" + + mode: str = "get_last_quote" + name: str = "polygon_last_quote" + description: str = ( + "A wrapper around Polygon's Last Quote API. " + "This tool is useful for fetching the latest price of a stock. " + "Input should be the ticker that you want to query the last price quote for." + ) + args_schema: Type[BaseModel] = Inputs + + api_wrapper: PolygonAPIWrapper + + def _run( + self, + query: str, + run_manager: Optional[CallbackManagerForToolRun] = None, + ) -> str: + """Use the Polygon API tool.""" + return self.api_wrapper.run(self.mode, ticker=query) diff --git a/libs/community/langchain_community/tools/requests/tool.py b/libs/community/langchain_community/tools/requests/tool.py index ae2a2ca9546d9..17985d5aa3a25 100644 --- a/libs/community/langchain_community/tools/requests/tool.py +++ b/libs/community/langchain_community/tools/requests/tool.py @@ -1,7 +1,7 @@ # flake8: noqa """Tools for making requests to an API endpoint.""" import json -from typing import Any, Dict, Optional +from typing import Any, Dict, Optional, Union from langchain_core.pydantic_v1 import BaseModel from langchain_core.callbacks import ( @@ -9,7 +9,7 @@ CallbackManagerForToolRun, ) -from langchain_community.utilities.requests import TextRequestsWrapper +from langchain_community.utilities.requests import GenericRequestsWrapper from langchain_core.tools import BaseTool @@ -26,7 +26,7 @@ def _clean_url(url: str) -> str: class BaseRequestsTool(BaseModel): """Base class for requests tools.""" - requests_wrapper: TextRequestsWrapper + requests_wrapper: GenericRequestsWrapper class RequestsGetTool(BaseRequestsTool, BaseTool): @@ -37,7 +37,7 @@ class RequestsGetTool(BaseRequestsTool, BaseTool): def _run( self, url: str, run_manager: Optional[CallbackManagerForToolRun] = None - ) -> str: + ) -> Union[str, Dict[str, Any]]: """Run the tool.""" return self.requests_wrapper.get(_clean_url(url)) @@ -45,7 +45,7 @@ async def _arun( self, url: str, run_manager: Optional[AsyncCallbackManagerForToolRun] = None, - ) -> str: + ) -> Union[str, Dict[str, Any]]: """Run the tool asynchronously.""" return await self.requests_wrapper.aget(_clean_url(url)) @@ -64,7 +64,7 @@ class RequestsPostTool(BaseRequestsTool, BaseTool): def _run( self, text: str, run_manager: Optional[CallbackManagerForToolRun] = None - ) -> str: + ) -> Union[str, Dict[str, Any]]: """Run the tool.""" try: data = _parse_input(text) @@ -76,7 +76,7 @@ async def _arun( self, text: str, run_manager: Optional[AsyncCallbackManagerForToolRun] = None, - ) -> str: + ) -> Union[str, Dict[str, Any]]: """Run the tool asynchronously.""" try: data = _parse_input(text) @@ -101,7 +101,7 @@ class RequestsPatchTool(BaseRequestsTool, BaseTool): def _run( self, text: str, run_manager: Optional[CallbackManagerForToolRun] = None - ) -> str: + ) -> Union[str, Dict[str, Any]]: """Run the tool.""" try: data = _parse_input(text) @@ -113,7 +113,7 @@ async def _arun( self, text: str, run_manager: Optional[AsyncCallbackManagerForToolRun] = None, - ) -> str: + ) -> Union[str, Dict[str, Any]]: """Run the tool asynchronously.""" try: data = _parse_input(text) @@ -138,7 +138,7 @@ class RequestsPutTool(BaseRequestsTool, BaseTool): def _run( self, text: str, run_manager: Optional[CallbackManagerForToolRun] = None - ) -> str: + ) -> Union[str, Dict[str, Any]]: """Run the tool.""" try: data = _parse_input(text) @@ -150,7 +150,7 @@ async def _arun( self, text: str, run_manager: Optional[AsyncCallbackManagerForToolRun] = None, - ) -> str: + ) -> Union[str, Dict[str, Any]]: """Run the tool asynchronously.""" try: data = _parse_input(text) @@ -171,7 +171,7 @@ def _run( self, url: str, run_manager: Optional[CallbackManagerForToolRun] = None, - ) -> str: + ) -> Union[str, Dict[str, Any]]: """Run the tool.""" return self.requests_wrapper.delete(_clean_url(url)) @@ -179,6 +179,6 @@ async def _arun( self, url: str, run_manager: Optional[AsyncCallbackManagerForToolRun] = None, - ) -> str: + ) -> Union[str, Dict[str, Any]]: """Run the tool asynchronously.""" return await self.requests_wrapper.adelete(_clean_url(url)) diff --git a/libs/community/langchain_community/tools/shell/tool.py b/libs/community/langchain_community/tools/shell/tool.py index 5f61631059d92..e26deb365dcfc 100644 --- a/libs/community/langchain_community/tools/shell/tool.py +++ b/libs/community/langchain_community/tools/shell/tool.py @@ -1,3 +1,4 @@ +import logging import platform import warnings from typing import Any, List, Optional, Type, Union @@ -8,6 +9,8 @@ from langchain_core.pydantic_v1 import BaseModel, Field, root_validator from langchain_core.tools import BaseTool +logger = logging.getLogger(__name__) + class ShellInput(BaseModel): """Commands for the Bash Shell tool.""" @@ -68,10 +71,32 @@ class ShellTool(BaseTool): args_schema: Type[BaseModel] = ShellInput """Schema for input arguments.""" + ask_human_input: bool = False + """ + If True, prompts the user for confirmation (y/n) before executing + a command generated by the language model in the bash shell. + """ + def _run( self, commands: Union[str, List[str]], run_manager: Optional[CallbackManagerForToolRun] = None, ) -> str: """Run commands and return final output.""" - return self.process.run(commands) + + print(f"Executing command:\n {commands}") + + try: + if self.ask_human_input: + user_input = input("Proceed with command execution? (y/n): ").lower() + if user_input == "y": + return self.process.run(commands) + else: + logger.info("Invalid input. User aborted command execution.") + return None + else: + return self.process.run(commands) + + except Exception as e: + logger.error(f"Error during command execution: {e}") + return None diff --git a/libs/community/langchain_community/tools/slack/get_channel.py b/libs/community/langchain_community/tools/slack/get_channel.py index 0ed5d817a20ec..a3dbe13939a19 100644 --- a/libs/community/langchain_community/tools/slack/get_channel.py +++ b/libs/community/langchain_community/tools/slack/get_channel.py @@ -1,6 +1,6 @@ import json import logging -from typing import Optional +from typing import Any, Optional from langchain_core.callbacks import CallbackManagerForToolRun @@ -11,11 +11,12 @@ class SlackGetChannel(SlackBaseTool): """Tool that gets Slack channel information.""" name: str = "get_channelid_name_dict" - description: str = "Use this tool to get channelid-name dict." + description: str = ( + "Use this tool to get channelid-name dict. There is no input to this tool" + ) def _run( - self, - run_manager: Optional[CallbackManagerForToolRun] = None, + self, *args: Any, run_manager: Optional[CallbackManagerForToolRun] = None ) -> str: try: logging.getLogger(__name__) diff --git a/libs/community/langchain_community/utilities/google_trends.py b/libs/community/langchain_community/utilities/google_trends.py index f0f15000c8a1c..59df34485606f 100644 --- a/libs/community/langchain_community/utilities/google_trends.py +++ b/libs/community/langchain_community/utilities/google_trends.py @@ -65,7 +65,12 @@ def run(self, query: str) -> str: total_results = [] client = self.serp_search_engine(params) - total_results = client.get_dict()["interest_over_time"]["timeline_data"] + client_dict = client.get_dict() + total_results = ( + client_dict["interest_over_time"]["timeline_data"] + if "interest_over_time" in client_dict + else None + ) if not total_results: return "No good Trend Result was found" diff --git a/libs/community/langchain_community/utilities/requests.py b/libs/community/langchain_community/utilities/requests.py index 651616ff53183..673df3d5e57fe 100644 --- a/libs/community/langchain_community/utilities/requests.py +++ b/libs/community/langchain_community/utilities/requests.py @@ -1,10 +1,11 @@ """Lightweight wrapper around requests library, with async support.""" from contextlib import asynccontextmanager -from typing import Any, AsyncGenerator, Dict, Optional +from typing import Any, AsyncGenerator, Dict, Literal, Optional, Union import aiohttp import requests from langchain_core.pydantic_v1 import BaseModel, Extra +from requests import Response class Requests(BaseModel): @@ -108,15 +109,13 @@ async def adelete( yield response -class TextRequestsWrapper(BaseModel): - """Lightweight wrapper around requests library. - - The main purpose of this wrapper is to always return a text output. - """ +class GenericRequestsWrapper(BaseModel): + """Lightweight wrapper around requests library.""" headers: Optional[Dict[str, str]] = None aiosession: Optional[aiohttp.ClientSession] = None auth: Optional[Any] = None + response_content_type: Literal["text", "json"] = "text" class Config: """Configuration for this pydantic object.""" @@ -130,50 +129,96 @@ def requests(self) -> Requests: headers=self.headers, aiosession=self.aiosession, auth=self.auth ) - def get(self, url: str, **kwargs: Any) -> str: + def _get_resp_content(self, response: Response) -> Union[str, Dict[str, Any]]: + if self.response_content_type == "text": + return response.text + elif self.response_content_type == "json": + return response.json() + else: + raise ValueError(f"Invalid return type: {self.response_content_type}") + + def _aget_resp_content( + self, response: aiohttp.ClientResponse + ) -> Union[str, Dict[str, Any]]: + if self.response_content_type == "text": + return response.text() + elif self.response_content_type == "json": + return response.json() + else: + raise ValueError(f"Invalid return type: {self.response_content_type}") + + def get(self, url: str, **kwargs: Any) -> Union[str, Dict[str, Any]]: """GET the URL and return the text.""" - return self.requests.get(url, **kwargs).text + return self._get_resp_content(self.requests.get(url, **kwargs)) - def post(self, url: str, data: Dict[str, Any], **kwargs: Any) -> str: + def post( + self, url: str, data: Dict[str, Any], **kwargs: Any + ) -> Union[str, Dict[str, Any]]: """POST to the URL and return the text.""" - return self.requests.post(url, data, **kwargs).text + return self._get_resp_content(self.requests.post(url, data, **kwargs)) - def patch(self, url: str, data: Dict[str, Any], **kwargs: Any) -> str: + def patch( + self, url: str, data: Dict[str, Any], **kwargs: Any + ) -> Union[str, Dict[str, Any]]: """PATCH the URL and return the text.""" - return self.requests.patch(url, data, **kwargs).text + return self._get_resp_content(self.requests.patch(url, data, **kwargs)) - def put(self, url: str, data: Dict[str, Any], **kwargs: Any) -> str: + def put( + self, url: str, data: Dict[str, Any], **kwargs: Any + ) -> Union[str, Dict[str, Any]]: """PUT the URL and return the text.""" - return self.requests.put(url, data, **kwargs).text + return self._get_resp_content(self.requests.put(url, data, **kwargs)) - def delete(self, url: str, **kwargs: Any) -> str: + def delete(self, url: str, **kwargs: Any) -> Union[str, Dict[str, Any]]: """DELETE the URL and return the text.""" - return self.requests.delete(url, **kwargs).text + return self._get_resp_content(self.requests.delete(url, **kwargs)) - async def aget(self, url: str, **kwargs: Any) -> str: + async def aget(self, url: str, **kwargs: Any) -> Union[str, Dict[str, Any]]: """GET the URL and return the text asynchronously.""" async with self.requests.aget(url, **kwargs) as response: - return await response.text() + return await self._aget_resp_content(response) - async def apost(self, url: str, data: Dict[str, Any], **kwargs: Any) -> str: + async def apost( + self, url: str, data: Dict[str, Any], **kwargs: Any + ) -> Union[str, Dict[str, Any]]: """POST to the URL and return the text asynchronously.""" async with self.requests.apost(url, data, **kwargs) as response: - return await response.text() + return await self._aget_resp_content(response) - async def apatch(self, url: str, data: Dict[str, Any], **kwargs: Any) -> str: + async def apatch( + self, url: str, data: Dict[str, Any], **kwargs: Any + ) -> Union[str, Dict[str, Any]]: """PATCH the URL and return the text asynchronously.""" async with self.requests.apatch(url, data, **kwargs) as response: - return await response.text() + return await self._aget_resp_content(response) - async def aput(self, url: str, data: Dict[str, Any], **kwargs: Any) -> str: + async def aput( + self, url: str, data: Dict[str, Any], **kwargs: Any + ) -> Union[str, Dict[str, Any]]: """PUT the URL and return the text asynchronously.""" async with self.requests.aput(url, data, **kwargs) as response: - return await response.text() + return await self._aget_resp_content(response) - async def adelete(self, url: str, **kwargs: Any) -> str: + async def adelete(self, url: str, **kwargs: Any) -> Union[str, Dict[str, Any]]: """DELETE the URL and return the text asynchronously.""" async with self.requests.adelete(url, **kwargs) as response: - return await response.text() + return await self._aget_resp_content(response) + + +class JsonRequestsWrapper(GenericRequestsWrapper): + """Lightweight wrapper around requests library, with async support. + + The main purpose of this wrapper is to always return a json output.""" + + response_content_type: Literal["text", "json"] = "json" + + +class TextRequestsWrapper(GenericRequestsWrapper): + """Lightweight wrapper around requests library, with async support. + + The main purpose of this wrapper is to always return a text output.""" + + response_content_type: Literal["text", "json"] = "text" # For backwards compatibility diff --git a/libs/community/langchain_community/utilities/sql_database.py b/libs/community/langchain_community/utilities/sql_database.py index 9f665b7132d82..e56423733cb29 100644 --- a/libs/community/langchain_community/utilities/sql_database.py +++ b/libs/community/langchain_community/utilities/sql_database.py @@ -1,10 +1,10 @@ """SQLAlchemy wrapper around a database.""" from __future__ import annotations -import warnings from typing import Any, Dict, Iterable, List, Literal, Optional, Sequence import sqlalchemy +from langchain_core._api import deprecated from langchain_core.utils import get_from_env from sqlalchemy import MetaData, Table, create_engine, inspect, select, text from sqlalchemy.engine import Engine @@ -272,11 +272,9 @@ def get_usable_table_names(self) -> Iterable[str]: return sorted(self._include_tables) return sorted(self._all_tables - self._ignore_tables) + @deprecated("0.0.1", alternative="get_usable_table_name", removal="0.2.0") def get_table_names(self) -> Iterable[str]: """Get names of tables available.""" - warnings.warn( - "This method is deprecated - please use `get_usable_table_names`." - ) return self.get_usable_table_names() @property @@ -409,8 +407,9 @@ def _execute( # If anybody using Sybase SQL anywhere database then it should not # go to else condition. It should be same as mssql. pass - else: # postgresql and other compatible dialects + elif self.dialect == "postgresql": # postgresql connection.exec_driver_sql("SET search_path TO %s", (self._schema,)) + cursor = connection.execute(text(command)) if cursor.returns_rows: if fetch == "all": @@ -486,3 +485,9 @@ def run_no_throw( except SQLAlchemyError as e: """Format the error message""" return f"Error: {e}" + + def get_context(self) -> Dict[str, Any]: + """Return db context that you may want in agent prompt.""" + table_names = list(self.get_usable_table_names()) + table_info = self.get_table_info_no_throw() + return {"table_info": table_info, "table_names": ", ".join(table_names)} diff --git a/libs/community/langchain_community/utilities/tavily_search.py b/libs/community/langchain_community/utilities/tavily_search.py index 54cd0810cc265..97dc45363f2ee 100644 --- a/libs/community/langchain_community/utilities/tavily_search.py +++ b/libs/community/langchain_community/utilities/tavily_search.py @@ -7,7 +7,7 @@ import aiohttp import requests -from langchain_core.pydantic_v1 import BaseModel, Extra, root_validator +from langchain_core.pydantic_v1 import BaseModel, Extra, SecretStr, root_validator from langchain_core.utils import get_from_dict_or_env TAVILY_API_URL = "https://api.tavily.com" @@ -16,7 +16,7 @@ class TavilySearchAPIWrapper(BaseModel): """Wrapper for Tavily Search API.""" - tavily_api_key: str + tavily_api_key: SecretStr class Config: """Configuration for this pydantic object.""" @@ -45,7 +45,7 @@ def raw_results( include_images: Optional[bool] = False, ) -> Dict: params = { - "api_key": self.tavily_api_key, + "api_key": self.tavily_api_key.get_secret_value(), "query": query, "max_results": max_results, "search_depth": search_depth, @@ -126,7 +126,7 @@ async def raw_results_async( # Function to perform the API call async def fetch() -> str: params = { - "api_key": self.tavily_api_key, + "api_key": self.tavily_api_key.get_secret_value(), "query": query, "max_results": max_results, "search_depth": search_depth, diff --git a/libs/community/langchain_community/vectorstores/__init__.py b/libs/community/langchain_community/vectorstores/__init__.py index 1cb42b8fc5131..a5fe62dd99dd3 100644 --- a/libs/community/langchain_community/vectorstores/__init__.py +++ b/libs/community/langchain_community/vectorstores/__init__.py @@ -210,6 +210,12 @@ def _import_hologres() -> Any: return Hologres +def _import_kdbai() -> Any: + from langchain_community.vectorstores.kdbai import KDBAI + + return KDBAI + + def _import_lancedb() -> Any: from langchain_community.vectorstores.lancedb import LanceDB @@ -523,6 +529,8 @@ def __getattr__(name: str) -> Any: return _import_faiss() elif name == "Hologres": return _import_hologres() + elif name == "KDBAI": + return _import_kdbai() elif name == "LanceDB": return _import_lancedb() elif name == "LLMRails": @@ -638,6 +646,7 @@ def __getattr__(name: str) -> Any: "Epsilla", "FAISS", "Hologres", + "KDBAI", "LanceDB", "LLMRails", "Marqo", diff --git a/libs/community/langchain_community/vectorstores/bigquery_vector_search.py b/libs/community/langchain_community/vectorstores/bigquery_vector_search.py index d132e7e071a73..64a1f4b76551c 100644 --- a/libs/community/langchain_community/vectorstores/bigquery_vector_search.py +++ b/libs/community/langchain_community/vectorstores/bigquery_vector_search.py @@ -28,6 +28,7 @@ DEFAULT_CONTENT_COLUMN_NAME = "content" # text content, do not rename DEFAULT_TOP_K = 4 # default number of documents returned from similarity search +_MIN_INDEX_ROWS = 5000 # minimal number of rows for creating an index _INDEX_CHECK_PERIOD_SECONDS = 60 # Do not check for index more often that this. _vector_table_lock = Lock() # process-wide BigQueryVectorSearch table lock @@ -192,6 +193,11 @@ def _initialize_vector_index(self) -> Any: if self._have_index or self._creating_index: # Already have an index or in the process of creating one. return + table = self.bq_client.get_table(self.vectors_table) + if (table.num_rows or 0) < _MIN_INDEX_ROWS: + # Not enough rows to create index. + self._logger.debug("Not enough rows to create a vector index.") + return if ( datetime.utcnow() - self._last_index_check ).total_seconds() < _INDEX_CHECK_PERIOD_SECONDS: @@ -228,6 +234,10 @@ def _create_index_in_background(self): def _create_index(self): from google.api_core.exceptions import ClientError + table = self.bq_client.get_table(self.vectors_table) + if (table.num_rows or 0) < _MIN_INDEX_ROWS: + # Not enough rows to create index. + return if self.distance_strategy == DistanceStrategy.EUCLIDEAN_DISTANCE: distance_type = "EUCLIDEAN" elif self.distance_strategy == DistanceStrategy.COSINE: @@ -534,6 +544,7 @@ def _search_with_score_and_embeddings_by_vector( else: metadata = {} metadata["__id"] = row[self.doc_id_field] + metadata["__job_id"] = job.job_id doc = Document(page_content=row[self.content_field], metadata=metadata) document_tuples.append( (doc, row[self.text_embedding_field], row["_vector_search_distance"]) @@ -833,3 +844,14 @@ def from_texts( vs_obj = BigQueryVectorSearch(embedding=embedding, **kwargs) vs_obj.add_texts(texts, metadatas) return vs_obj + + def explore_job_stats(self, job_id: str) -> Dict: + """Return the statistics for a single job execution. + + Args: + job_id: The BigQuery Job id. + + Returns: + A dictionary of job statistics for a given job. + """ + return self.bq_client.get_job(job_id)._properties["statistics"] diff --git a/libs/community/langchain_community/vectorstores/cassandra.py b/libs/community/langchain_community/vectorstores/cassandra.py index 2a062014339a6..041f699520083 100644 --- a/libs/community/langchain_community/vectorstores/cassandra.py +++ b/libs/community/langchain_community/vectorstores/cassandra.py @@ -75,7 +75,7 @@ def __init__( ttl_seconds: Optional[int] = None, ) -> None: try: - from cassio.vector import VectorTable + from cassio.table import MetadataVectorCassandraTable except (ImportError, ModuleNotFoundError): raise ImportError( "Could not import cassio python package. " @@ -90,11 +90,12 @@ def __init__( # self._embedding_dimension = None # - self.table = VectorTable( + self.table = MetadataVectorCassandraTable( session=session, keyspace=keyspace, table=table_name, - embedding_dimension=self._get_embedding_dimension(), + vector_dimension=self._get_embedding_dimension(), + metadata_indexing="all", primary_key_type="TEXT", ) @@ -127,7 +128,7 @@ def clear(self) -> None: self.table.clear() def delete_by_document_id(self, document_id: str) -> None: - return self.table.delete(document_id) + return self.table.delete(row_id=document_id) def delete(self, ids: Optional[List[str]] = None, **kwargs: Any) -> Optional[bool]: """Delete by vector IDs. @@ -188,7 +189,11 @@ def add_texts( futures = [ self.table.put_async( - text, embedding_vector, text_id, metadata, ttl_seconds + row_id=text_id, + body_blob=text, + vector=embedding_vector, + metadata=metadata or {}, + ttl_seconds=ttl_seconds, ) for text, embedding_vector, text_id, metadata in zip( batch_texts, batch_embedding_vectors, batch_ids, batch_metadatas @@ -215,11 +220,10 @@ def similarity_search_with_score_id_by_vector( """ search_metadata = self._filter_to_metadata(filter) # - hits = self.table.search( - embedding_vector=embedding, - top_k=k, + hits = self.table.metric_ann_search( + vector=embedding, + n=k, metric="cos", - metric_threshold=None, metadata=search_metadata, ) # We stick to 'cos' distance as it can be normalized on a 0-1 axis @@ -227,11 +231,11 @@ def similarity_search_with_score_id_by_vector( return [ ( Document( - page_content=hit["document"], + page_content=hit["body_blob"], metadata=hit["metadata"], ), 0.5 + 0.5 * hit["distance"], - hit["document_id"], + hit["row_id"], ) for hit in hits ] @@ -340,31 +344,32 @@ def max_marginal_relevance_search_by_vector( """ search_metadata = self._filter_to_metadata(filter) - prefetchHits = self.table.search( - embedding_vector=embedding, - top_k=fetch_k, - metric="cos", - metric_threshold=None, - metadata=search_metadata, + prefetch_hits = list( + self.table.metric_ann_search( + vector=embedding, + n=fetch_k, + metric="cos", + metadata=search_metadata, + ) ) # let the mmr utility pick the *indices* in the above array - mmrChosenIndices = maximal_marginal_relevance( + mmr_chosen_indices = maximal_marginal_relevance( np.array(embedding, dtype=np.float32), - [pfHit["embedding_vector"] for pfHit in prefetchHits], + [pf_hit["vector"] for pf_hit in prefetch_hits], k=k, lambda_mult=lambda_mult, ) - mmrHits = [ - pfHit - for pfIndex, pfHit in enumerate(prefetchHits) - if pfIndex in mmrChosenIndices + mmr_hits = [ + pf_hit + for pf_index, pf_hit in enumerate(prefetch_hits) + if pf_index in mmr_chosen_indices ] return [ Document( - page_content=hit["document"], + page_content=hit["body_blob"], metadata=hit["metadata"], ) - for hit in mmrHits + for hit in mmr_hits ] def max_marginal_relevance_search( diff --git a/libs/community/langchain_community/vectorstores/elasticsearch.py b/libs/community/langchain_community/vectorstores/elasticsearch.py index 24c91472c9f62..2f8a9e2469bbb 100644 --- a/libs/community/langchain_community/vectorstores/elasticsearch.py +++ b/libs/community/langchain_community/vectorstores/elasticsearch.py @@ -214,6 +214,8 @@ def index( similarityAlgo = "l2_norm" elif similarity is DistanceStrategy.DOT_PRODUCT: similarityAlgo = "dot_product" + elif similarity is DistanceStrategy.MAX_INNER_PRODUCT: + similarityAlgo = "max_inner_product" else: raise ValueError(f"Similarity {similarity} not supported.") @@ -388,7 +390,6 @@ class ElasticsearchStore(VectorStore): from langchain_community.vectorstores import ElasticsearchStore from langchain_community.embeddings.openai import OpenAIEmbeddings - embeddings = OpenAIEmbeddings() vectorstore = ElasticsearchStore( embedding=OpenAIEmbeddings(), index_name="langchain-demo", @@ -413,7 +414,7 @@ class ElasticsearchStore(VectorStore): distance_strategy: Optional. Distance strategy to use when searching the index. Defaults to COSINE. Can be one of COSINE, - EUCLIDEAN_DISTANCE, or DOT_PRODUCT. + EUCLIDEAN_DISTANCE, MAX_INNER_PRODUCT or DOT_PRODUCT. If you want to use a cloud hosted Elasticsearch instance, you can pass in the cloud_id argument instead of the es_url argument. @@ -509,6 +510,7 @@ def __init__( DistanceStrategy.COSINE, DistanceStrategy.DOT_PRODUCT, DistanceStrategy.EUCLIDEAN_DISTANCE, + DistanceStrategy.MAX_INNER_PRODUCT, ] ] = None, strategy: BaseRetrievalStrategy = ApproxRetrievalStrategy(), @@ -693,6 +695,25 @@ def max_marginal_relevance_search( return selected_docs + @staticmethod + def _identity_fn(score: float) -> float: + return score + + def _select_relevance_score_fn(self) -> Callable[[float], float]: + """ + The 'correct' relevance function + may differ depending on a few things, including: + - the distance / similarity metric used by the VectorStore + - the scale of your embeddings (OpenAI's are unit normed. Many others are not!) + - embedding dimensionality + - etc. + + Vectorstores should define their own selection based method of relevance. + """ + # All scores from Elasticsearch are already normalized similarities: + # https://www.elastic.co/guide/en/elasticsearch/reference/current/dense-vector.html#dense-vector-params + return self._identity_fn + def similarity_search_with_score( self, query: str, k: int = 4, filter: Optional[List[dict]] = None, **kwargs: Any ) -> List[Tuple[Document, float]]: @@ -706,6 +727,9 @@ def similarity_search_with_score( Returns: List of Documents most similar to the query and score for each """ + if isinstance(self.strategy, ApproxRetrievalStrategy) and self.strategy.hybrid: + raise ValueError("scores are currently not supported in hybrid mode") + return self._search(query=query, k=k, filter=filter, **kwargs) def similarity_search_by_vector_with_relevance_scores( @@ -725,6 +749,9 @@ def similarity_search_by_vector_with_relevance_scores( Returns: List of Documents most similar to the embedding and score for each """ + if isinstance(self.strategy, ApproxRetrievalStrategy) and self.strategy.hybrid: + raise ValueError("scores are currently not supported in hybrid mode") + return self._search(query_vector=embedding, k=k, filter=filter, **kwargs) def _search( @@ -1104,7 +1131,8 @@ def from_texts( distance_strategy: Optional. Name of the distance strategy to use. Defaults to "COSINE". can be one of "COSINE", - "EUCLIDEAN_DISTANCE", "DOT_PRODUCT". + "EUCLIDEAN_DISTANCE", "DOT_PRODUCT", + "MAX_INNER_PRODUCT". bulk_kwargs: Optional. Additional arguments to pass to Elasticsearch bulk. """ diff --git a/libs/community/langchain_community/vectorstores/faiss.py b/libs/community/langchain_community/vectorstores/faiss.py index 4e7e5619e192c..8ca609b72592b 100644 --- a/libs/community/langchain_community/vectorstores/faiss.py +++ b/libs/community/langchain_community/vectorstores/faiss.py @@ -986,11 +986,10 @@ def from_embeddings( text_embedding_pairs = zip(texts, text_embeddings) faiss = FAISS.from_embeddings(text_embedding_pairs, embeddings) """ - texts = [t[0] for t in text_embeddings] - embeddings = [t[1] for t in text_embeddings] + texts, embeddings = zip(*text_embeddings) return cls.__from( - texts, - embeddings, + list(texts), + list(embeddings), embedding, metadatas=metadatas, ids=ids, diff --git a/libs/community/langchain_community/vectorstores/jaguar.py b/libs/community/langchain_community/vectorstores/jaguar.py index 688cc81fa10bf..2771530d66dfb 100644 --- a/libs/community/langchain_community/vectorstores/jaguar.py +++ b/libs/community/langchain_community/vectorstores/jaguar.py @@ -158,6 +158,7 @@ def add_texts( """ vcol = self._vector_index filecol = kwargs.get("file_column", "") + text_tag = kwargs.get("text_tag", "") podstorevcol = self._pod + "." + self._store + "." + vcol q = "textcol " + podstorevcol js = self.run(q) @@ -165,6 +166,12 @@ def add_texts( return [] textcol = js["data"] + if text_tag != "": + tag_texts = [] + for t in texts: + tag_texts.append(text_tag + " " + t) + texts = tag_texts + embeddings = self._embedding.embed_documents(list(texts)) ids = [] if metadatas is None: @@ -444,4 +451,5 @@ def _parseMeta(self, nvmap: dict, filecol: str) -> Tuple[List[str], List[str], s nvec.append(k) vvec.append(v) - return nvec, vvec, filepath + vvec_s = [str(e) for e in vvec] + return nvec, vvec_s, filepath diff --git a/libs/community/langchain_community/vectorstores/kdbai.py b/libs/community/langchain_community/vectorstores/kdbai.py new file mode 100644 index 0000000000000..1122b691d439b --- /dev/null +++ b/libs/community/langchain_community/vectorstores/kdbai.py @@ -0,0 +1,267 @@ +from __future__ import annotations + +import logging +import uuid +from typing import Any, Iterable, List, Optional, Tuple + +from langchain_core.documents import Document +from langchain_core.embeddings import Embeddings +from langchain_core.vectorstores import VectorStore + +from langchain_community.vectorstores.utils import DistanceStrategy + +logger = logging.getLogger(__name__) + + +class KDBAI(VectorStore): + """`KDB.AI` vector store [https://kdb.ai](https://kdb.ai) + + To use, you should have the `kdbai_client` python package installed. + + Args: + table: kdbai_client.Table object to use as storage, + embedding: Any embedding function implementing + `langchain.embeddings.base.Embeddings` interface, + distance_strategy: One option from DistanceStrategy.EUCLIDEAN_DISTANCE, + DistanceStrategy.DOT_PRODUCT or DistanceStrategy.COSINE. + + See the example [notebook](https://github.com/KxSystems/langchain/blob/KDB.AI/docs/docs/integrations/vectorstores/kdbai.ipynb). + """ + + def __init__( + self, + table: Any, + embedding: Embeddings, + distance_strategy: Optional[ + DistanceStrategy + ] = DistanceStrategy.EUCLIDEAN_DISTANCE, + ): + try: + import kdbai_client # noqa + except ImportError: + raise ImportError( + "Could not import kdbai_client python package. " + "Please install it with `pip install kdbai_client`." + ) + self._table = table + self._embedding = embedding + self.distance_strategy = distance_strategy + + @property + def embeddings(self) -> Optional[Embeddings]: + if isinstance(self._embedding, Embeddings): + return self._embedding + return None + + def _embed_documents(self, texts: Iterable[str]) -> List[List[float]]: + if isinstance(self._embedding, Embeddings): + return self._embedding.embed_documents(list(texts)) + return [self._embedding(t) for t in texts] + + def _embed_query(self, text: str) -> List[float]: + if isinstance(self._embedding, Embeddings): + return self._embedding.embed_query(text) + return self._embedding(text) + + def _insert( + self, + texts: List[str], + ids: Optional[List[str]], + metadata: Optional[Any] = None, + ) -> None: + try: + import numpy as np + except ImportError: + raise ImportError( + "Could not import numpy python package. " + "Please install it with `pip install numpy`." + ) + + try: + import pandas as pd + except ImportError: + raise ImportError( + "Could not import pandas python package. " + "Please install it with `pip install pandas`." + ) + + embeds = self._embedding.embed_documents(texts) + df = pd.DataFrame() + df["id"] = ids + df["text"] = [t.encode("utf-8") for t in texts] + df["embeddings"] = [np.array(e, dtype="float32") for e in embeds] + if metadata is not None: + df = pd.concat([df, metadata], axis=1) + self._table.insert(df, warn=False) + + def add_texts( + self, + texts: Iterable[str], + metadatas: Optional[List[dict]] = None, + ids: Optional[List[str]] = None, + batch_size: int = 32, + **kwargs: Any, + ) -> List[str]: + """Run more texts through the embeddings and add to the vectorstore. + + Args: + texts (Iterable[str]): Texts to add to the vectorstore. + metadatas (Optional[List[dict]]): List of metadata corresponding to each + chunk of text. + ids (Optional[List[str]]): List of IDs corresponding to each chunk of text. + batch_size (Optional[int]): Size of batch of chunks of text to insert at + once. + + Returns: + List[str]: List of IDs of the added texts. + """ + + try: + import pandas as pd + except ImportError: + raise ImportError( + "Could not import pandas python package. " + "Please install it with `pip install pandas`." + ) + + texts = list(texts) + metadf: pd.DataFrame = None + if metadatas is not None: + if isinstance(metadatas, pd.DataFrame): + metadf = metadatas + else: + metadf = pd.DataFrame(metadatas) + out_ids: List[str] = [] + nbatches = (len(texts) - 1) // batch_size + 1 + for i in range(nbatches): + istart = i * batch_size + iend = (i + 1) * batch_size + batch = texts[istart:iend] + if ids: + batch_ids = ids[istart:iend] + else: + batch_ids = [str(uuid.uuid4()) for _ in range(len(batch))] + if metadf is not None: + batch_meta = metadf.iloc[istart:iend].reset_index(drop=True) + else: + batch_meta = None + self._insert(batch, batch_ids, batch_meta) + out_ids = out_ids + batch_ids + return out_ids + + def add_documents( + self, documents: List[Document], batch_size: int = 32, **kwargs: Any + ) -> List[str]: + """Run more documents through the embeddings and add to the vectorstore. + + Args: + documents (List[Document]: Documents to add to the vectorstore. + batch_size (Optional[int]): Size of batch of documents to insert at once. + + Returns: + List[str]: List of IDs of the added texts. + """ + + try: + import pandas as pd + except ImportError: + raise ImportError( + "Could not import pandas python package. " + "Please install it with `pip install pandas`." + ) + + texts = [x.page_content for x in documents] + metadata = pd.DataFrame([x.metadata for x in documents]) + return self.add_texts(texts, metadata=metadata, batch_size=batch_size) + + def similarity_search_with_score( + self, + query: str, + k: int = 1, + filter: Optional[List] = [], + **kwargs: Any, + ) -> List[Tuple[Document, float]]: + """Run similarity search with distance from a query string. + + Args: + query (str): Query string. + k (Optional[int]): number of neighbors to retrieve. + filter (Optional[List]): KDB.AI metadata filter clause: https://code.kx.com/kdbai/use/filter.html + + Returns: + List[Document]: List of similar documents. + """ + return self.similarity_search_by_vector_with_score( + self._embed_query(query), k=k, filter=filter, **kwargs + ) + + def similarity_search_by_vector_with_score( + self, + embedding: List[float], + *, + k: int = 1, + filter: Optional[List] = [], + **kwargs: Any, + ) -> List[Tuple[Document, float]]: + """Return pinecone documents most similar to embedding, along with scores. + + Args: + embedding (List[float]): query vector. + k (Optional[int]): number of neighbors to retrieve. + filter (Optional[List]): KDB.AI metadata filter clause: https://code.kx.com/kdbai/use/filter.html + + Returns: + List[Document]: List of similar documents. + """ + if "n" in kwargs: + k = kwargs.pop("n") + matches = self._table.search(vectors=[embedding], n=k, filter=filter, **kwargs)[ + 0 + ] + docs = [] + for row in matches.to_dict(orient="records"): + text = row.pop("text") + score = row.pop("__nn_distance") + docs.append( + ( + Document( + page_content=text, + metadata={k: v for k, v in row.items() if k != "text"}, + ), + score, + ) + ) + return docs + + def similarity_search( + self, + query: str, + k: int = 1, + filter: Optional[List] = [], + **kwargs: Any, + ) -> List[Document]: + """Run similarity search from a query string. + + Args: + query (str): Query string. + k (Optional[int]): number of neighbors to retrieve. + filter (Optional[List]): KDB.AI metadata filter clause: https://code.kx.com/kdbai/use/filter.html + + Returns: + List[Document]: List of similar documents. + """ + docs_and_scores = self.similarity_search_with_score( + query, k=k, filter=filter, **kwargs + ) + return [doc for doc, _ in docs_and_scores] + + @classmethod + def from_texts( + cls: Any, + texts: List[str], + embedding: Embeddings, + metadatas: Optional[List[dict]] = None, + **kwargs: Any, + ) -> Any: + """Not implemented.""" + raise Exception("Not implemented.") diff --git a/libs/community/langchain_community/vectorstores/milvus.py b/libs/community/langchain_community/vectorstores/milvus.py index 4c626edce8126..ed751271e0fb7 100644 --- a/libs/community/langchain_community/vectorstores/milvus.py +++ b/libs/community/langchain_community/vectorstores/milvus.py @@ -446,9 +446,15 @@ def _load( timeout: Optional[float] = None, ) -> None: """Load the collection if available.""" - from pymilvus import Collection - - if isinstance(self.col, Collection) and self._get_index() is not None: + from pymilvus import Collection, utility + from pymilvus.client.types import LoadState + + if ( + isinstance(self.col, Collection) + and self._get_index() is not None + and utility.load_state(self.collection_name, using=self.alias) + == LoadState.NotLoad + ): self.col.load( partition_names=partition_names, replica_number=replica_number, diff --git a/libs/community/langchain_community/vectorstores/mongodb_atlas.py b/libs/community/langchain_community/vectorstores/mongodb_atlas.py index f91071ac865ca..c105e6f536f35 100644 --- a/libs/community/langchain_community/vectorstores/mongodb_atlas.py +++ b/libs/community/langchain_community/vectorstores/mongodb_atlas.py @@ -209,6 +209,7 @@ def _similarity_search_with_score( for res in cursor: text = res.pop(self._text_key) score = res.pop("score") + del res["embedding"] docs.append((Document(page_content=text, metadata=res), score)) return docs @@ -221,11 +222,8 @@ def similarity_search_with_score( ) -> List[Tuple[Document, float]]: """Return MongoDB documents most similar to the given query and their scores. - Uses the $vectorSearch stage - performs aNN search on a vector in the specified field. - Index the field as "vector" using Atlas Vector Search "vectorSearch" index type - - For more info : https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-stage/ + Uses the vectorSearch operator available in MongoDB Atlas Search. + For more: https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-stage/ Args: query: Text to look up documents similar to. @@ -233,7 +231,7 @@ def similarity_search_with_score( pre_filter: (Optional) dictionary of argument(s) to prefilter document fields on. post_filter_pipeline: (Optional) Pipeline of MongoDB aggregation stages - following the vector Search. + following the vectorSearch stage. Returns: List of documents most similar to the query and their scores. @@ -257,11 +255,8 @@ def similarity_search( ) -> List[Document]: """Return MongoDB documents most similar to the given query. - Uses the $vectorSearch stage - performs aNN search on a vector in the specified field. - Index the field as "vector" using Atlas Vector Search "vectorSearch" index type - - For more info : https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-stage/ + Uses the vectorSearch operator available in MongoDB Atlas Search. + For more: https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-stage/ Args: query: Text to look up documents similar to. @@ -269,17 +264,22 @@ def similarity_search( pre_filter: (Optional) dictionary of argument(s) to prefilter document fields on. post_filter_pipeline: (Optional) Pipeline of MongoDB aggregation stages - following the vector search. + following the vectorSearch stage. Returns: List of documents most similar to the query and their scores. """ + additional = kwargs.get("additional") docs_and_scores = self.similarity_search_with_score( query, k=k, pre_filter=pre_filter, post_filter_pipeline=post_filter_pipeline, ) + + if additional and "similarity_score" in additional: + for doc, score in docs_and_scores: + doc.metadata["score"] = score return [doc for doc, _ in docs_and_scores] def max_marginal_relevance_search( @@ -309,7 +309,7 @@ def max_marginal_relevance_search( pre_filter: (Optional) dictionary of argument(s) to prefilter on document fields. post_filter_pipeline: (Optional) pipeline of MongoDB aggregation stages - following the vector search. + following the vectorSearch stage. Returns: List of documents selected by maximal marginal relevance. """ diff --git a/libs/community/langchain_community/vectorstores/pgvecto_rs.py b/libs/community/langchain_community/vectorstores/pgvecto_rs.py index 6c4a887a2d8d2..2b14cd8036ea7 100644 --- a/libs/community/langchain_community/vectorstores/pgvecto_rs.py +++ b/libs/community/langchain_community/vectorstores/pgvecto_rs.py @@ -1,32 +1,17 @@ from __future__ import annotations import uuid -from typing import Any, Iterable, List, Literal, Optional, Tuple, Type +from typing import Any, Dict, Iterable, List, Literal, Optional, Tuple, Union -import numpy as np -import sqlalchemy from langchain_core.documents import Document from langchain_core.embeddings import Embeddings from langchain_core.vectorstores import VectorStore -from sqlalchemy import insert, select -from sqlalchemy.dialects import postgresql -from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column -from sqlalchemy.orm.session import Session - - -class _ORMBase(DeclarativeBase): - __tablename__: str - id: Mapped[uuid.UUID] - text: Mapped[str] - meta: Mapped[dict] - embedding: Mapped[np.ndarray] class PGVecto_rs(VectorStore): """VectorStore backed by pgvecto_rs.""" - _engine: sqlalchemy.engine.Engine - _table: Type[_ORMBase] + _store = None _embedding: Embeddings def __init__( @@ -45,28 +30,22 @@ def __init__( db_url: Database URL. collection_name: Name of the collection. new_table: Whether to create a new table or connect to an existing one. - Defaults to False. + If true, the table will be dropped if exists, then recreated. + Defaults to False. """ try: - from pgvecto_rs.sqlalchemy import Vector + from pgvecto_rs.sdk import PGVectoRs except ImportError as e: raise ImportError( - "Unable to import pgvector_rs, please install with " - "`pip install pgvector_rs`." + "Unable to import pgvector_rs.sdk , please install with " + '`pip install "pgvecto_rs[sdk]"`.' ) from e - - class _Table(_ORMBase): - __tablename__ = f"collection_{collection_name}" - id: Mapped[uuid.UUID] = mapped_column( - postgresql.UUID(as_uuid=True), primary_key=True, default=uuid.uuid4 - ) - text: Mapped[str] = mapped_column(sqlalchemy.String) - meta: Mapped[dict] = mapped_column(postgresql.JSONB) - embedding: Mapped[np.ndarray] = mapped_column(Vector(dimension)) - - self._engine = sqlalchemy.create_engine(db_url) - self._table = _Table - self._table.__table__.create(self._engine, checkfirst=not new_table) # type: ignore + self._store = PGVectoRs( + db_url=db_url, + collection_name=collection_name, + dimension=dimension, + recreate=new_table, + ) self._embedding = embedding # ================ Create interface ================= @@ -90,7 +69,6 @@ def from_texts( dimension=dimension, db_url=db_url, collection_name=collection_name, - new_table=True, ) _self.add_texts(texts, metadatas, **kwargs) return _self @@ -148,19 +126,15 @@ def add_texts( List of ids of the added texts. """ + from pgvecto_rs.sdk import Record + embeddings = self._embedding.embed_documents(list(texts)) - with Session(self._engine) as _session: - results: List[str] = [] - for text, embedding, metadata in zip( - texts, embeddings, metadatas or [dict()] * len(list(texts)) - ): - t = insert(self._table).values( - text=text, meta=metadata, embedding=embedding - ) - id = _session.execute(t).inserted_primary_key[0] # type: ignore - results.append(str(id)) - _session.commit() - return results + records = [ + Record.from_text(text, embedding, meta) + for text, embedding, meta in zip(texts, embeddings, metadatas or []) + ] + self._store.insert(records) + return [str(record.id) for record in records] def add_documents(self, documents: List[Document], **kwargs: Any) -> List[str]: """Run more documents through the embeddings and add to the vectorstore. @@ -185,31 +159,41 @@ def similarity_search_with_score_by_vector( distance_func: Literal[ "sqrt_euclid", "neg_dot_prod", "ned_cos" ] = "sqrt_euclid", + filter: Union[None, Dict[str, Any], Any] = None, **kwargs: Any, ) -> List[Tuple[Document, float]]: """Return docs most similar to query vector, with its score.""" - with Session(self._engine) as _session: - real_distance_func = ( - self._table.embedding.squared_euclidean_distance - if distance_func == "sqrt_euclid" - else self._table.embedding.negative_dot_product_distance - if distance_func == "neg_dot_prod" - else self._table.embedding.negative_cosine_distance - if distance_func == "ned_cos" - else None - ) - if real_distance_func is None: - raise ValueError("Invalid distance function") - t = ( - select(self._table, real_distance_func(query_vector).label("score")) - .order_by("score") - .limit(k) # type: ignore + from pgvecto_rs.sdk.filters import meta_contains + + distance_func_map = { + "sqrt_euclid": "<->", + "neg_dot_prod": "<#>", + "ned_cos": "<=>", + } + if filter is None: + real_filter = None + elif isinstance(filter, dict): + real_filter = meta_contains(filter) + else: + real_filter = filter + results = self._store.search( + query_vector, + distance_func_map[distance_func], + k, + filter=real_filter, + ) + + return [ + ( + Document( + page_content=res[0].text, + metadata=res[0].meta, + ), + res[1], ) - return [ - (Document(page_content=row[0].text, metadata=row[0].meta), row[1]) - for row in _session.execute(t) - ] + for res in results + ] def similarity_search_by_vector( self, @@ -218,11 +202,12 @@ def similarity_search_by_vector( distance_func: Literal[ "sqrt_euclid", "neg_dot_prod", "ned_cos" ] = "sqrt_euclid", + filter: Optional[Any] = None, **kwargs: Any, ) -> List[Document]: return [ doc - for doc, score in self.similarity_search_with_score_by_vector( + for doc, _score in self.similarity_search_with_score_by_vector( embedding, k, distance_func, **kwargs ) ] @@ -254,7 +239,7 @@ def similarity_search( query_vector = self._embedding.embed_query(query) return [ doc - for doc, score in self.similarity_search_with_score_by_vector( + for doc, _score in self.similarity_search_with_score_by_vector( query_vector, k, distance_func, **kwargs ) ] diff --git a/libs/community/langchain_community/vectorstores/pgvector.py b/libs/community/langchain_community/vectorstores/pgvector.py index e57fb16358c8e..fe439d86c5a7e 100644 --- a/libs/community/langchain_community/vectorstores/pgvector.py +++ b/libs/community/langchain_community/vectorstores/pgvector.py @@ -62,7 +62,7 @@ class BaseModel(Base): _classes: Any = None -def _get_embedding_collection_store() -> Any: +def _get_embedding_collection_store(vector_dimension: Optional[int] = None) -> Any: global _classes if _classes is not None: return _classes @@ -125,7 +125,7 @@ class EmbeddingStore(BaseModel): ) collection = relationship(CollectionStore, back_populates="embeddings") - embedding: Vector = sqlalchemy.Column(Vector(None)) + embedding: Vector = sqlalchemy.Column(Vector(vector_dimension)) document = sqlalchemy.Column(sqlalchemy.String, nullable=True) cmetadata = sqlalchemy.Column(JSON, nullable=True) @@ -151,6 +151,10 @@ class PGVector(VectorStore): connection_string: Postgres connection string. embedding_function: Any embedding function implementing `langchain.embeddings.base.Embeddings` interface. + embedding_length: The length of the embedding vector. (default: None) + NOTE: This is not mandatory. Defining it will prevent vectors of + any other size to be added to the embeddings table but, without it, + the embeddings can't be indexed. collection_name: The name of the collection to use. (default: langchain) NOTE: This is not the name of the table, but the name of the collection. The tables will be created when initializing the store (if not exists) @@ -183,6 +187,7 @@ def __init__( self, connection_string: str, embedding_function: Embeddings, + embedding_length: Optional[int] = None, collection_name: str = _LANGCHAIN_DEFAULT_COLLECTION_NAME, collection_metadata: Optional[dict] = None, distance_strategy: DistanceStrategy = DEFAULT_DISTANCE_STRATEGY, @@ -195,6 +200,7 @@ def __init__( ) -> None: self.connection_string = connection_string self.embedding_function = embedding_function + self._embedding_length = embedding_length self.collection_name = collection_name self.collection_metadata = collection_metadata self._distance_strategy = distance_strategy @@ -211,7 +217,9 @@ def __post_init__( """Initialize the store.""" self.create_vector_extension() - EmbeddingStore, CollectionStore = _get_embedding_collection_store() + EmbeddingStore, CollectionStore = _get_embedding_collection_store( + self._embedding_length + ) self.CollectionStore = CollectionStore self.EmbeddingStore = EmbeddingStore self.create_tables_if_not_exists() @@ -379,6 +387,7 @@ def add_embeddings( collection = self.get_collection(session) if not collection: raise ValueError("Collection not found") + documents = [] for text, metadata, embedding, id in zip(texts, metadatas, embeddings, ids): embedding_store = self.EmbeddingStore( embedding=embedding, @@ -387,7 +396,8 @@ def add_embeddings( custom_id=id, collection_id=collection.uuid, ) - session.add(embedding_store) + documents.append(embedding_store) + session.bulk_save_objects(documents) session.commit() return ids @@ -545,13 +555,13 @@ def _create_filter_clause(self, key, value): self._create_filter_clause(key, sub_value) for sub_value in value_case_insensitive[OR] ] - filter_by_metadata = sqlalchemy.or_(or_clauses) + filter_by_metadata = sqlalchemy.or_(*or_clauses) elif AND in map(str.lower, value): and_clauses = [ self._create_filter_clause(key, sub_value) for sub_value in value_case_insensitive[AND] ] - filter_by_metadata = sqlalchemy.and_(and_clauses) + filter_by_metadata = sqlalchemy.and_(*and_clauses) else: filter_by_metadata = None diff --git a/libs/community/langchain_community/vectorstores/pinecone.py b/libs/community/langchain_community/vectorstores/pinecone.py index 9e67ad64f6e92..410d55420fa57 100644 --- a/libs/community/langchain_community/vectorstores/pinecone.py +++ b/libs/community/langchain_community/vectorstores/pinecone.py @@ -201,7 +201,7 @@ def similarity_search_by_vector_with_score( namespace = self._namespace docs = [] results = self._index.query( - [embedding], + vector=[embedding], top_k=k, include_metadata=True, namespace=namespace, @@ -299,7 +299,7 @@ def max_marginal_relevance_search_by_vector( if namespace is None: namespace = self._namespace results = self._index.query( - [embedding], + vector=[embedding], top_k=fetch_k, include_values=True, include_metadata=True, diff --git a/libs/community/langchain_community/vectorstores/qdrant.py b/libs/community/langchain_community/vectorstores/qdrant.py index 5073943819b4a..ce64d054d1aa7 100644 --- a/libs/community/langchain_community/vectorstores/qdrant.py +++ b/libs/community/langchain_community/vectorstores/qdrant.py @@ -1138,8 +1138,7 @@ def delete(self, ids: Optional[List[str]] = None, **kwargs: Any) -> Optional[boo **kwargs: Other keyword arguments that subclasses might use. Returns: - Optional[bool]: True if deletion is successful, - False otherwise, None if not implemented. + True if deletion is successful, False otherwise. """ from qdrant_client.http import models as rest @@ -1149,6 +1148,37 @@ def delete(self, ids: Optional[List[str]] = None, **kwargs: Any) -> Optional[boo ) return result.status == rest.UpdateStatus.COMPLETED + @sync_call_fallback + async def adelete( + self, ids: Optional[List[str]] = None, **kwargs: Any + ) -> Optional[bool]: + """Delete by vector ID or other criteria. + + Args: + ids: List of ids to delete. + **kwargs: Other keyword arguments that subclasses might use. + + Returns: + True if deletion is successful, False otherwise. + """ + from qdrant_client.local.async_qdrant_local import AsyncQdrantLocal + + if self.async_client is None or isinstance( + self.async_client._client, AsyncQdrantLocal + ): + raise NotImplementedError( + "QdrantLocal cannot interoperate with sync and async clients" + ) + + from qdrant_client.http import models as rest + + result = await self.async_client.delete( + collection_name=self.collection_name, + points_selector=ids, + ) + + return result.status == rest.UpdateStatus.COMPLETED + @classmethod def from_texts( cls: Type[Qdrant], diff --git a/libs/community/langchain_community/vectorstores/surrealdb.py b/libs/community/langchain_community/vectorstores/surrealdb.py index 951ec04463874..d21f5bf0e0200 100644 --- a/libs/community/langchain_community/vectorstores/surrealdb.py +++ b/libs/community/langchain_community/vectorstores/surrealdb.py @@ -55,14 +55,25 @@ def __init__( embedding_function: Embeddings, **kwargs: Any, ) -> None: - from surrealdb import Surreal + try: + from surrealdb import Surreal + except ImportError as e: + raise ImportError( + """Cannot import from surrealdb. + please install with `pip install surrealdb`.""" + ) from e + + self.dburl = kwargs.pop("dburl", "ws://localhost:8000/rpc") + + if self.dburl[0:2] == "ws": + self.sdb = Surreal(self.dburl) + else: + raise ValueError("Only websocket connections are supported at this time.") - self.collection = kwargs.pop("collection", "documents") self.ns = kwargs.pop("ns", "langchain") self.db = kwargs.pop("db", "database") - self.dburl = kwargs.pop("dburl", "ws://localhost:8000/rpc") + self.collection = kwargs.pop("collection", "documents") self.embedding_function = embedding_function - self.sdb = Surreal(self.dburl) self.kwargs = kwargs async def initialize(self) -> None: @@ -70,12 +81,11 @@ async def initialize(self) -> None: Initialize connection to surrealdb database and authenticate if credentials are provided """ - await self.sdb.connect(self.dburl) + await self.sdb.connect() if "db_user" in self.kwargs and "db_pass" in self.kwargs: user = self.kwargs.get("db_user") password = self.kwargs.get("db_pass") await self.sdb.signin({"user": user, "pass": password}) - await self.sdb.use(self.ns, self.db) @property diff --git a/libs/community/langchain_community/vectorstores/vectara.py b/libs/community/langchain_community/vectorstores/vectara.py index 4a9334b3fbb4b..0fc910042168f 100644 --- a/libs/community/langchain_community/vectorstores/vectara.py +++ b/libs/community/langchain_community/vectorstores/vectara.py @@ -22,11 +22,14 @@ class SummaryConfig: is_enabled: True if summary is enabled, False otherwise max_results: maximum number of results to summarize response_lang: requested language for the summary + prompt_name: name of the prompt to use for summarization + (see https://docs.vectara.com/docs/learn/grounded-generation/select-a-summarizer) """ is_enabled: bool = False max_results: int = 7 response_lang: str = "eng" + prompt_name: str = "vectara-summary-ext-v1.2.0" @dataclass @@ -364,6 +367,7 @@ def vectara_query( { "maxSummarizedResults": config.summary_config.max_results, "responseLang": config.summary_config.response_lang, + "summarizerPromptName": config.summary_config.prompt_name, } ] @@ -570,6 +574,7 @@ class VectaraRetriever(VectorStoreRetriever): "k": 5, "filter": "", "n_sentence_context": "2", + "summary_config": SummaryConfig(), } ) diff --git a/libs/community/langchain_community/vectorstores/vikngdb.py b/libs/community/langchain_community/vectorstores/vikngdb.py new file mode 100644 index 0000000000000..2f235f0bf4cbd --- /dev/null +++ b/libs/community/langchain_community/vectorstores/vikngdb.py @@ -0,0 +1,375 @@ +from __future__ import annotations + +import logging +import uuid +from typing import Any, List, Optional, Tuple + +import numpy as np +from langchain_core.documents import Document +from langchain_core.embeddings import Embeddings +from langchain_core.vectorstores import VectorStore + +from langchain_community.vectorstores.utils import maximal_marginal_relevance + +logger = logging.getLogger(__name__) + + +class VikingDBConfig(object): + def __init__(self, host="host", region="region", ak="ak", sk="sk", scheme="http"): + self.host = host + self.region = region + self.ak = ak + self.sk = sk + self.scheme = scheme + + +class VikingDB(VectorStore): + def __init__( + self, + embedding_function: Embeddings, + collection_name: str = "LangChainCollection", + connection_args: Optional[VikingDBConfig] = None, + index_params: Optional[dict] = None, + drop_old: Optional[bool] = False, + **kwargs: Any, + ): + try: + from volcengine.viking_db import Collection, VikingDBService + except ImportError: + raise ValueError( + "Could not import volcengine python package. " + "Please install it with `pip install --upgrade volcengine`." + ) + self.embedding_func = embedding_function + self.collection_name = collection_name + self.index_name = "LangChainIndex" + self.connection_args = connection_args + self.index_params = index_params + self.drop_old = drop_old + self.service = VikingDBService( + connection_args.host, + connection_args.region, + connection_args.ak, + connection_args.sk, + connection_args.scheme, + ) + + try: + col = self.service.get_collection(collection_name) + except Exception: + col = None + self.collection = col + self.index = None + if self.collection is not None: + self.index = self.service.get_index(self.collection_name, self.index_name) + + if drop_old and isinstance(self.collection, Collection): + indexes = self.service.list_indexes(collection_name) + for index in indexes: + self.service.drop_index(collection_name, index.index_name) + self.service.drop_collection(collection_name) + self.collection = None + self.index = None + + @property + def embeddings(self) -> Embeddings: + return self.embedding_func + + def _create_collection( + self, embeddings: List, metadatas: Optional[List[dict]] = None + ) -> None: + try: + from volcengine.viking_db import Field, FieldType + except ImportError: + raise ValueError( + "Could not import volcengine python package. " + "Please install it with `pip install --upgrade volcengine`." + ) + dim = len(embeddings[0]) + fields = [] + if metadatas: + for key, value in metadatas[0].items(): + # print(key, value) + if isinstance(value, str): + fields.append(Field(key, FieldType.String)) + if isinstance(value, int): + fields.append(Field(key, FieldType.Int64)) + if isinstance(value, bool): + fields.append(Field(key, FieldType.Bool)) + if isinstance(value, list) and all( + isinstance(item, str) for item in value + ): + fields.append(Field(key, FieldType.List_String)) + if isinstance(value, list) and all( + isinstance(item, int) for item in value + ): + fields.append(Field(key, FieldType.List_Int64)) + fields.append(Field("text", FieldType.String)) + + fields.append(Field("primary_key", FieldType.String, is_primary_key=True)) + + fields.append(Field("vector", FieldType.Vector, dim=dim)) + + self.collection = self.service.create_collection(self.collection_name, fields) + + def _create_index(self) -> None: + try: + from volcengine.viking_db import VectorIndexParams + except ImportError: + raise ValueError( + "Could not import volcengine python package. " + "Please install it with `pip install --upgrade volcengine`." + ) + cpu_quota = 2 + vector_index = VectorIndexParams() + partition_by = "" + scalar_index = None + if self.index_params is not None: + if self.index_params.get("cpu_quota") is not None: + cpu_quota = self.index_params["cpu_quota"] + if self.index_params.get("vector_index") is not None: + vector_index = self.index_params["vector_index"] + if self.index_params.get("partition_by") is not None: + partition_by = self.index_params["partition_by"] + if self.index_params.get("scalar_index") is not None: + scalar_index = self.index_params["scalar_index"] + + self.index = self.service.create_index( + self.collection_name, + self.index_name, + vector_index=vector_index, + cpu_quota=cpu_quota, + partition_by=partition_by, + scalar_index=scalar_index, + ) + + def add_texts( + self, + texts: List[str], + metadatas: Optional[List[dict]] = None, + batch_size: int = 1000, + **kwargs: Any, + ) -> List[str]: + try: + from volcengine.viking_db import Data + except ImportError: + raise ValueError( + "Could not import volcengine python package. " + "Please install it with `pip install --upgrade volcengine`." + ) + texts = list(texts) + try: + embeddings = self.embedding_func.embed_documents(texts) + except NotImplementedError: + embeddings = [self.embedding_func.embed_query(x) for x in texts] + if len(embeddings) == 0: + logger.debug("Nothing to insert, skipping.") + return [] + if self.collection is None: + self._create_collection(embeddings, metadatas) + self._create_index() + + # insert data + data = [] + pks: List[str] = [] + for index in range(len(embeddings)): + primary_key = str(uuid.uuid4()) + pks.append(primary_key) + field = { + "text": texts[index], + "primary_key": primary_key, + "vector": embeddings[index], + } + if metadatas is not None and index < len(metadatas): + names = list(metadatas[index].keys()) + for name in names: + field[name] = metadatas[index].get(name) + data.append(Data(field)) + + total_count = len(data) + for i in range(0, total_count, batch_size): + end = min(i + batch_size, total_count) + insert_data = data[i:end] + # print(insert_data) + self.collection.upsert_data(insert_data) + return pks + + def similarity_search( + self, + query: str, + params: Optional[dict] = None, + **kwargs: Any, + ) -> List[Document]: + res = self.similarity_search_with_score(query=query, params=params, **kwargs) + return [doc for doc, _ in res] + + def similarity_search_with_score( + self, + query: str, + params: Optional[dict] = None, + **kwargs: Any, + ) -> List[Tuple[Document, float]]: + embedding = self.embedding_func.embed_query(query) + + res = self.similarity_search_with_score_by_vector( + embedding=embedding, params=params, **kwargs + ) + return res + + def similarity_search_by_vector( + self, + embedding: List[float], + params: Optional[dict] = None, + **kwargs: Any, + ) -> List[Document]: + res = self.similarity_search_with_score_by_vector( + embedding=embedding, params=params, **kwargs + ) + return [doc for doc, _ in res] + + def similarity_search_with_score_by_vector( + self, + embedding: List[float], + params: Optional[dict] = None, + **kwargs: Any, + ) -> List[Tuple[Document, float]]: + if self.collection is None: + logger.debug("No existing collection to search.") + return [] + + filter = None + limit = 10 + output_fields = None + partition = "default" + if params is not None: + if params.get("filter") is not None: + filter = params["filter"] + if params.get("limit") is not None: + limit = params["limit"] + if params.get("output_fields") is not None: + output_fields = params["output_fields"] + if params.get("partition") is not None: + partition = params["partition"] + + res = self.index.search_by_vector( + embedding, + filter=filter, + limit=limit, + output_fields=output_fields, + partition=partition, + ) + + ret = [] + for item in res: + item.fields.pop("primary_key") + item.fields.pop("vector") + page_content = item.fields.pop("text") + doc = Document(page_content=page_content, metadata=item.fields) + pair = (doc, item.score) + ret.append(pair) + return ret + + def max_marginal_relevance_search( + self, + query: str, + k: int = 4, + lambda_mult: float = 0.5, + params: Optional[dict] = None, + **kwargs: Any, + ) -> List[Document]: + embedding = self.embedding_func.embed_query(query) + return self.max_marginal_relevance_search_by_vector( + embedding=embedding, + k=k, + lambda_mult=lambda_mult, + params=params, + **kwargs, + ) + + def max_marginal_relevance_search_by_vector( + self, + embedding: List[float], + k: int = 4, + lambda_mult: float = 0.5, + params: Optional[dict] = None, + **kwargs: Any, + ) -> List[Document]: + if self.collection is None: + logger.debug("No existing collection to search.") + return [] + filter = None + limit = 10 + output_fields = None + partition = "default" + if params is not None: + if params.get("filter") is not None: + filter = params["filter"] + if params.get("limit") is not None: + limit = params["limit"] + if params.get("output_fields") is not None: + output_fields = params["output_fields"] + if params.get("partition") is not None: + partition = params["partition"] + + res = self.index.search_by_vector( + embedding, + filter=filter, + limit=limit, + output_fields=output_fields, + partition=partition, + ) + documents = [] + ordered_result_embeddings = [] + for item in res: + ordered_result_embeddings.append(item.fields.pop("vector")) + item.fields.pop("primary_key") + page_content = item.fields.pop("text") + doc = Document(page_content=page_content, metadata=item.fields) + documents.append(doc) + + new_ordering = maximal_marginal_relevance( + np.array(embedding), ordered_result_embeddings, k=k, lambda_mult=lambda_mult + ) + # Reorder the values and return. + ret = [] + for x in new_ordering: + # Function can return -1 index + if x == -1: + break + else: + ret.append(documents[x]) + return ret + + def delete( + self, + ids: Optional[List[str]] = None, + **kwargs: Any, + ) -> None: + if self.collection is None: + logger.debug("No existing collection to search.") + self.collection.delete_data(ids) + + @classmethod + def from_texts( + cls, + texts: List[str], + embedding: Embeddings, + connection_args: Optional[VikingDBConfig] = None, + metadatas: Optional[List[dict]] = None, + collection_name: str = "LangChainCollection", + index_params: Optional[dict] = None, + drop_old: bool = False, + **kwargs: Any, + ): + if connection_args is None: + raise Exception("VikingDBConfig does not exists") + vector_db = cls( + embedding_function=embedding, + collection_name=collection_name, + connection_args=connection_args, + index_params=index_params, + drop_old=drop_old, + **kwargs, + ) + vector_db.add_texts(texts=texts, metadatas=metadatas) + return vector_db diff --git a/libs/community/poetry.lock b/libs/community/poetry.lock index db439c02327f2..cd2e0d3ae5e0c 100644 --- a/libs/community/poetry.lock +++ b/libs/community/poetry.lock @@ -1765,6 +1765,42 @@ files = [ duckdb = ">=0.4.0" sqlalchemy = ">=1.3.22" +[[package]] +name = "elastic-transport" +version = "8.12.0" +description = "Transport classes and utilities shared among Python Elastic client libraries" +optional = true +python-versions = ">=3.7" +files = [ + {file = "elastic-transport-8.12.0.tar.gz", hash = "sha256:48839b942fcce199eece1558ecea6272e116c58da87ca8d495ef12eb61effaf7"}, + {file = "elastic_transport-8.12.0-py3-none-any.whl", hash = "sha256:87d9dc9dee64a05235e7624ed7e6ab6e5ca16619aa7a6d22e853273b9f1cfbee"}, +] + +[package.dependencies] +certifi = "*" +urllib3 = ">=1.26.2,<3" + +[package.extras] +develop = ["aiohttp", "furo", "mock", "pytest", "pytest-asyncio", "pytest-cov", "pytest-httpserver", "pytest-mock", "requests", "sphinx (>2)", "sphinx-autodoc-typehints", "trustme"] + +[[package]] +name = "elasticsearch" +version = "8.12.0" +description = "Python client for Elasticsearch" +optional = true +python-versions = ">=3.7" +files = [ + {file = "elasticsearch-8.12.0-py3-none-any.whl", hash = "sha256:d394c5ce746bb8cb97827feae57759dae462bce34df221a6fdb6875c56476389"}, + {file = "elasticsearch-8.12.0.tar.gz", hash = "sha256:58fd3876682f7529c33b9eeee701e71cfcc334bb45d725e315e22a0a5e2611fb"}, +] + +[package.dependencies] +elastic-transport = ">=8,<9" + +[package.extras] +async = ["aiohttp (>=3,<4)"] +requests = ["requests (>=2.4.0,<3.0.0)"] + [[package]] name = "entrypoints" version = "0.4" @@ -3881,7 +3917,7 @@ files = [ [[package]] name = "langchain-core" -version = "0.1.9" +version = "0.1.14" description = "Building applications with LLMs through composability" optional = false python-versions = ">=3.8.1,<4.0" @@ -3891,7 +3927,7 @@ develop = true [package.dependencies] anyio = ">=3,<5" jsonpatch = "^1.33" -langsmith = "~0.0.63" +langsmith = ">=0.0.83,<0.1" packaging = "^23.2" pydantic = ">=1,<3" PyYAML = ">=5.3" @@ -3907,13 +3943,13 @@ url = "../core" [[package]] name = "langsmith" -version = "0.0.69" +version = "0.0.83" description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform." optional = false python-versions = ">=3.8.1,<4.0" files = [ - {file = "langsmith-0.0.69-py3-none-any.whl", hash = "sha256:49a2546bb83eedb0552673cf81a068bb08078d6d48471f4f1018e1d5c6aa46b1"}, - {file = "langsmith-0.0.69.tar.gz", hash = "sha256:8fb5297f274db0576ec650d9bab0319acfbb6622d62bc5bb9fe31c6235dc0358"}, + {file = "langsmith-0.0.83-py3-none-any.whl", hash = "sha256:a5bb7ac58c19a415a9d5f51db56dd32ee2cd7343a00825bbc2018312eb3d122a"}, + {file = "langsmith-0.0.83.tar.gz", hash = "sha256:94427846b334ad9bdbec3266fee12903fe9f5448f628667689d0412012aaf392"}, ] [package.dependencies] @@ -9139,9 +9175,9 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [extras] cli = ["typer"] -extended-testing = ["aiosqlite", "aleph-alpha-client", "anthropic", "arxiv", "assemblyai", "atlassian-python-api", "azure-ai-documentintelligence", "beautifulsoup4", "bibtexparser", "cassio", "chardet", "cohere", "dashvector", "databricks-vectorsearch", "datasets", "dgml-utils", "esprima", "faiss-cpu", "feedparser", "fireworks-ai", "geopandas", "gitpython", "google-cloud-documentai", "gql", "gradientai", "hologres-vector", "html2text", "javelin-sdk", "jinja2", "jq", "jsonschema", "lxml", "markdownify", "motor", "msal", "mwparserfromhell", "mwxml", "newspaper3k", "numexpr", "openai", "openapi-pydantic", "oracle-ads", "pandas", "pdfminer-six", "pgvector", "praw", "psychicapi", "py-trello", "pymupdf", "pypdf", "pypdfium2", "pyspark", "rank-bm25", "rapidfuzz", "rapidocr-onnxruntime", "requests-toolbelt", "rspace_client", "scikit-learn", "sqlite-vss", "streamlit", "sympy", "telethon", "timescale-vector", "tqdm", "upstash-redis", "xata", "xmltodict", "zhipuai"] +extended-testing = ["aiosqlite", "aleph-alpha-client", "anthropic", "arxiv", "assemblyai", "atlassian-python-api", "azure-ai-documentintelligence", "beautifulsoup4", "bibtexparser", "cassio", "chardet", "cohere", "dashvector", "databricks-vectorsearch", "datasets", "dgml-utils", "elasticsearch", "esprima", "faiss-cpu", "feedparser", "fireworks-ai", "geopandas", "gitpython", "google-cloud-documentai", "gql", "gradientai", "hologres-vector", "html2text", "javelin-sdk", "jinja2", "jq", "jsonschema", "lxml", "markdownify", "motor", "msal", "mwparserfromhell", "mwxml", "newspaper3k", "numexpr", "openai", "openapi-pydantic", "oracle-ads", "pandas", "pdfminer-six", "pgvector", "praw", "psychicapi", "py-trello", "pymupdf", "pypdf", "pypdfium2", "pyspark", "rank-bm25", "rapidfuzz", "rapidocr-onnxruntime", "requests-toolbelt", "rspace_client", "scikit-learn", "sqlite-vss", "streamlit", "sympy", "telethon", "timescale-vector", "tqdm", "upstash-redis", "xata", "xmltodict", "zhipuai"] [metadata] lock-version = "2.0" python-versions = ">=3.8.1,<4.0" -content-hash = "edadd024e8b2b4a817a90336013a1d92be102d03d4c41fbf5ac137f16d97fdfb" +content-hash = "73184aec5978e0de5b99029724164fa76394beb6359b59763ca488a258b0df4d" diff --git a/libs/community/pyproject.toml b/libs/community/pyproject.toml index 3c92ca76ff795..9a781bc75b05c 100644 --- a/libs/community/pyproject.toml +++ b/libs/community/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "langchain-community" -version = "0.0.12" +version = "0.0.15" description = "Community contributed LangChain integrations." authors = [] license = "MIT" @@ -9,7 +9,7 @@ repository = "https://github.com/langchain-ai/langchain" [tool.poetry.dependencies] python = ">=3.8.1,<4.0" -langchain-core = ">=0.1.9,<0.2" +langchain-core = ">=0.1.14,<0.2" SQLAlchemy = ">=1.4,<3" requests = "^2" PyYAML = ">=5.3" @@ -17,7 +17,7 @@ numpy = "^1" aiohttp = "^3.8.3" tenacity = "^8.1.0" dataclasses-json = ">= 0.5.7, < 0.7" -langsmith = "~0.0.63" +langsmith = ">=0.0.83,<0.1" tqdm = {version = ">=4.48.0", optional = true} openapi-pydantic = {version = "^0.3.2", optional = true} faiss-cpu = {version = "^1", optional = true} @@ -87,6 +87,7 @@ datasets = {version = "^2.15.0", optional = true} azure-ai-documentintelligence = {version = "^1.0.0b1", optional = true} oracle-ads = {version = "^2.9.1", optional = true} zhipuai = {version = "^1.0.7", optional = true} +elasticsearch = {version = "^8.12.0", optional = true} [tool.poetry.group.test] optional = true @@ -249,6 +250,7 @@ extended_testing = [ "azure-ai-documentintelligence", "oracle-ads", "zhipuai", + "elasticsearch", ] [tool.ruff] diff --git a/libs/community/tests/examples/fake.vsdx b/libs/community/tests/examples/fake.vsdx new file mode 100644 index 0000000000000..4e6502942eaba Binary files /dev/null and b/libs/community/tests/examples/fake.vsdx differ diff --git a/libs/community/tests/integration_tests/.env.example b/libs/community/tests/integration_tests/.env.example new file mode 100644 index 0000000000000..99be838353376 --- /dev/null +++ b/libs/community/tests/integration_tests/.env.example @@ -0,0 +1,52 @@ +# openai +# your api key from https://platform.openai.com/account/api-keys +OPENAI_API_KEY=your_openai_api_key_here + + +# searchapi +# your api key from https://www.searchapi.io/ +SEARCHAPI_API_KEY=your_searchapi_api_key_here + + +# astra db +ASTRA_DB_API_ENDPOINT=https://your_astra_db_id-your_region.apps.astra.datastax.com +ASTRA_DB_APPLICATION_TOKEN=AstraCS:your_astra_db_application_token +# ASTRA_DB_KEYSPACE=your_astra_db_namespace + + +# cassandra +CASSANDRA_CONTACT_POINTS=127.0.0.1 +# CASSANDRA_USERNAME=your_cassandra_username +# CASSANDRA_PASSWORD=your_cassandra_password +# CASSANDRA_KEYSPACE=your_cassandra_keyspace + + +# pinecone +# your api key from left menu "API Keys" in https://app.pinecone.io +PINECONE_API_KEY=your_pinecone_api_key_here +# your pinecone environment from left menu "API Keys" in https://app.pinecone.io +PINECONE_ENVIRONMENT=us-west4-gcp + + +# jira +# your api token from https://id.atlassian.com/manage-profile/security/api-tokens +# more details here: https://confluence.atlassian.com/enterprise/using-personal-access-tokens-1026032365.html +# JIRA_API_TOKEN=your_jira_api_token_here +# JIRA_USERNAME=your_jira_username_here +# JIRA_INSTANCE_URL=your_jira_instance_url_here + + +# clickup +CLICKUP_ACCESS_TOKEN=your_clickup_access_token + + +# power bi +# sign in to azure in order to authenticate with DefaultAzureCredentials +# details here https://learn.microsoft.com/en-us/dotnet/api/azure.identity.defaultazurecredential?view=azure-dotnet +POWERBI_DATASET_ID=_powerbi_dataset_id_here +POWERBI_TABLE_NAME=_test_table_name_here +POWERBI_NUMROWS=_num_rows_in_your_test_table + + +# MongoDB Atlas Vector Search +MONGODB_ATLAS_URI=your_mongodb_atlas_connection_string diff --git a/libs/community/tests/integration_tests/chat_message_histories/test_tidb.py b/libs/community/tests/integration_tests/chat_message_histories/test_tidb.py new file mode 100644 index 0000000000000..17601af48b557 --- /dev/null +++ b/libs/community/tests/integration_tests/chat_message_histories/test_tidb.py @@ -0,0 +1,101 @@ +import os + +import pytest +from langchain_core.messages import AIMessage, HumanMessage + +from langchain_community.chat_message_histories import TiDBChatMessageHistory + +try: + CONNECTION_STRING = os.getenv("TEST_TiDB_CHAT_URL", "") + + if CONNECTION_STRING == "": + raise OSError("TEST_TiDB_URL environment variable is not set") + + tidb_available = True +except (OSError, ImportError): + tidb_available = False + + +@pytest.mark.skipif(not tidb_available, reason="tidb is not available") +def test_add_messages() -> None: + """Basic testing: adding messages to the TiDBChatMessageHistory.""" + message_store = TiDBChatMessageHistory("23334", CONNECTION_STRING) + message_store.clear() + assert len(message_store.messages) == 0 + message_store.add_user_message("Hello! Language Chain!") + message_store.add_ai_message("Hi Guys!") + + # create another message store to check if the messages are stored correctly + message_store_another = TiDBChatMessageHistory("46666", CONNECTION_STRING) + message_store_another.clear() + assert len(message_store_another.messages) == 0 + message_store_another.add_user_message("Hello! Bot!") + message_store_another.add_ai_message("Hi there!") + message_store_another.add_user_message("How's this pr going?") + + # Now check if the messages are stored in the database correctly + assert len(message_store.messages) == 2 + assert isinstance(message_store.messages[0], HumanMessage) + assert isinstance(message_store.messages[1], AIMessage) + assert message_store.messages[0].content == "Hello! Language Chain!" + assert message_store.messages[1].content == "Hi Guys!" + + assert len(message_store_another.messages) == 3 + assert isinstance(message_store_another.messages[0], HumanMessage) + assert isinstance(message_store_another.messages[1], AIMessage) + assert isinstance(message_store_another.messages[2], HumanMessage) + assert message_store_another.messages[0].content == "Hello! Bot!" + assert message_store_another.messages[1].content == "Hi there!" + assert message_store_another.messages[2].content == "How's this pr going?" + + # Now clear the first history + message_store.clear() + assert len(message_store.messages) == 0 + assert len(message_store_another.messages) == 3 + message_store_another.clear() + assert len(message_store.messages) == 0 + assert len(message_store_another.messages) == 0 + + +def test_tidb_recent_chat_message(): + """Test the TiDBChatMessageHistory with earliest_time parameter.""" + import time + from datetime import datetime + + # prepare some messages + message_store = TiDBChatMessageHistory("2333", CONNECTION_STRING) + message_store.clear() + assert len(message_store.messages) == 0 + message_store.add_user_message("Hello! Language Chain!") + message_store.add_ai_message("Hi Guys!") + assert len(message_store.messages) == 2 + assert isinstance(message_store.messages[0], HumanMessage) + assert isinstance(message_store.messages[1], AIMessage) + assert message_store.messages[0].content == "Hello! Language Chain!" + assert message_store.messages[1].content == "Hi Guys!" + + # now we add some recent messages to the database + earliest_time = datetime.utcnow() + time.sleep(1) + + message_store.add_user_message("How's this pr going?") + message_store.add_ai_message("It's almost done!") + assert len(message_store.messages) == 4 + assert isinstance(message_store.messages[2], HumanMessage) + assert isinstance(message_store.messages[3], AIMessage) + assert message_store.messages[2].content == "How's this pr going?" + assert message_store.messages[3].content == "It's almost done!" + + # now we create another message store with earliest_time parameter + message_store_another = TiDBChatMessageHistory( + "2333", CONNECTION_STRING, earliest_time=earliest_time + ) + assert len(message_store_another.messages) == 2 + assert isinstance(message_store_another.messages[0], HumanMessage) + assert isinstance(message_store_another.messages[1], AIMessage) + assert message_store_another.messages[0].content == "How's this pr going?" + assert message_store_another.messages[1].content == "It's almost done!" + + # now we clear the message store + message_store.clear() + assert len(message_store.messages) == 0 diff --git a/libs/community/tests/integration_tests/chat_models/test_azureml_endpoint.py b/libs/community/tests/integration_tests/chat_models/test_azureml_endpoint.py index d8871e784f010..31092d625ba75 100644 --- a/libs/community/tests/integration_tests/chat_models/test_azureml_endpoint.py +++ b/libs/community/tests/integration_tests/chat_models/test_azureml_endpoint.py @@ -5,31 +5,31 @@ from langchain_community.chat_models.azureml_endpoint import ( AzureMLChatOnlineEndpoint, - LlamaContentFormatter, + LlamaChatContentFormatter, ) def test_llama_call() -> None: """Test valid call to Open Source Foundation Model.""" - chat = AzureMLChatOnlineEndpoint(content_formatter=LlamaContentFormatter()) - response = chat(messages=[HumanMessage(content="Foo")]) + chat = AzureMLChatOnlineEndpoint(content_formatter=LlamaChatContentFormatter()) + response = chat.invoke([HumanMessage(content="Foo")]) assert isinstance(response, BaseMessage) assert isinstance(response.content, str) -def test_timeout_kwargs() -> None: +def test_temperature_kwargs() -> None: """Test that timeout kwarg works.""" - chat = AzureMLChatOnlineEndpoint(content_formatter=LlamaContentFormatter()) - response = chat(messages=[HumanMessage(content="FOO")], timeout=60) + chat = AzureMLChatOnlineEndpoint(content_formatter=LlamaChatContentFormatter()) + response = chat.invoke([HumanMessage(content="FOO")], temperature=0.8) assert isinstance(response, BaseMessage) assert isinstance(response.content, str) def test_message_history() -> None: """Test that multiple messages works.""" - chat = AzureMLChatOnlineEndpoint(content_formatter=LlamaContentFormatter()) - response = chat( - messages=[ + chat = AzureMLChatOnlineEndpoint(content_formatter=LlamaChatContentFormatter()) + response = chat.invoke( + [ HumanMessage(content="Hello."), AIMessage(content="Hello!"), HumanMessage(content="How are you doing?"), @@ -40,7 +40,7 @@ def test_message_history() -> None: def test_multiple_messages() -> None: - chat = AzureMLChatOnlineEndpoint(content_formatter=LlamaContentFormatter()) + chat = AzureMLChatOnlineEndpoint(content_formatter=LlamaChatContentFormatter()) message = HumanMessage(content="Hi!") response = chat.generate([[message], [message]]) diff --git a/libs/community/tests/integration_tests/chat_models/test_baichuan.py b/libs/community/tests/integration_tests/chat_models/test_baichuan.py index 0ad3ab74799ca..6caec6003e95d 100644 --- a/libs/community/tests/integration_tests/chat_models/test_baichuan.py +++ b/libs/community/tests/integration_tests/chat_models/test_baichuan.py @@ -2,17 +2,36 @@ from langchain_community.chat_models.baichuan import ChatBaichuan +# For testing, run: +# TEST_FILE=tests/integration_tests/chat_models/test_baichuan.py make test -def test_chat_baichuan() -> None: + +def test_chat_baichuan_default() -> None: + chat = ChatBaichuan(streaming=True) + message = HumanMessage(content="请完整背诵将进酒,背诵5遍") + response = chat([message]) + assert isinstance(response, AIMessage) + assert isinstance(response.content, str) + + +def test_chat_baichuan_default_non_streaming() -> None: chat = ChatBaichuan() + message = HumanMessage(content="请完整背诵将进酒,背诵5遍") + response = chat([message]) + assert isinstance(response, AIMessage) + assert isinstance(response.content, str) + + +def test_chat_baichuan_turbo() -> None: + chat = ChatBaichuan(model="Baichuan2-Turbo", streaming=True) message = HumanMessage(content="Hello") response = chat([message]) assert isinstance(response, AIMessage) assert isinstance(response.content, str) -def test_chat_baichuan_with_model() -> None: - chat = ChatBaichuan(model="Baichuan2-13B") +def test_chat_baichuan_turbo_non_streaming() -> None: + chat = ChatBaichuan(model="Baichuan2-Turbo") message = HumanMessage(content="Hello") response = chat([message]) assert isinstance(response, AIMessage) @@ -20,7 +39,7 @@ def test_chat_baichuan_with_model() -> None: def test_chat_baichuan_with_temperature() -> None: - chat = ChatBaichuan(model="Baichuan2-13B", temperature=1.0) + chat = ChatBaichuan(temperature=1.0) message = HumanMessage(content="Hello") response = chat([message]) assert isinstance(response, AIMessage) @@ -29,13 +48,15 @@ def test_chat_baichuan_with_temperature() -> None: def test_chat_baichuan_with_kwargs() -> None: chat = ChatBaichuan() - message = HumanMessage(content="Hello") - response = chat([message], temperature=0.88, top_p=0.7) + message = HumanMessage(content="百川192K API是什么时候上线的?") + response = chat([message], temperature=0.88, top_p=0.7, with_search_enhance=True) + print(response) assert isinstance(response, AIMessage) assert isinstance(response.content, str) def test_extra_kwargs() -> None: - chat = ChatBaichuan(temperature=0.88, top_p=0.7) + chat = ChatBaichuan(temperature=0.88, top_p=0.7, with_search_enhance=True) assert chat.temperature == 0.88 assert chat.top_p == 0.7 + assert chat.with_search_enhance is True diff --git a/libs/community/tests/integration_tests/chat_models/test_deepinfra.py b/libs/community/tests/integration_tests/chat_models/test_deepinfra.py new file mode 100644 index 0000000000000..0fa4593ace8ab --- /dev/null +++ b/libs/community/tests/integration_tests/chat_models/test_deepinfra.py @@ -0,0 +1,65 @@ +"""Test ChatDeepInfra wrapper.""" +from langchain_core.messages import BaseMessage, HumanMessage +from langchain_core.outputs import ChatGeneration, LLMResult + +from langchain_community.chat_models.deepinfra import ChatDeepInfra +from tests.unit_tests.callbacks.fake_callback_handler import FakeCallbackHandler + + +def test_chat_deepinfra() -> None: + """Test valid call to DeepInfra.""" + chat = ChatDeepInfra( + max_tokens=10, + ) + response = chat.invoke([HumanMessage(content="Hello")]) + assert isinstance(response, BaseMessage) + assert isinstance(response.content, str) + + +def test_chat_deepinfra_streaming() -> None: + callback_handler = FakeCallbackHandler() + chat = ChatDeepInfra( + callbacks=[callback_handler], + streaming=True, + max_tokens=10, + ) + response = chat.invoke([HumanMessage(content="Hello")]) + assert callback_handler.llm_streams > 0 + assert isinstance(response, BaseMessage) + + +async def test_async_chat_deepinfra() -> None: + """Test async generation.""" + chat = ChatDeepInfra( + max_tokens=10, + ) + message = HumanMessage(content="Hello") + response = await chat.agenerate([[message]]) + assert isinstance(response, LLMResult) + assert len(response.generations) == 1 + assert len(response.generations[0]) == 1 + generation = response.generations[0][0] + assert isinstance(generation, ChatGeneration) + assert isinstance(generation.text, str) + assert generation.text == generation.message.content + + +async def test_async_chat_deepinfra_streaming() -> None: + callback_handler = FakeCallbackHandler() + chat = ChatDeepInfra( + # model="meta-llama/Llama-2-7b-chat-hf", + callbacks=[callback_handler], + max_tokens=10, + streaming=True, + timeout=5, + ) + message = HumanMessage(content="Hello") + response = await chat.agenerate([[message]]) + assert callback_handler.llm_streams > 0 + assert isinstance(response, LLMResult) + assert len(response.generations) == 1 + assert len(response.generations[0]) == 1 + generation = response.generations[0][0] + assert isinstance(generation, ChatGeneration) + assert isinstance(generation.text, str) + assert generation.text == generation.message.content diff --git a/libs/community/tests/integration_tests/chat_models/test_konko.py b/libs/community/tests/integration_tests/chat_models/test_konko.py index b87e709d2088a..9f38f740eb45a 100644 --- a/libs/community/tests/integration_tests/chat_models/test_konko.py +++ b/libs/community/tests/integration_tests/chat_models/test_konko.py @@ -63,7 +63,7 @@ def test_konko_chat_test() -> None: def test_konko_chat_test_openai() -> None: """Evaluate basic ChatKonko functionality.""" - chat_instance = ChatKonko(max_tokens=10, model="gpt-3.5-turbo") + chat_instance = ChatKonko(max_tokens=10, model="meta-llama/llama-2-70b-chat") msg = HumanMessage(content="Hi") chat_response = chat_instance([msg]) assert isinstance(chat_response, BaseMessage) diff --git a/libs/community/tests/integration_tests/chat_models/test_sparkllm.py b/libs/community/tests/integration_tests/chat_models/test_sparkllm.py new file mode 100644 index 0000000000000..a219b8857420a --- /dev/null +++ b/libs/community/tests/integration_tests/chat_models/test_sparkllm.py @@ -0,0 +1,36 @@ +from langchain_core.messages import AIMessage, AIMessageChunk, HumanMessage + +from langchain_community.chat_models.sparkllm import ChatSparkLLM + + +def test_chat_spark_llm() -> None: + chat = ChatSparkLLM() + message = HumanMessage(content="Hello") + response = chat([message]) + assert isinstance(response, AIMessage) + assert isinstance(response.content, str) + + +def test_chat_spark_llm_streaming() -> None: + chat = ChatSparkLLM(streaming=True) + for chunk in chat.stream("Hello!"): + assert isinstance(chunk, AIMessageChunk) + assert isinstance(chunk.content, str) + + +def test_chat_spark_llm_with_domain() -> None: + chat = ChatSparkLLM(spark_llm_domain="generalv3") + message = HumanMessage(content="Hello") + response = chat([message]) + print(response) + assert isinstance(response, AIMessage) + assert isinstance(response.content, str) + + +def test_chat_spark_llm_with_temperature() -> None: + chat = ChatSparkLLM(temperature=0.9, top_k=2) + message = HumanMessage(content="Hello") + response = chat([message]) + print(response) + assert isinstance(response, AIMessage) + assert isinstance(response.content, str) diff --git a/libs/community/tests/integration_tests/conftest.py b/libs/community/tests/integration_tests/conftest.py new file mode 100644 index 0000000000000..02b518e8695a2 --- /dev/null +++ b/libs/community/tests/integration_tests/conftest.py @@ -0,0 +1,19 @@ +# Getting the absolute path of the current file's directory +import os + +ABS_PATH = os.path.dirname(os.path.abspath(__file__)) + +# Getting the absolute path of the project's root directory +PROJECT_DIR = os.path.abspath(os.path.join(ABS_PATH, os.pardir, os.pardir)) + + +# Loading the .env file if it exists +def _load_env() -> None: + dotenv_path = os.path.join(PROJECT_DIR, "tests", "integration_tests", ".env") + if os.path.exists(dotenv_path): + from dotenv import load_dotenv + + load_dotenv(dotenv_path) + + +_load_env() diff --git a/libs/community/tests/integration_tests/document_loaders/test_cassandra.py b/libs/community/tests/integration_tests/document_loaders/test_cassandra.py new file mode 100644 index 0000000000000..59db9f7d3e818 --- /dev/null +++ b/libs/community/tests/integration_tests/document_loaders/test_cassandra.py @@ -0,0 +1,121 @@ +""" +Test of Cassandra document loader class `CassandraLoader` +""" +import os +from typing import Any + +import pytest +from langchain_core.documents import Document + +from langchain_community.document_loaders.cassandra import CassandraLoader + +CASSANDRA_DEFAULT_KEYSPACE = "docloader_test_keyspace" +CASSANDRA_TABLE = "docloader_test_table" + + +@pytest.fixture(autouse=True, scope="session") +def keyspace() -> str: + import cassio + from cassandra.cluster import Cluster + from cassio.config import check_resolve_session, resolve_keyspace + from cassio.table.tables import PlainCassandraTable + + if any( + env_var in os.environ + for env_var in [ + "CASSANDRA_CONTACT_POINTS", + "ASTRA_DB_APPLICATION_TOKEN", + "ASTRA_DB_INIT_STRING", + ] + ): + cassio.init(auto=True) + session = check_resolve_session() + else: + cluster = Cluster() + session = cluster.connect() + keyspace = resolve_keyspace() or CASSANDRA_DEFAULT_KEYSPACE + cassio.init(session=session, keyspace=keyspace) + + session.execute( + ( + f"CREATE KEYSPACE IF NOT EXISTS {keyspace} " + f"WITH replication = {{'class': 'SimpleStrategy', 'replication_factor': 1}}" + ) + ) + + # We use a cassio table by convenience to seed the DB + table = PlainCassandraTable( + table=CASSANDRA_TABLE, keyspace=keyspace, session=session + ) + table.put(row_id="id1", body_blob="text1") + table.put(row_id="id2", body_blob="text2") + + yield keyspace + + session.execute(f"DROP TABLE IF EXISTS {keyspace}.{CASSANDRA_TABLE}") + + +def test_loader_table(keyspace: str) -> None: + loader = CassandraLoader(table=CASSANDRA_TABLE) + assert loader.load() == [ + Document( + page_content='{"row_id": "id1", "body_blob": "text1"}', + metadata={"table": CASSANDRA_TABLE, "keyspace": keyspace}, + ), + Document( + page_content='{"row_id": "id2", "body_blob": "text2"}', + metadata={"table": CASSANDRA_TABLE, "keyspace": keyspace}, + ), + ] + + +def test_loader_query(keyspace: str) -> None: + loader = CassandraLoader( + query=f"SELECT body_blob FROM {keyspace}.{CASSANDRA_TABLE}" + ) + assert loader.load() == [ + Document(page_content='{"body_blob": "text1"}'), + Document(page_content='{"body_blob": "text2"}'), + ] + + +def test_loader_page_content_mapper(keyspace: str) -> None: + def mapper(row: Any) -> str: + return str(row.body_blob) + + loader = CassandraLoader(table=CASSANDRA_TABLE, page_content_mapper=mapper) + assert loader.load() == [ + Document( + page_content="text1", + metadata={"table": CASSANDRA_TABLE, "keyspace": keyspace}, + ), + Document( + page_content="text2", + metadata={"table": CASSANDRA_TABLE, "keyspace": keyspace}, + ), + ] + + +def test_loader_metadata_mapper(keyspace: str) -> None: + def mapper(row: Any) -> dict: + return {"id": row.row_id} + + loader = CassandraLoader(table=CASSANDRA_TABLE, metadata_mapper=mapper) + assert loader.load() == [ + Document( + page_content='{"row_id": "id1", "body_blob": "text1"}', + metadata={ + "table": CASSANDRA_TABLE, + "keyspace": keyspace, + "id": "id1", + }, + ), + Document( + page_content='{"row_id": "id2", "body_blob": "text2"}', + metadata={ + "table": CASSANDRA_TABLE, + "keyspace": keyspace, + "id": "id2", + }, + ), + ] diff --git a/libs/community/tests/integration_tests/document_loaders/test_unstructured.py b/libs/community/tests/integration_tests/document_loaders/test_unstructured.py index bb1d809ca5d0e..5bdd30f2c2e2a 100644 --- a/libs/community/tests/integration_tests/document_loaders/test_unstructured.py +++ b/libs/community/tests/integration_tests/document_loaders/test_unstructured.py @@ -28,6 +28,23 @@ def add_the_end(text: str) -> str: assert docs[0].page_content.endswith("THE END!") +def test_unstructured_file_loader_multiple_files() -> None: + """Test unstructured loader.""" + file_paths = [ + os.path.join(EXAMPLE_DOCS_DIRECTORY, "layout-parser-paper.pdf"), + os.path.join(EXAMPLE_DOCS_DIRECTORY, "whatsapp_chat.txt"), + ] + + loader = UnstructuredFileLoader( + file_path=file_paths, + strategy="fast", + mode="elements", + ) + docs = loader.load() + + assert len(docs) > 1 + + def test_unstructured_api_file_loader() -> None: """Test unstructured loader.""" file_path = os.path.join(EXAMPLE_DOCS_DIRECTORY, "layout-parser-paper.pdf") diff --git a/libs/community/tests/integration_tests/embeddings/test_deepinfra.py b/libs/community/tests/integration_tests/embeddings/test_deepinfra.py index 8b3fe25e6675e..f3a418ed2391c 100644 --- a/libs/community/tests/integration_tests/embeddings/test_deepinfra.py +++ b/libs/community/tests/integration_tests/embeddings/test_deepinfra.py @@ -5,7 +5,7 @@ def test_deepinfra_call() -> None: """Test valid call to DeepInfra.""" - deepinfra_emb = DeepInfraEmbeddings(model_id="sentence-transformers/clip-ViT-B-32") + deepinfra_emb = DeepInfraEmbeddings(model_id="BAAI/bge-base-en-v1.5") r1 = deepinfra_emb.embed_documents( [ "Alpha is the first letter of Greek alphabet", @@ -13,7 +13,7 @@ def test_deepinfra_call() -> None: ] ) assert len(r1) == 2 - assert len(r1[0]) == 512 - assert len(r1[1]) == 512 + assert len(r1[0]) == 768 + assert len(r1[1]) == 768 r2 = deepinfra_emb.embed_query("What is the third letter of Greek alphabet") - assert len(r2) == 512 + assert len(r2) == 768 diff --git a/libs/community/tests/integration_tests/graphs/test_memgraph.py b/libs/community/tests/integration_tests/graphs/test_memgraph.py new file mode 100644 index 0000000000000..663f974d3f106 --- /dev/null +++ b/libs/community/tests/integration_tests/graphs/test_memgraph.py @@ -0,0 +1,62 @@ +import os + +from langchain_community.graphs import MemgraphGraph + + +def test_cypher_return_correct_schema() -> None: + """Test that chain returns direct results.""" + url = os.environ.get("MEMGRAPH_URI", "bolt://localhost:7687") + username = os.environ.get("MEMGRAPH_USERNAME", "") + password = os.environ.get("MEMGRAPH_PASSWORD", "") + assert url is not None + assert username is not None + assert password is not None + + graph = MemgraphGraph( + url=url, + username=username, + password=password, + ) + # Delete all nodes in the graph + graph.query("MATCH (n) DETACH DELETE n") + # Create two nodes and a relationship + graph.query( + """ + CREATE (la:LabelA {property_a: 'a'}) + CREATE (lb:LabelB) + CREATE (lc:LabelC) + MERGE (la)-[:REL_TYPE]-> (lb) + MERGE (la)-[:REL_TYPE {rel_prop: 'abc'}]-> (lc) + """ + ) + # Refresh schema information + graph.refresh_schema() + relationships = graph.query( + "CALL llm_util.schema('raw') YIELD schema " + "WITH schema.relationships AS relationships " + "UNWIND relationships AS relationship " + "RETURN relationship['start'] AS start, " + "relationship['type'] AS type, " + "relationship['end'] AS end " + "ORDER BY start, type, end;" + ) + + node_props = graph.query( + "CALL llm_util.schema('raw') YIELD schema " + "WITH schema.node_props AS nodes " + "WITH nodes['LabelA'] AS properties " + "UNWIND properties AS property " + "RETURN property['property'] AS prop, " + "property['type'] AS type " + "ORDER BY prop ASC;" + ) + + expected_relationships = [ + {"start": "LabelA", "type": "REL_TYPE", "end": "LabelB"}, + {"start": "LabelA", "type": "REL_TYPE", "end": "LabelC"}, + ] + + expected_node_props = [{"prop": "property_a", "type": "str"}] + + assert relationships == expected_relationships + assert node_props == expected_node_props diff --git a/libs/community/tests/integration_tests/graphs/test_neo4j.py b/libs/community/tests/integration_tests/graphs/test_neo4j.py index fd9e3bb36f786..e4b47f907ead8 100644 --- a/libs/community/tests/integration_tests/graphs/test_neo4j.py +++ b/libs/community/tests/integration_tests/graphs/test_neo4j.py @@ -69,3 +69,50 @@ def test_cypher_return_correct_schema() -> None: sorted(relationships, key=lambda x: x["output"]["end"]) == expected_relationships ) + + +def test_neo4j_timeout() -> None: + """Test that neo4j uses the timeout correctly.""" + url = os.environ.get("NEO4J_URI") + username = os.environ.get("NEO4J_USERNAME") + password = os.environ.get("NEO4J_PASSWORD") + assert url is not None + assert username is not None + assert password is not None + + graph = Neo4jGraph(url=url, username=username, password=password, timeout=0.1) + try: + graph.query("UNWIND range(0,100000,1) AS i MERGE (:Foo {id:i})") + except Exception as e: + assert ( + e.code + == "Neo.ClientError.Transaction.TransactionTimedOutClientConfiguration" + ) + + +def test_neo4j_sanitize_values() -> None: + """Test that neo4j uses the timeout correctly.""" + url = os.environ.get("NEO4J_URI") + username = os.environ.get("NEO4J_USERNAME") + password = os.environ.get("NEO4J_PASSWORD") + assert url is not None + assert username is not None + assert password is not None + + graph = Neo4jGraph(url=url, username=username, password=password, sanitize=True) + # Delete all nodes in the graph + graph.query("MATCH (n) DETACH DELETE n") + # Create two nodes and a relationship + graph.query( + """ + CREATE (la:LabelA {property_a: 'a'}) + CREATE (lb:LabelB) + CREATE (lc:LabelC) + MERGE (la)-[:REL_TYPE]-> (lb) + MERGE (la)-[:REL_TYPE {rel_prop: 'abc'}]-> (lc) + """ + ) + graph.refresh_schema() + + output = graph.query("RETURN range(0,130,1) AS result") + assert output == [{}] diff --git a/libs/community/tests/integration_tests/llms/test_azureml_endpoint.py b/libs/community/tests/integration_tests/llms/test_azureml_endpoint.py index a0562e27a14a4..4d0e86b510257 100644 --- a/libs/community/tests/integration_tests/llms/test_azureml_endpoint.py +++ b/libs/community/tests/integration_tests/llms/test_azureml_endpoint.py @@ -7,6 +7,7 @@ from urllib.request import HTTPError import pytest +from langchain_core.pydantic_v1 import ValidationError from langchain_community.llms.azureml_endpoint import ( AzureMLOnlineEndpoint, @@ -26,7 +27,7 @@ def test_gpt2_call() -> None: deployment_name=os.getenv("OSS_DEPLOYMENT_NAME"), content_formatter=OSSContentFormatter(), ) - output = llm("Foo") + output = llm.invoke("Foo") assert isinstance(output, str) @@ -38,7 +39,7 @@ def test_hf_call() -> None: deployment_name=os.getenv("HF_DEPLOYMENT_NAME"), content_formatter=HFContentFormatter(), ) - output = llm("Foo") + output = llm.invoke("Foo") assert isinstance(output, str) @@ -50,7 +51,7 @@ def test_dolly_call() -> None: deployment_name=os.getenv("DOLLY_DEPLOYMENT_NAME"), content_formatter=DollyContentFormatter(), ) - output = llm("Foo") + output = llm.invoke("Foo") assert isinstance(output, str) @@ -81,7 +82,7 @@ def format_response_payload(self, output: bytes) -> str: deployment_name=os.getenv("BART_DEPLOYMENT_NAME"), content_formatter=CustomFormatter(), ) - output = llm("Foo") + output = llm.invoke("Foo") assert isinstance(output, str) @@ -93,7 +94,7 @@ def test_missing_content_formatter() -> None: endpoint_url=os.getenv("OSS_ENDPOINT_URL"), deployment_name=os.getenv("OSS_DEPLOYMENT_NAME"), ) - llm("Foo") + llm.invoke("Foo") def test_invalid_request_format() -> None: @@ -123,7 +124,31 @@ def format_response_payload(self, output: bytes) -> str: deployment_name=os.getenv("OSS_DEPLOYMENT_NAME"), content_formatter=CustomContentFormatter(), ) - llm("Foo") + llm.invoke("Foo") + + +def test_incorrect_url() -> None: + """Testing AzureML Endpoint for an incorrect URL""" + with pytest.raises(ValidationError): + llm = AzureMLOnlineEndpoint( + endpoint_api_key=os.getenv("OSS_ENDPOINT_API_KEY"), + endpoint_url="https://endpoint.inference.com", + deployment_name=os.getenv("OSS_DEPLOYMENT_NAME"), + content_formatter=OSSContentFormatter(), + ) + llm.invoke("Foo") + + +def test_incorrect_api_type() -> None: + with pytest.raises(ValidationError): + llm = AzureMLOnlineEndpoint( + endpoint_api_key=os.getenv("OSS_ENDPOINT_API_KEY"), + endpoint_url=os.getenv("OSS_ENDPOINT_URL"), + deployment_name=os.getenv("OSS_DEPLOYMENT_NAME"), + endpoint_api_type="serverless", + content_formatter=OSSContentFormatter(), + ) + llm.invoke("Foo") def test_incorrect_key() -> None: @@ -135,7 +160,7 @@ def test_incorrect_key() -> None: deployment_name=os.getenv("OSS_DEPLOYMENT_NAME"), content_formatter=OSSContentFormatter(), ) - llm("Foo") + llm.invoke("Foo") def test_saving_loading_llm(tmp_path: Path) -> None: diff --git a/libs/community/tests/integration_tests/llms/test_deepinfra.py b/libs/community/tests/integration_tests/llms/test_deepinfra.py index 08b5e566e8006..54057e657e6ea 100644 --- a/libs/community/tests/integration_tests/llms/test_deepinfra.py +++ b/libs/community/tests/integration_tests/llms/test_deepinfra.py @@ -5,13 +5,13 @@ def test_deepinfra_call() -> None: """Test valid call to DeepInfra.""" llm = DeepInfra(model_id="meta-llama/Llama-2-7b-chat-hf") - output = llm("What is 2 + 2?") + output = llm.invoke("What is 2 + 2?") assert isinstance(output, str) async def test_deepinfra_acall() -> None: llm = DeepInfra(model_id="meta-llama/Llama-2-7b-chat-hf") - output = await llm.apredict("What is 2 + 2?") + output = await llm.ainvoke("What is 2 + 2?") assert llm._llm_type == "deepinfra" assert isinstance(output, str) diff --git a/libs/community/tests/integration_tests/llms/test_konko.py b/libs/community/tests/integration_tests/llms/test_konko.py new file mode 100644 index 0000000000000..3e0fe0f31bb99 --- /dev/null +++ b/libs/community/tests/integration_tests/llms/test_konko.py @@ -0,0 +1,36 @@ +"""Test Konko API wrapper. + +In order to run this test, you need to have an Konko api key. +You'll then need to set KONKO_API_KEY environment variable to your api key. +""" +import pytest as pytest + +from langchain_community.llms import Konko + + +def test_konko_call() -> None: + """Test simple call to konko.""" + llm = Konko( + model="mistralai/mistral-7b-v0.1", + temperature=0.2, + max_tokens=250, + ) + output = llm("Say foo:") + + assert llm._llm_type == "konko" + assert isinstance(output, str) + + +async def test_konko_acall() -> None: + """Test simple call to konko.""" + llm = Konko( + model="mistralai/mistral-7b-v0.1", + temperature=0.2, + max_tokens=250, + ) + output = await llm.agenerate(["Say foo:"], stop=["bar"]) + + assert llm._llm_type == "konko" + output_text = output.generations[0][0].text + assert isinstance(output_text, str) + assert output_text.count("bar") <= 1 diff --git a/libs/community/tests/integration_tests/storage/test_sql.py b/libs/community/tests/integration_tests/storage/test_sql.py new file mode 100644 index 0000000000000..c09b9745c65cd --- /dev/null +++ b/libs/community/tests/integration_tests/storage/test_sql.py @@ -0,0 +1,228 @@ +"""Implement integration tests for SQL storage.""" +import os + +from langchain_core.documents import Document + +from langchain_community.storage.sql import SQLDocStore, SQLStrStore + + +def connection_string_from_db_params() -> str: + """Return connection string from database parameters.""" + dbdriver = os.environ.get("TEST_SQL_DBDRIVER", "postgresql+psycopg2") + host = os.environ.get("TEST_SQL_HOST", "localhost") + port = int(os.environ.get("TEST_SQL_PORT", "5432")) + database = os.environ.get("TEST_SQL_DATABASE", "postgres") + user = os.environ.get("TEST_SQL_USER", "postgres") + password = os.environ.get("TEST_SQL_PASSWORD", "postgres") + return f"{dbdriver}://{user}:{password}@{host}:{port}/{database}" + + +CONNECTION_STRING = connection_string_from_db_params() +COLLECTION_NAME = "test_collection" +COLLECTION_NAME_2 = "test_collection_2" + + +def test_str_store_mget() -> None: + store = SQLStrStore( + collection_name=COLLECTION_NAME, + connection_string=CONNECTION_STRING, + pre_delete_collection=True, + ) + store.mset([("key1", "value1"), ("key2", "value2")]) + + values = store.mget(["key1", "key2"]) + assert values == ["value1", "value2"] + + # Test non-existent key + non_existent_value = store.mget(["key3"]) + assert non_existent_value == [None] + store.delete_collection() + + +def test_str_store_mset() -> None: + store = SQLStrStore( + collection_name=COLLECTION_NAME, + connection_string=CONNECTION_STRING, + pre_delete_collection=True, + ) + store.mset([("key1", "value1"), ("key2", "value2")]) + + values = store.mget(["key1", "key2"]) + assert values == ["value1", "value2"] + store.delete_collection() + + +def test_str_store_mdelete() -> None: + store = SQLStrStore( + collection_name=COLLECTION_NAME, + connection_string=CONNECTION_STRING, + pre_delete_collection=True, + ) + store.mset([("key1", "value1"), ("key2", "value2")]) + + store.mdelete(["key1"]) + + values = store.mget(["key1", "key2"]) + assert values == [None, "value2"] + + # Test deleting non-existent key + store.mdelete(["key3"]) # No error should be raised + store.delete_collection() + + +def test_str_store_yield_keys() -> None: + store = SQLStrStore( + collection_name=COLLECTION_NAME, + connection_string=CONNECTION_STRING, + pre_delete_collection=True, + ) + store.mset([("key1", "value1"), ("key2", "value2"), ("key3", "value3")]) + + keys = list(store.yield_keys()) + assert set(keys) == {"key1", "key2", "key3"} + + keys_with_prefix = list(store.yield_keys(prefix="key")) + assert set(keys_with_prefix) == {"key1", "key2", "key3"} + + keys_with_invalid_prefix = list(store.yield_keys(prefix="x")) + assert keys_with_invalid_prefix == [] + store.delete_collection() + + +def test_str_store_collection() -> None: + """Test that collections are isolated within a db.""" + store_1 = SQLStrStore( + collection_name=COLLECTION_NAME, + connection_string=CONNECTION_STRING, + pre_delete_collection=True, + ) + store_2 = SQLStrStore( + collection_name=COLLECTION_NAME_2, + connection_string=CONNECTION_STRING, + pre_delete_collection=True, + ) + + store_1.mset([("key1", "value1"), ("key2", "value2")]) + store_2.mset([("key3", "value3"), ("key4", "value4")]) + + values = store_1.mget(["key1", "key2"]) + assert values == ["value1", "value2"] + values = store_1.mget(["key3", "key4"]) + assert values == [None, None] + + values = store_2.mget(["key1", "key2"]) + assert values == [None, None] + values = store_2.mget(["key3", "key4"]) + assert values == ["value3", "value4"] + + store_1.delete_collection() + store_2.delete_collection() + + +def test_doc_store_mget() -> None: + store = SQLDocStore( + collection_name=COLLECTION_NAME, + connection_string=CONNECTION_STRING, + pre_delete_collection=True, + ) + doc_1 = Document(page_content="value1") + doc_2 = Document(page_content="value2") + store.mset([("key1", doc_1), ("key2", doc_2)]) + + values = store.mget(["key1", "key2"]) + assert values == [doc_1, doc_2] + + # Test non-existent key + non_existent_value = store.mget(["key3"]) + assert non_existent_value == [None] + store.delete_collection() + + +def test_doc_store_mset() -> None: + store = SQLDocStore( + collection_name=COLLECTION_NAME, + connection_string=CONNECTION_STRING, + pre_delete_collection=True, + ) + doc_1 = Document(page_content="value1") + doc_2 = Document(page_content="value2") + store.mset([("key1", doc_1), ("key2", doc_2)]) + + values = store.mget(["key1", "key2"]) + assert values == [doc_1, doc_2] + store.delete_collection() + + +def test_doc_store_mdelete() -> None: + store = SQLDocStore( + collection_name=COLLECTION_NAME, + connection_string=CONNECTION_STRING, + pre_delete_collection=True, + ) + doc_1 = Document(page_content="value1") + doc_2 = Document(page_content="value2") + store.mset([("key1", doc_1), ("key2", doc_2)]) + + store.mdelete(["key1"]) + + values = store.mget(["key1", "key2"]) + assert values == [None, doc_2] + + # Test deleting non-existent key + store.mdelete(["key3"]) # No error should be raised + store.delete_collection() + + +def test_doc_store_yield_keys() -> None: + store = SQLDocStore( + collection_name=COLLECTION_NAME, + connection_string=CONNECTION_STRING, + pre_delete_collection=True, + ) + doc_1 = Document(page_content="value1") + doc_2 = Document(page_content="value2") + doc_3 = Document(page_content="value3") + store.mset([("key1", doc_1), ("key2", doc_2), ("key3", doc_3)]) + + keys = list(store.yield_keys()) + assert set(keys) == {"key1", "key2", "key3"} + + keys_with_prefix = list(store.yield_keys(prefix="key")) + assert set(keys_with_prefix) == {"key1", "key2", "key3"} + + keys_with_invalid_prefix = list(store.yield_keys(prefix="x")) + assert keys_with_invalid_prefix == [] + store.delete_collection() + + +def test_doc_store_collection() -> None: + """Test that collections are isolated within a db.""" + store_1 = SQLDocStore( + collection_name=COLLECTION_NAME, + connection_string=CONNECTION_STRING, + pre_delete_collection=True, + ) + store_2 = SQLDocStore( + collection_name=COLLECTION_NAME_2, + connection_string=CONNECTION_STRING, + pre_delete_collection=True, + ) + doc_1 = Document(page_content="value1") + doc_2 = Document(page_content="value2") + doc_3 = Document(page_content="value3") + doc_4 = Document(page_content="value4") + store_1.mset([("key1", doc_1), ("key2", doc_2)]) + store_2.mset([("key3", doc_3), ("key4", doc_4)]) + + values = store_1.mget(["key1", "key2"]) + assert values == [doc_1, doc_2] + values = store_1.mget(["key3", "key4"]) + assert values == [None, None] + + values = store_2.mget(["key1", "key2"]) + assert values == [None, None] + values = store_2.mget(["key3", "key4"]) + assert values == [doc_3, doc_4] + + store_1.delete_collection() + store_2.delete_collection() diff --git a/libs/community/tests/integration_tests/utilities/test_google_trends.py b/libs/community/tests/integration_tests/utilities/test_google_trends.py new file mode 100644 index 0000000000000..0455f16a58060 --- /dev/null +++ b/libs/community/tests/integration_tests/utilities/test_google_trends.py @@ -0,0 +1,33 @@ +"""Unit test for Google Trends API Wrapper.""" +import os +from unittest.mock import patch + +from langchain_community.utilities.google_trends import GoogleTrendsAPIWrapper + + +@patch("serpapi.SerpApiClient.get_json") +def test_unexpected_response(mocked_serpapiclient): + os.environ["SERPAPI_API_KEY"] = "123abcd" + resp = { + "search_metadata": { + "id": "659f32ec36e6a9107b46b5b4", + "status": "Error", + "json_endpoint": "https://serpapi.com/searches/.../659f32ec36e6a9107b46b5b4.json", + "created_at": "2024-01-11 00:14:36 UTC", + "processed_at": "2024-01-11 00:14:36 UTC", + "google_trends_url": "https://trends.google.com/trends/api/explore?tz=420&req=%7B%22comparisonItem%22%3A%5B%7B%22keyword%22%3A%22Lego+building+trends+2022%22%2C%22geo%22%3A%22%22%2C%22time%22%3A%22today+12-m%22%7D%5D%2C%22category%22%3A0%2C%22property%22%3A%22%22%2C%22userConfig%22%3A%22%7BuserType%3A+%5C%22USER_TYPE_LEGIT_USER%5C%22%7D%22%7D", + "prettify_html_file": "https://serpapi.com/searches/.../659f32ec36e6a9107b46b5b4.prettify", + "total_time_taken": 90.14, + }, + "search_parameters": { + "engine": "google_trends", + "q": "Lego building trends 2022", + "date": "today 12-m", + "tz": "420", + "data_type": "TIMESERIES", + }, + "error": "We couldn't get valid ... Please try again later.", + } + mocked_serpapiclient.return_value = resp + tool = GoogleTrendsAPIWrapper() + tool.run("does not matter") diff --git a/libs/community/tests/integration_tests/vectorstores/test_elasticsearch.py b/libs/community/tests/integration_tests/vectorstores/test_elasticsearch.py index 8d99fbe5ead77..ff13a96dc8a15 100644 --- a/libs/community/tests/integration_tests/vectorstores/test_elasticsearch.py +++ b/libs/community/tests/integration_tests/vectorstores/test_elasticsearch.py @@ -157,7 +157,7 @@ def assert_query(query_body: dict, query: str) -> dict: output = docsearch.similarity_search("foo", k=1, custom_query=assert_query) assert output == [Document(page_content="foo")] - async def test_similarity_search_without_metadat_async( + async def test_similarity_search_without_metadata_async( self, elasticsearch_connection: dict, index_name: str ) -> None: """Test end to end construction and search without metadata.""" @@ -400,7 +400,7 @@ def assert_query(query_body: dict, query: str) -> dict: "script": { "source": """ double value = dotProduct(params.query_vector, 'vector'); - return sigmoid(1, Math.E, -value); + return sigmoid(1, Math.E, -value); """, "params": { "query_vector": [ @@ -777,6 +777,44 @@ def test_elasticsearch_with_relevance_score( ) assert output == [(Document(page_content="foo", metadata={"page": "0"}), 1.0)] + def test_elasticsearch_with_relevance_threshold( + self, elasticsearch_connection: dict, index_name: str + ) -> None: + """Test to make sure the relevance threshold is respected.""" + texts = ["foo", "bar", "baz"] + metadatas = [{"page": str(i)} for i in range(len(texts))] + embeddings = FakeEmbeddings() + + docsearch = ElasticsearchStore.from_texts( + index_name=index_name, + texts=texts, + embedding=embeddings, + metadatas=metadatas, + **elasticsearch_connection, + ) + + # Find a good threshold for testing + query_string = "foo" + embedded_query = embeddings.embed_query(query_string) + top3 = docsearch.similarity_search_by_vector_with_relevance_scores( + embedding=embedded_query, k=3 + ) + similarity_of_second_ranked = top3[1][1] + assert len(top3) == 3 + + # Test threshold + retriever = docsearch.as_retriever( + search_type="similarity_score_threshold", + search_kwargs={"score_threshold": similarity_of_second_ranked}, + ) + output = retriever.get_relevant_documents(query=query_string) + + assert output == [ + top3[0][0], + top3[1][0], + # third ranked is out + ] + def test_elasticsearch_delete_ids( self, elasticsearch_connection: dict, index_name: str ) -> None: diff --git a/libs/community/tests/unit_tests/agent_toolkits/test_imports.py b/libs/community/tests/unit_tests/agent_toolkits/test_imports.py index c6557b7391ee8..416b66849b38f 100644 --- a/libs/community/tests/unit_tests/agent_toolkits/test_imports.py +++ b/libs/community/tests/unit_tests/agent_toolkits/test_imports.py @@ -14,6 +14,7 @@ "O365Toolkit", "OpenAPIToolkit", "PlayWrightBrowserToolkit", + "PolygonToolkit", "PowerBIToolkit", "SlackToolkit", "SteamToolkit", diff --git a/libs/community/tests/unit_tests/callbacks/test_callback_manager.py b/libs/community/tests/unit_tests/callbacks/test_callback_manager.py index b02b7c57dfad0..353a72c85b0f5 100644 --- a/libs/community/tests/unit_tests/callbacks/test_callback_manager.py +++ b/libs/community/tests/unit_tests/callbacks/test_callback_manager.py @@ -16,6 +16,7 @@ def test_callback_manager_configure_context_vars( """Test callback manager configuration.""" monkeypatch.setenv("LANGCHAIN_TRACING_V2", "true") monkeypatch.setenv("LANGCHAIN_TRACING", "false") + monkeypatch.setenv("LANGCHAIN_API_KEY", "foo") with patch.object(LangChainTracer, "_update_run_single"): with patch.object(LangChainTracer, "_persist_run_single"): with trace_as_chain_group("test") as group_manager: diff --git a/libs/community/tests/unit_tests/chat_models/konko.py b/libs/community/tests/unit_tests/chat_models/konko.py new file mode 100644 index 0000000000000..2fca6e67cb5c2 --- /dev/null +++ b/libs/community/tests/unit_tests/chat_models/konko.py @@ -0,0 +1,174 @@ +"""Evaluate ChatKonko Interface.""" +from typing import Any + +import pytest +from langchain_core.callbacks import CallbackManager +from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage +from langchain_core.outputs import ChatGeneration, ChatResult, LLMResult + +from langchain_community.chat_models.konko import ChatKonko +from tests.unit_tests.callbacks.fake_callback_handler import FakeCallbackHandler + + +def test_konko_chat_test() -> None: + """Evaluate basic ChatKonko functionality.""" + chat_instance = ChatKonko(max_tokens=10) + msg = HumanMessage(content="Hi") + chat_response = chat_instance([msg]) + assert isinstance(chat_response, BaseMessage) + assert isinstance(chat_response.content, str) + + +def test_konko_chat_test_openai() -> None: + """Evaluate basic ChatKonko functionality.""" + chat_instance = ChatKonko(max_tokens=10, model="meta-llama/llama-2-70b-chat") + msg = HumanMessage(content="Hi") + chat_response = chat_instance([msg]) + assert isinstance(chat_response, BaseMessage) + assert isinstance(chat_response.content, str) + + +def test_konko_model_test() -> None: + """Check how ChatKonko manages model_name.""" + chat_instance = ChatKonko(model="alpha") + assert chat_instance.model == "alpha" + chat_instance = ChatKonko(model="beta") + assert chat_instance.model == "beta" + + +def test_konko_available_model_test() -> None: + """Check how ChatKonko manages model_name.""" + chat_instance = ChatKonko(max_tokens=10, n=2) + res = chat_instance.get_available_models() + assert isinstance(res, set) + + +def test_konko_system_msg_test() -> None: + """Evaluate ChatKonko's handling of system messages.""" + chat_instance = ChatKonko(max_tokens=10) + sys_msg = SystemMessage(content="Initiate user chat.") + user_msg = HumanMessage(content="Hi there") + chat_response = chat_instance([sys_msg, user_msg]) + assert isinstance(chat_response, BaseMessage) + assert isinstance(chat_response.content, str) + + +def test_konko_generation_test() -> None: + """Check ChatKonko's generation ability.""" + chat_instance = ChatKonko(max_tokens=10, n=2) + msg = HumanMessage(content="Hi") + gen_response = chat_instance.generate([[msg], [msg]]) + assert isinstance(gen_response, LLMResult) + assert len(gen_response.generations) == 2 + for gen_list in gen_response.generations: + assert len(gen_list) == 2 + for gen in gen_list: + assert isinstance(gen, ChatGeneration) + assert isinstance(gen.text, str) + assert gen.text == gen.message.content + + +def test_konko_multiple_outputs_test() -> None: + """Test multiple completions with ChatKonko.""" + chat_instance = ChatKonko(max_tokens=10, n=5) + msg = HumanMessage(content="Hi") + gen_response = chat_instance._generate([msg]) + assert isinstance(gen_response, ChatResult) + assert len(gen_response.generations) == 5 + for gen in gen_response.generations: + assert isinstance(gen.message, BaseMessage) + assert isinstance(gen.message.content, str) + + +def test_konko_streaming_callback_test() -> None: + """Evaluate streaming's token callback functionality.""" + callback_instance = FakeCallbackHandler() + callback_mgr = CallbackManager([callback_instance]) + chat_instance = ChatKonko( + max_tokens=10, + streaming=True, + temperature=0, + callback_manager=callback_mgr, + verbose=True, + ) + msg = HumanMessage(content="Hi") + chat_response = chat_instance([msg]) + assert callback_instance.llm_streams > 0 + assert isinstance(chat_response, BaseMessage) + + +def test_konko_streaming_info_test() -> None: + """Ensure generation details are retained during streaming.""" + + class TestCallback(FakeCallbackHandler): + data_store: dict = {} + + def on_llm_end(self, *args: Any, **kwargs: Any) -> Any: + self.data_store["generation"] = args[0] + + callback_instance = TestCallback() + callback_mgr = CallbackManager([callback_instance]) + chat_instance = ChatKonko( + max_tokens=2, + temperature=0, + callback_manager=callback_mgr, + ) + list(chat_instance.stream("hey")) + gen_data = callback_instance.data_store["generation"] + assert gen_data.generations[0][0].text == " Hey" + + +def test_konko_llm_model_name_test() -> None: + """Check if llm_output has model info.""" + chat_instance = ChatKonko(max_tokens=10) + msg = HumanMessage(content="Hi") + llm_data = chat_instance.generate([[msg]]) + assert llm_data.llm_output is not None + assert llm_data.llm_output["model_name"] == chat_instance.model + + +def test_konko_streaming_model_name_test() -> None: + """Check model info during streaming.""" + chat_instance = ChatKonko(max_tokens=10, streaming=True) + msg = HumanMessage(content="Hi") + llm_data = chat_instance.generate([[msg]]) + assert llm_data.llm_output is not None + assert llm_data.llm_output["model_name"] == chat_instance.model + + +def test_konko_streaming_param_validation_test() -> None: + """Ensure correct token callback during streaming.""" + with pytest.raises(ValueError): + ChatKonko( + max_tokens=10, + streaming=True, + temperature=0, + n=5, + ) + + +def test_konko_additional_args_test() -> None: + """Evaluate extra arguments for ChatKonko.""" + chat_instance = ChatKonko(extra=3, max_tokens=10) + assert chat_instance.max_tokens == 10 + assert chat_instance.model_kwargs == {"extra": 3} + + chat_instance = ChatKonko(extra=3, model_kwargs={"addition": 2}) + assert chat_instance.model_kwargs == {"extra": 3, "addition": 2} + + with pytest.raises(ValueError): + ChatKonko(extra=3, model_kwargs={"extra": 2}) + + with pytest.raises(ValueError): + ChatKonko(model_kwargs={"temperature": 0.2}) + + with pytest.raises(ValueError): + ChatKonko(model_kwargs={"model": "text-davinci-003"}) + + +def test_konko_token_streaming_test() -> None: + """Check token streaming for ChatKonko.""" + chat_instance = ChatKonko(max_tokens=10) + + for token in chat_instance.stream("Just a test"): + assert isinstance(token.content, str) diff --git a/libs/community/tests/unit_tests/chat_models/test_baichuan.py b/libs/community/tests/unit_tests/chat_models/test_baichuan.py index 6a4b4d2009cf4..f5664c88ffe56 100644 --- a/libs/community/tests/unit_tests/chat_models/test_baichuan.py +++ b/libs/community/tests/unit_tests/chat_models/test_baichuan.py @@ -18,7 +18,6 @@ _convert_delta_to_message_chunk, _convert_dict_to_message, _convert_message_to_dict, - _signature, ) @@ -85,62 +84,33 @@ def test__convert_delta_to_message_human() -> None: assert result == expected_output -def test__signature() -> None: - secret_key = SecretStr("YOUR_SECRET_KEY") - - result = _signature( - secret_key=secret_key, - payload={ - "model": "Baichuan2-53B", - "messages": [{"role": "user", "content": "Hi"}], - }, - timestamp=1697734335, - ) - - # The signature was generated by the demo provided by Baichuan. - # https://platform.baichuan-ai.com/docs/api#4 - expected_output = "24a50b2db1648e25a244c67c5ab57d3f" - assert result == expected_output - - def test_baichuan_key_masked_when_passed_from_env( monkeypatch: MonkeyPatch, capsys: CaptureFixture ) -> None: """Test initialization with an API key provided via an env variable""" monkeypatch.setenv("BAICHUAN_API_KEY", "test-api-key") - monkeypatch.setenv("BAICHUAN_SECRET_KEY", "test-secret-key") chat = ChatBaichuan() print(chat.baichuan_api_key, end="") captured = capsys.readouterr() assert captured.out == "**********" - print(chat.baichuan_secret_key, end="") - captured = capsys.readouterr() - assert captured.out == "**********" - def test_baichuan_key_masked_when_passed_via_constructor( capsys: CaptureFixture, ) -> None: """Test initialization with an API key provided via the initializer""" - chat = ChatBaichuan( - baichuan_api_key="test-api-key", baichuan_secret_key="test-secret-key" - ) + chat = ChatBaichuan(baichuan_api_key="test-api-key") print(chat.baichuan_api_key, end="") captured = capsys.readouterr() assert captured.out == "**********" - print(chat.baichuan_secret_key, end="") - captured = capsys.readouterr() - - assert captured.out == "**********" - def test_uses_actual_secret_value_from_secret_str() -> None: """Test that actual secret is retrieved using `.get_secret_value()`.""" chat = ChatBaichuan( - baichuan_api_key="test-api-key", baichuan_secret_key="test-secret-key" + baichuan_api_key="test-api-key", + baichuan_secret_key="test-secret-key", # For backward compatibility ) assert cast(SecretStr, chat.baichuan_api_key).get_secret_value() == "test-api-key" assert ( diff --git a/libs/community/tests/unit_tests/chat_models/test_bedrock.py b/libs/community/tests/unit_tests/chat_models/test_bedrock.py index 93f03b8ecedc1..b515c99e5ad80 100644 --- a/libs/community/tests/unit_tests/chat_models/test_bedrock.py +++ b/libs/community/tests/unit_tests/chat_models/test_bedrock.py @@ -37,17 +37,24 @@ def test_formatting(messages: List[BaseMessage], expected: str) -> None: assert result == expected -def test_anthropic_bedrock() -> None: +@pytest.mark.parametrize( + "model_id", + ["anthropic.claude-v2", "amazon.titan-text-express-v1"], +) +def test_different_models_bedrock(model_id: str) -> None: + provider = model_id.split(".")[0] client = MagicMock() - respbody = MagicMock( - read=MagicMock( - return_value=MagicMock( - decode=MagicMock(return_value=b'{"completion":"Hi back"}') - ) + respbody = MagicMock() + if provider == "anthropic": + respbody.read.return_value = MagicMock( + decode=MagicMock(return_value=b'{"completion":"Hi back"}'), ) - ) - client.invoke_model.return_value = {"body": respbody} - model = BedrockChat(model_id="anthropic.claude-v2", client=client) + client.invoke_model.return_value = {"body": respbody} + elif provider == "amazon": + respbody.read.return_value = '{"results": [{"outputText": "Hi back"}]}' + client.invoke_model.return_value = {"body": respbody} + + model = BedrockChat(model_id=model_id, client=client) # should not throw an error model.invoke("hello there") diff --git a/libs/community/tests/unit_tests/chat_models/test_imports.py b/libs/community/tests/unit_tests/chat_models/test_imports.py index 031fb96e8937f..29d3529f7545e 100644 --- a/libs/community/tests/unit_tests/chat_models/test_imports.py +++ b/libs/community/tests/unit_tests/chat_models/test_imports.py @@ -10,6 +10,7 @@ "ChatAnthropic", "ChatCohere", "ChatDatabricks", + "ChatDeepInfra", "ChatGooglePalm", "ChatHuggingFace", "ChatMlflow", @@ -32,6 +33,7 @@ "ChatBaichuan", "ChatHunyuan", "GigaChat", + "ChatSparkLLM", "VolcEngineMaasChat", "LlamaEdgeChatService", "GPTRouter", diff --git a/libs/community/tests/unit_tests/document_loaders/parsers/test_public_api.py b/libs/community/tests/unit_tests/document_loaders/parsers/test_public_api.py index 3dc19adb31076..efaf28ecc89ce 100644 --- a/libs/community/tests/unit_tests/document_loaders/parsers/test_public_api.py +++ b/libs/community/tests/unit_tests/document_loaders/parsers/test_public_api.py @@ -15,4 +15,5 @@ def test_parsers_public_api_correct() -> None: "PyMuPDFParser", "PyPDFium2Parser", "PDFPlumberParser", + "VsdxParser", } diff --git a/libs/community/tests/unit_tests/document_loaders/parsers/test_vsdx_parser.py b/libs/community/tests/unit_tests/document_loaders/parsers/test_vsdx_parser.py new file mode 100644 index 0000000000000..70cbba035154b --- /dev/null +++ b/libs/community/tests/unit_tests/document_loaders/parsers/test_vsdx_parser.py @@ -0,0 +1,51 @@ +"""Tests for the VSDX parsers.""" +from pathlib import Path +from typing import Iterator + +import pytest + +from langchain_community.document_loaders.base import BaseBlobParser +from langchain_community.document_loaders.blob_loaders import Blob +from langchain_community.document_loaders.parsers import VsdxParser + +_THIS_DIR = Path(__file__).parents[3] + +_EXAMPLES_DIR = _THIS_DIR / "examples" + +# Paths to test VSDX file +FAKE_FILE = _EXAMPLES_DIR / "fake.vsdx" + + +def _assert_with_parser(parser: BaseBlobParser, splits_by_page: bool = True) -> None: + """Standard tests to verify that the given parser works. + + Args: + parser (BaseBlobParser): The parser to test. + splits_by_page (bool): Whether the parser splits by page or not by default. + """ + + blob = Blob.from_path(FAKE_FILE) + doc_generator = parser.lazy_parse(blob) + assert isinstance(doc_generator, Iterator) + docs = list(doc_generator) + + if splits_by_page: + assert len(docs) == 14 + else: + assert len(docs) == 1 + # Test is imprecise since the parsers yield different parse information depending + # on configuration. Each parser seems to yield a slightly different result + # for this page! + assert "This is a title" in docs[0].page_content + metadata = docs[0].metadata + + assert metadata["source"] == str(FAKE_FILE) + + if splits_by_page: + assert int(metadata["page"]) == 0 + + +@pytest.mark.requires("xmltodict") +def test_vsdx_parser() -> None: + """Test the VSDX parser.""" + _assert_with_parser(VsdxParser()) diff --git a/libs/community/tests/unit_tests/document_loaders/test_imports.py b/libs/community/tests/unit_tests/document_loaders/test_imports.py index d730f6bfc19a3..e50342a9a0a20 100644 --- a/libs/community/tests/unit_tests/document_loaders/test_imports.py +++ b/libs/community/tests/unit_tests/document_loaders/test_imports.py @@ -132,6 +132,7 @@ "SnowflakeLoader", "SpreedlyLoader", "StripeLoader", + "SurrealDBLoader", "TelegramChatApiLoader", "TelegramChatFileLoader", "TelegramChatLoader", @@ -164,6 +165,7 @@ "UnstructuredURLLoader", "UnstructuredWordDocumentLoader", "UnstructuredXMLLoader", + "VsdxLoader", "WeatherDataLoader", "WebBaseLoader", "WhatsAppChatLoader", diff --git a/libs/community/tests/unit_tests/embeddings/test_huggingface.py b/libs/community/tests/unit_tests/embeddings/test_huggingface.py new file mode 100644 index 0000000000000..22f91acb8aaf6 --- /dev/null +++ b/libs/community/tests/unit_tests/embeddings/test_huggingface.py @@ -0,0 +1,7 @@ +from langchain_community.embeddings.huggingface import HuggingFaceInferenceAPIEmbeddings + + +def test_hugginggface_inferenceapi_embedding_documents_init() -> None: + """Test huggingface embeddings.""" + embedding = HuggingFaceInferenceAPIEmbeddings(api_key="abcd123") + assert "abcd123" not in repr(embedding) diff --git a/libs/community/tests/unit_tests/embeddings/test_imports.py b/libs/community/tests/unit_tests/embeddings/test_imports.py index 6aac6609a9968..dee9b1ba836fd 100644 --- a/libs/community/tests/unit_tests/embeddings/test_imports.py +++ b/libs/community/tests/unit_tests/embeddings/test_imports.py @@ -18,6 +18,7 @@ "HuggingFaceHubEmbeddings", "MlflowAIGatewayEmbeddings", "MlflowEmbeddings", + "MlflowCohereEmbeddings", "ModelScopeEmbeddings", "TensorflowHubEmbeddings", "SagemakerEndpointEmbeddings", diff --git a/libs/community/tests/unit_tests/graphs/test_imports.py b/libs/community/tests/unit_tests/graphs/test_imports.py index c50dfe769b0b9..fb98e973d0792 100644 --- a/libs/community/tests/unit_tests/graphs/test_imports.py +++ b/libs/community/tests/unit_tests/graphs/test_imports.py @@ -11,6 +11,7 @@ "RdfGraph", "ArangoGraph", "FalkorDBGraph", + "TigerGraph", ] diff --git a/libs/community/tests/unit_tests/graphs/test_neo4j_graph.py b/libs/community/tests/unit_tests/graphs/test_neo4j_graph.py new file mode 100644 index 0000000000000..b352529ba6734 --- /dev/null +++ b/libs/community/tests/unit_tests/graphs/test_neo4j_graph.py @@ -0,0 +1,32 @@ +from langchain_community.graphs.neo4j_graph import value_sanitize + + +def test_value_sanitize_with_small_list(): + small_list = list(range(15)) # list size > LIST_LIMIT + input_dict = {"key1": "value1", "small_list": small_list} + expected_output = {"key1": "value1", "small_list": small_list} + assert value_sanitize(input_dict) == expected_output + + +def test_value_sanitize_with_oversized_list(): + oversized_list = list(range(150)) # list size > LIST_LIMIT + input_dict = {"key1": "value1", "oversized_list": oversized_list} + expected_output = { + "key1": "value1" + # oversized_list should not be included + } + assert value_sanitize(input_dict) == expected_output + + +def test_value_sanitize_with_nested_oversized_list(): + oversized_list = list(range(150)) # list size > LIST_LIMIT + input_dict = {"key1": "value1", "oversized_list": {"key": oversized_list}} + expected_output = {"key1": "value1", "oversized_list": {}} + assert value_sanitize(input_dict) == expected_output + + +def test_value_sanitize_with_dict_in_list(): + oversized_list = list(range(150)) # list size > LIST_LIMIT + input_dict = {"key1": "value1", "oversized_list": [1, 2, {"key": oversized_list}]} + expected_output = {"key1": "value1", "oversized_list": [1, 2, {}]} + assert value_sanitize(input_dict) == expected_output diff --git a/libs/community/tests/unit_tests/llms/konko.py b/libs/community/tests/unit_tests/llms/konko.py new file mode 100644 index 0000000000000..3e0fe0f31bb99 --- /dev/null +++ b/libs/community/tests/unit_tests/llms/konko.py @@ -0,0 +1,36 @@ +"""Test Konko API wrapper. + +In order to run this test, you need to have an Konko api key. +You'll then need to set KONKO_API_KEY environment variable to your api key. +""" +import pytest as pytest + +from langchain_community.llms import Konko + + +def test_konko_call() -> None: + """Test simple call to konko.""" + llm = Konko( + model="mistralai/mistral-7b-v0.1", + temperature=0.2, + max_tokens=250, + ) + output = llm("Say foo:") + + assert llm._llm_type == "konko" + assert isinstance(output, str) + + +async def test_konko_acall() -> None: + """Test simple call to konko.""" + llm = Konko( + model="mistralai/mistral-7b-v0.1", + temperature=0.2, + max_tokens=250, + ) + output = await llm.agenerate(["Say foo:"], stop=["bar"]) + + assert llm._llm_type == "konko" + output_text = output.generations[0][0].text + assert isinstance(output_text, str) + assert output_text.count("bar") <= 1 diff --git a/libs/community/tests/unit_tests/llms/test_bedrock.py b/libs/community/tests/unit_tests/llms/test_bedrock.py index 12467b4f42a43..60012182936cc 100644 --- a/libs/community/tests/unit_tests/llms/test_bedrock.py +++ b/libs/community/tests/unit_tests/llms/test_bedrock.py @@ -1,6 +1,14 @@ +import json +from typing import AsyncGenerator, Dict +from unittest.mock import MagicMock, patch + import pytest -from langchain_community.llms.bedrock import ALTERNATION_ERROR, _human_assistant_format +from langchain_community.llms.bedrock import ( + ALTERNATION_ERROR, + Bedrock, + _human_assistant_format, +) TEST_CASES = { """Hey""": """ @@ -250,3 +258,51 @@ def test__human_assistant_format() -> None: else: output = _human_assistant_format(input_text) assert output == expected_output + + +# Sample mock streaming response data +MOCK_STREAMING_RESPONSE = [ + {"chunk": {"bytes": b'{"text": "nice"}'}}, + {"chunk": {"bytes": b'{"text": " to meet"}'}}, + {"chunk": {"bytes": b'{"text": " you"}'}}, +] + + +async def async_gen_mock_streaming_response() -> AsyncGenerator[Dict, None]: + for item in MOCK_STREAMING_RESPONSE: + yield item + + +@pytest.mark.asyncio +async def test_bedrock_async_streaming_call() -> None: + # Mock boto3 import + mock_boto3 = MagicMock() + mock_boto3.Session.return_value.client.return_value = ( + MagicMock() + ) # Mocking the client method of the Session object + + with patch.dict( + "sys.modules", {"boto3": mock_boto3} + ): # Mocking boto3 at the top level using patch.dict + # Mock the `Bedrock` class's method that invokes the model + mock_invoke_method = MagicMock(return_value=async_gen_mock_streaming_response()) + with patch.object( + Bedrock, "_aprepare_input_and_invoke_stream", mock_invoke_method + ): + # Instantiate the Bedrock LLM + llm = Bedrock( + client=None, + model_id="anthropic.claude-v2", + streaming=True, + ) + # Call the _astream method + chunks = [ + json.loads(chunk["chunk"]["bytes"])["text"] # type: ignore + async for chunk in llm._astream("Hey, how are you?") + ] + + # Assertions + assert len(chunks) == 3 + assert chunks[0] == "nice" + assert chunks[1] == " to meet" + assert chunks[2] == " you" diff --git a/libs/community/tests/unit_tests/llms/test_imports.py b/libs/community/tests/unit_tests/llms/test_imports.py index 7bb66ff341b8d..2e5fed3a70c94 100644 --- a/libs/community/tests/unit_tests/llms/test_imports.py +++ b/libs/community/tests/unit_tests/llms/test_imports.py @@ -41,6 +41,7 @@ "HuggingFaceTextGenInference", "HumanInputLLM", "KoboldApiLLM", + "Konko", "LlamaCpp", "TextGen", "ManifestWrapper", diff --git a/libs/community/tests/unit_tests/llms/test_ollama.py b/libs/community/tests/unit_tests/llms/test_ollama.py index ff18daacfa64c..bf2229b4fcdc7 100644 --- a/libs/community/tests/unit_tests/llms/test_ollama.py +++ b/libs/community/tests/unit_tests/llms/test_ollama.py @@ -31,7 +31,7 @@ def test_pass_headers_if_provided(monkeypatch: MonkeyPatch) -> None: timeout=300, ) - def mockPost(url, headers, json, stream, timeout): + def mock_post(url, headers, json, stream, timeout): assert url == "https://ollama-hostname:8000/api/generate/" assert headers == { "Content-Type": "application/json", @@ -44,7 +44,7 @@ def mockPost(url, headers, json, stream, timeout): return mock_response_stream() - monkeypatch.setattr(requests, "post", mockPost) + monkeypatch.setattr(requests, "post", mock_post) llm("Test prompt") @@ -52,7 +52,7 @@ def mockPost(url, headers, json, stream, timeout): def test_handle_if_headers_not_provided(monkeypatch: MonkeyPatch) -> None: llm = Ollama(base_url="https://ollama-hostname:8000", model="foo", timeout=300) - def mockPost(url, headers, json, stream, timeout): + def mock_post(url, headers, json, stream, timeout): assert url == "https://ollama-hostname:8000/api/generate/" assert headers == { "Content-Type": "application/json", @@ -63,6 +63,131 @@ def mockPost(url, headers, json, stream, timeout): return mock_response_stream() - monkeypatch.setattr(requests, "post", mockPost) + monkeypatch.setattr(requests, "post", mock_post) llm("Test prompt") + + +def test_handle_kwargs_top_level_parameters(monkeypatch: MonkeyPatch) -> None: + """Test that top level params are sent to the endpoint as top level params""" + llm = Ollama(base_url="https://ollama-hostname:8000", model="foo", timeout=300) + + def mock_post(url, headers, json, stream, timeout): + assert url == "https://ollama-hostname:8000/api/generate/" + assert headers == { + "Content-Type": "application/json", + } + assert json == { + "format": None, + "images": None, + "model": "test-model", + "options": { + "mirostat": None, + "mirostat_eta": None, + "mirostat_tau": None, + "num_ctx": None, + "num_gpu": None, + "num_thread": None, + "repeat_last_n": None, + "repeat_penalty": None, + "stop": [], + "temperature": None, + "tfs_z": None, + "top_k": None, + "top_p": None, + }, + "prompt": "Test prompt", + "system": "Test system prompt", + "template": None, + } + assert stream is True + assert timeout == 300 + + return mock_response_stream() + + monkeypatch.setattr(requests, "post", mock_post) + + llm("Test prompt", model="test-model", system="Test system prompt") + + +def test_handle_kwargs_with_unknown_param(monkeypatch: MonkeyPatch) -> None: + """ + Test that params that are not top level params will be sent to the endpoint + as options + """ + llm = Ollama(base_url="https://ollama-hostname:8000", model="foo", timeout=300) + + def mock_post(url, headers, json, stream, timeout): + assert url == "https://ollama-hostname:8000/api/generate/" + assert headers == { + "Content-Type": "application/json", + } + assert json == { + "format": None, + "images": None, + "model": "foo", + "options": { + "mirostat": None, + "mirostat_eta": None, + "mirostat_tau": None, + "num_ctx": None, + "num_gpu": None, + "num_thread": None, + "repeat_last_n": None, + "repeat_penalty": None, + "stop": [], + "temperature": 0.8, + "tfs_z": None, + "top_k": None, + "top_p": None, + "unknown": "Unknown parameter value", + }, + "prompt": "Test prompt", + "system": None, + "template": None, + } + assert stream is True + assert timeout == 300 + + return mock_response_stream() + + monkeypatch.setattr(requests, "post", mock_post) + + llm("Test prompt", unknown="Unknown parameter value", temperature=0.8) + + +def test_handle_kwargs_with_options(monkeypatch: MonkeyPatch) -> None: + """ + Test that if options provided it will be sent to the endpoint as options, + ignoring other params that are not top level params. + """ + llm = Ollama(base_url="https://ollama-hostname:8000", model="foo", timeout=300) + + def mock_post(url, headers, json, stream, timeout): + assert url == "https://ollama-hostname:8000/api/generate/" + assert headers == { + "Content-Type": "application/json", + } + assert json == { + "format": None, + "images": None, + "model": "test-another-model", + "options": {"unknown_option": "Unknown option value"}, + "prompt": "Test prompt", + "system": None, + "template": None, + } + assert stream is True + assert timeout == 300 + + return mock_response_stream() + + monkeypatch.setattr(requests, "post", mock_post) + + llm( + "Test prompt", + model="test-another-model", + options={"unknown_option": "Unknown option value"}, + unknown="Unknown parameter value", + temperature=0.8, + ) diff --git a/libs/community/tests/unit_tests/retrievers/test_bm25.py b/libs/community/tests/unit_tests/retrievers/test_bm25.py index d36f6dae15ab1..ef40b25ba7dee 100644 --- a/libs/community/tests/unit_tests/retrievers/test_bm25.py +++ b/libs/community/tests/unit_tests/retrievers/test_bm25.py @@ -32,3 +32,14 @@ def test_from_documents() -> None: bm25_retriever = BM25Retriever.from_documents(documents=input_docs) assert len(bm25_retriever.docs) == 3 assert bm25_retriever.vectorizer.doc_len == [4, 5, 4] + + +@pytest.mark.requires("rank_bm25") +def test_repr() -> None: + input_docs = [ + Document(page_content="I have a pen."), + Document(page_content="Do you have a pen?"), + Document(page_content="I have a bag."), + ] + bm25_retriever = BM25Retriever.from_documents(documents=input_docs) + assert "I have a pen" not in repr(bm25_retriever) diff --git a/libs/community/tests/unit_tests/storage/test_imports.py b/libs/community/tests/unit_tests/storage/test_imports.py index 27bcec56d4b26..0cc4914a7406d 100644 --- a/libs/community/tests/unit_tests/storage/test_imports.py +++ b/libs/community/tests/unit_tests/storage/test_imports.py @@ -6,6 +6,8 @@ "RedisStore", "UpstashRedisByteStore", "UpstashRedisStore", + "SQLDocStore", + "SQLStrStore", ] diff --git a/libs/community/tests/unit_tests/storage/test_sql.py b/libs/community/tests/unit_tests/storage/test_sql.py new file mode 100644 index 0000000000000..1d988414637a8 --- /dev/null +++ b/libs/community/tests/unit_tests/storage/test_sql.py @@ -0,0 +1,7 @@ +"""Light weight unit test that attempts to import SQLDocStore/SQLStrStore. +""" + + +def test_import_storage() -> None: + """Attempt to import storage modules.""" + from langchain_community.storage.sql import SQLDocStore, SQLStrStore # noqa diff --git a/libs/community/tests/unit_tests/tools/requests/test_tool.py b/libs/community/tests/unit_tests/tools/requests/test_tool.py index 3ced01aea8d28..b2d53dcc6661c 100644 --- a/libs/community/tests/unit_tests/tools/requests/test_tool.py +++ b/libs/community/tests/unit_tests/tools/requests/test_tool.py @@ -1,4 +1,5 @@ import asyncio +import json from typing import Any, Dict import pytest @@ -11,7 +12,10 @@ RequestsPutTool, _parse_input, ) -from langchain_community.utilities.requests import TextRequestsWrapper +from langchain_community.utilities.requests import ( + JsonRequestsWrapper, + TextRequestsWrapper, +) class _MockTextRequestsWrapper(TextRequestsWrapper): @@ -98,3 +102,97 @@ def test_requests_delete_tool(mock_requests_wrapper: TextRequestsWrapper) -> Non tool = RequestsDeleteTool(requests_wrapper=mock_requests_wrapper) assert tool.run("https://example.com") == "delete_response" assert asyncio.run(tool.arun("https://example.com")) == "adelete_response" + + +class _MockJsonRequestsWrapper(JsonRequestsWrapper): + @staticmethod + def get(url: str, **kwargs: Any) -> Dict[str, Any]: + return {"response": "get_response"} + + @staticmethod + async def aget(url: str, **kwargs: Any) -> Dict[str, Any]: + return {"response": "aget_response"} + + @staticmethod + def post(url: str, data: Dict[str, Any], **kwargs: Any) -> Dict[str, Any]: + return {"response": f"post {json.dumps(data)}"} + + @staticmethod + async def apost(url: str, data: Dict[str, Any], **kwargs: Any) -> Dict[str, Any]: + return {"response": f"apost {json.dumps(data)}"} + + @staticmethod + def patch(url: str, data: Dict[str, Any], **kwargs: Any) -> Dict[str, Any]: + return {"response": f"patch {json.dumps(data)}"} + + @staticmethod + async def apatch(url: str, data: Dict[str, Any], **kwargs: Any) -> Dict[str, Any]: + return {"response": f"apatch {json.dumps(data)}"} + + @staticmethod + def put(url: str, data: Dict[str, Any], **kwargs: Any) -> Dict[str, Any]: + return {"response": f"put {json.dumps(data)}"} + + @staticmethod + async def aput(url: str, data: Dict[str, Any], **kwargs: Any) -> Dict[str, Any]: + return {"response": f"aput {json.dumps(data)}"} + + @staticmethod + def delete(url: str, **kwargs: Any) -> Dict[str, Any]: + return {"response": "delete_response"} + + @staticmethod + async def adelete(url: str, **kwargs: Any) -> Dict[str, Any]: + return {"response": "adelete_response"} + + +@pytest.fixture +def mock_json_requests_wrapper() -> JsonRequestsWrapper: + return _MockJsonRequestsWrapper() + + +def test_requests_get_tool_json( + mock_json_requests_wrapper: JsonRequestsWrapper, +) -> None: + tool = RequestsGetTool(requests_wrapper=mock_json_requests_wrapper) + assert tool.run("https://example.com") == {"response": "get_response"} + assert asyncio.run(tool.arun("https://example.com")) == { + "response": "aget_response" + } + + +def test_requests_post_tool_json( + mock_json_requests_wrapper: JsonRequestsWrapper, +) -> None: + tool = RequestsPostTool(requests_wrapper=mock_json_requests_wrapper) + input_text = '{"url": "https://example.com", "data": {"key": "value"}}' + assert tool.run(input_text) == {"response": 'post {"key": "value"}'} + assert asyncio.run(tool.arun(input_text)) == {"response": 'apost {"key": "value"}'} + + +def test_requests_patch_tool_json( + mock_json_requests_wrapper: JsonRequestsWrapper, +) -> None: + tool = RequestsPatchTool(requests_wrapper=mock_json_requests_wrapper) + input_text = '{"url": "https://example.com", "data": {"key": "value"}}' + assert tool.run(input_text) == {"response": 'patch {"key": "value"}'} + assert asyncio.run(tool.arun(input_text)) == {"response": 'apatch {"key": "value"}'} + + +def test_requests_put_tool_json( + mock_json_requests_wrapper: JsonRequestsWrapper, +) -> None: + tool = RequestsPutTool(requests_wrapper=mock_json_requests_wrapper) + input_text = '{"url": "https://example.com", "data": {"key": "value"}}' + assert tool.run(input_text) == {"response": 'put {"key": "value"}'} + assert asyncio.run(tool.arun(input_text)) == {"response": 'aput {"key": "value"}'} + + +def test_requests_delete_tool_json( + mock_json_requests_wrapper: JsonRequestsWrapper, +) -> None: + tool = RequestsDeleteTool(requests_wrapper=mock_json_requests_wrapper) + assert tool.run("https://example.com") == {"response": "delete_response"} + assert asyncio.run(tool.arun("https://example.com")) == { + "response": "adelete_response" + } diff --git a/libs/community/tests/unit_tests/tools/shell/test_shell.py b/libs/community/tests/unit_tests/tools/shell/test_shell.py index ab6b5abe38c8e..b792505f1c435 100644 --- a/libs/community/tests/unit_tests/tools/shell/test_shell.py +++ b/libs/community/tests/unit_tests/tools/shell/test_shell.py @@ -1,5 +1,6 @@ import warnings from typing import List +from unittest.mock import patch from langchain_community.tools.shell.tool import ShellInput, ShellTool @@ -65,3 +66,29 @@ def test_shell_tool_run_str() -> None: shell_tool = ShellTool(process=placeholder) result = shell_tool._run(commands="echo 'Hello, World!'") assert result.strip() == "hello" + + +async def test_shell_tool_arun_with_user_confirmation() -> None: + placeholder = PlaceholderProcess(output="hello") + shell_tool = ShellTool(process=placeholder, ask_human_input=True) + + with patch("builtins.input", return_value="y"): + result = await shell_tool._arun(commands=test_commands) + assert result.strip() == "hello" + + with patch("builtins.input", return_value="n"): + result = await shell_tool._arun(commands=test_commands) + assert result is None + + +def test_shell_tool_run_with_user_confirmation() -> None: + placeholder = PlaceholderProcess(output="hello") + shell_tool = ShellTool(process=placeholder, ask_human_input=True) + + with patch("builtins.input", return_value="y"): + result = shell_tool._run(commands="echo 'Hello, World!'") + assert result.strip() == "hello" + + with patch("builtins.input", return_value="n"): + result = shell_tool._run(commands="echo 'Hello, World!'") + assert result is None diff --git a/libs/community/tests/unit_tests/tools/test_imports.py b/libs/community/tests/unit_tests/tools/test_imports.py index 424540de098dd..9fdcf157e9638 100644 --- a/libs/community/tests/unit_tests/tools/test_imports.py +++ b/libs/community/tests/unit_tests/tools/test_imports.py @@ -79,6 +79,7 @@ "OpenAPISpec", "OpenWeatherMapQueryRun", "PubmedQueryRun", + "PolygonLastQuote", "RedditSearchRun", "QueryCheckerTool", "QueryPowerBITool", diff --git a/libs/community/tests/unit_tests/tools/test_public_api.py b/libs/community/tests/unit_tests/tools/test_public_api.py index 624262f3d1967..0f6102c45e4be 100644 --- a/libs/community/tests/unit_tests/tools/test_public_api.py +++ b/libs/community/tests/unit_tests/tools/test_public_api.py @@ -81,6 +81,7 @@ "OpenAPISpec", "OpenWeatherMapQueryRun", "PubmedQueryRun", + "PolygonLastQuote", "RedditSearchRun", "QueryCheckerTool", "QueryPowerBITool", diff --git a/libs/community/tests/unit_tests/utilities/test_tavily.py b/libs/community/tests/unit_tests/utilities/test_tavily.py new file mode 100644 index 0000000000000..751bca0ccadfd --- /dev/null +++ b/libs/community/tests/unit_tests/utilities/test_tavily.py @@ -0,0 +1,7 @@ +from langchain_community.utilities.tavily_search import TavilySearchAPIWrapper + + +def test_api_wrapper_api_key_not_visible() -> None: + """Test that an exception is raised if the API key is not present.""" + wrapper = TavilySearchAPIWrapper(tavily_api_key="abcd123") + assert "abcd123" not in repr(wrapper) diff --git a/libs/community/tests/unit_tests/vectorstores/test_elasticsearch.py b/libs/community/tests/unit_tests/vectorstores/test_elasticsearch.py new file mode 100644 index 0000000000000..4a6b5a4dab7bf --- /dev/null +++ b/libs/community/tests/unit_tests/vectorstores/test_elasticsearch.py @@ -0,0 +1,32 @@ +"""Test Elasticsearch functionality.""" +import pytest + +from langchain_community.vectorstores.elasticsearch import ( + ApproxRetrievalStrategy, + ElasticsearchStore, +) +from tests.integration_tests.vectorstores.fake_embeddings import FakeEmbeddings + + +@pytest.mark.requires("elasticsearch") +def test_elasticsearch_hybrid_scores_guard() -> None: + """Ensure an error is raised when search with score in hybrid mode + because in this case Elasticsearch does not return any score. + """ + from elasticsearch import Elasticsearch + + query_string = "foo" + embeddings = FakeEmbeddings() + + store = ElasticsearchStore( + index_name="dummy_index", + es_connection=Elasticsearch(hosts=["http://dummy-host:9200"]), + embedding=embeddings, + strategy=ApproxRetrievalStrategy(hybrid=True), + ) + with pytest.raises(ValueError): + store.similarity_search_with_score(query_string) + + embedded_query = embeddings.embed_query(query_string) + with pytest.raises(ValueError): + store.similarity_search_by_vector_with_relevance_scores(embedded_query) diff --git a/libs/community/tests/unit_tests/vectorstores/test_faiss.py b/libs/community/tests/unit_tests/vectorstores/test_faiss.py index 8b6cf76751b30..db8228962ca67 100644 --- a/libs/community/tests/unit_tests/vectorstores/test_faiss.py +++ b/libs/community/tests/unit_tests/vectorstores/test_faiss.py @@ -10,6 +10,7 @@ from langchain_community.docstore.base import Docstore from langchain_community.docstore.in_memory import InMemoryDocstore from langchain_community.vectorstores.faiss import FAISS +from langchain_community.vectorstores.utils import DistanceStrategy from tests.integration_tests.vectorstores.fake_embeddings import FakeEmbeddings _PAGE_CONTENT = """This is a page about LangChain. @@ -687,6 +688,26 @@ def test_missing_normalize_score_fn() -> None: faiss_instance.similarity_search_with_relevance_scores("foo", k=2) +@pytest.mark.skip(reason="old relevance score feature") +@pytest.mark.requires("faiss") +def test_ip_score() -> None: + embedding = FakeEmbeddings() + vector = embedding.embed_query("hi") + assert vector == [1] * 9 + [0], f"FakeEmbeddings() has changed, produced {vector}" + + db = FAISS.from_texts( + ["sundays coming so i drive my car"], + embedding=FakeEmbeddings(), + distance_strategy=DistanceStrategy.MAX_INNER_PRODUCT, + ) + scores = db.similarity_search_with_relevance_scores("sundays", k=1) + assert len(scores) == 1, "only one vector should be in db" + _, score = scores[0] + assert ( + score == 1 + ), f"expected inner product of equivalent vectors to be 1, not {score}" + + @pytest.mark.requires("faiss") async def test_async_missing_normalize_score_fn() -> None: """Test doesn't perform similarity search without a valid distance strategy.""" diff --git a/libs/community/tests/unit_tests/vectorstores/test_public_api.py b/libs/community/tests/unit_tests/vectorstores/test_public_api.py index 25db0bf3ac1e9..b94c8ae47ece5 100644 --- a/libs/community/tests/unit_tests/vectorstores/test_public_api.py +++ b/libs/community/tests/unit_tests/vectorstores/test_public_api.py @@ -28,6 +28,7 @@ "Epsilla", "FAISS", "Hologres", + "KDBAI", "LanceDB", "Lantern", "LLMRails", diff --git a/libs/core/README.md b/libs/core/README.md index 019f5b400bdce..7de5b5a7f1b66 100644 --- a/libs/core/README.md +++ b/libs/core/README.md @@ -11,29 +11,52 @@ pip install langchain-core ## What is it? -LangChain Core contains the base abstractions that power the rest of the LangChain ecosystem. -These abstractions are designed to be as modular and simple as possible. -Examples of these abstractions include those for language models, document loaders, embedding models, vectorstores, retrievers, and more. +LangChain Core contains the base abstractions that power the rest of the LangChain ecosystem. + +These abstractions are designed to be as modular and simple as possible. Examples of these abstractions include those for language models, document loaders, embedding models, vectorstores, retrievers, and more. + The benefit of having these abstractions is that any provider can implement the required interface and then easily be used in the rest of the LangChain ecosystem. For full documentation see the [API reference](https://api.python.langchain.com/en/stable/core_api_reference.html). -## What is LangChain Expression Language? +## 1️⃣ Core Interface: Runnables + +The concept of a Runnable is central to LangChain Core – it is the interface that most LangChain Core components implement, giving them + +- a common invocation interface (invoke, batch, stream, etc.) +- built-in utilities for retries, fallbacks, schemas and runtime configurability +- easy deployment with [LangServe](https://github.com/langchain-ai/langserve) + +For more check out the [runnable docs](https://python.langchain.com/docs/expression_language/interface). Examples of components that implement the interface include: LLMs, Chat Models, Prompts, Retrievers, Tools, Output Parsers. + +You can use LangChain Core objects in two ways: + +1. **imperative**, ie. call them directly, eg. `model.invoke(...)` + +2. **declarative**, with LangChain Expression Language (LCEL) -LangChain Core also contains LangChain Expression Language, or LCEL, a runtime that allows users to compose arbitrary sequences together and get several benefits that are important when building LLM applications. -We call these sequences “runnables”. +3. or a mix of both! eg. one of the steps in your LCEL sequence can be a custom function -All runnables expose the same interface with single-invocation, batch, streaming and async methods. -This design is useful because it is not enough to have a single sync interface when building an LLM application. -Batch is needed for efficient processing of many inputs. -Streaming (and streaming of intermediate steps) is needed to show the user that progress is being made. -Async interfaces are nice when moving into production. -Rather than having to write multiple implementations for all of those, LCEL allows you to write a runnable once and invoke it in many different ways. +| Feature | Imperative | Declarative | +| --------- | ------------------------------- | -------------- | +| Syntax | All of Python | LCEL | +| Tracing | ✅ – Automatic | ✅ – Automatic | +| Parallel | ✅ – with threads or coroutines | ✅ – Automatic | +| Streaming | ✅ – by yielding | ✅ – Automatic | +| Async | ✅ – by writing async functions | ✅ – Automatic | + +## ⚡️ What is LangChain Expression Language? + +LangChain Expression Language (LCEL) is a _declarative language_ for composing LangChain Core runnables into sequences (or DAGs), covering the most common patterns when building with LLMs. + +LangChain Core compiles LCEL sequences to an _optimized execution plan_, with automatic parallelization, streaming, tracing, and async support. For more check out the [LCEL docs](https://python.langchain.com/docs/expression_language/). ![Diagram outlining the hierarchical organization of the LangChain framework, displaying the interconnected parts across multiple layers.](../../docs/static/img/langchain_stack.png "LangChain Framework Overview") +For more advanced use cases, also check out [LangGraph](https://github.com/langchain-ai/langgraph), which is a graph-based runner for cyclic and recursive LLM workflows. + ## 📕 Releases & Versioning `langchain-core` is currently on version `0.1.x`. @@ -55,4 +78,13 @@ Patch version increases will occur for: As an open-source project in a rapidly developing field, we are extremely open to contributions, whether it be in the form of a new feature, improved infrastructure, or better documentation. -For detailed information on how to contribute, see the [Contributing Guide](https://python.langchain.com/docs/contributing/). \ No newline at end of file +For detailed information on how to contribute, see the [Contributing Guide](https://python.langchain.com/docs/contributing/). + +## ⛰️ Why build on top of LangChain Core? + +The whole LangChain ecosystem is built on top of LangChain Core, so you're in good company when building on top of it. Some of the benefits: + +- **Modularity**: LangChain Core is designed around abstractions that are independent of each other, and not tied to any specific model provider. +- **Stability**: We are committed to a stable versioning scheme, and will communicate any breaking changes with advance notice and version bumps. +- **Battle-tested**: LangChain Core components have the largest install base in the LLM ecosystem, and are used in production by many companies. +- **Community**: LangChain Core is developed in the open, and we welcome contributions from the community. diff --git a/libs/core/langchain_core/_api/deprecation.py b/libs/core/langchain_core/_api/deprecation.py index b9a0c39e485fe..b4d56b70cb494 100644 --- a/libs/core/langchain_core/_api/deprecation.py +++ b/libs/core/langchain_core/_api/deprecation.py @@ -144,6 +144,15 @@ def warning_emitting_wrapper(*args: Any, **kwargs: Any) -> Any: emit_warning() return wrapped(*args, **kwargs) + async def awarning_emitting_wrapper(*args: Any, **kwargs: Any) -> Any: + """Same as warning_emitting_wrapper, but for async functions.""" + + nonlocal warned + if not warned and not is_caller_internal(): + warned = True + emit_warning() + return await wrapped(*args, **kwargs) + if isinstance(obj, type): if not _obj_type: _obj_type = "class" @@ -256,7 +265,10 @@ def finalize( # type: ignore f" {details}" ) - return finalize(warning_emitting_wrapper, new_doc) + if inspect.iscoroutinefunction(obj): + return finalize(awarning_emitting_wrapper, new_doc) + else: + return finalize(warning_emitting_wrapper, new_doc) return deprecate diff --git a/libs/core/langchain_core/callbacks/base.py b/libs/core/langchain_core/callbacks/base.py index ed30e50ff14a4..ff5e7770c5aa5 100644 --- a/libs/core/langchain_core/callbacks/base.py +++ b/libs/core/langchain_core/callbacks/base.py @@ -219,6 +219,7 @@ def on_tool_start( parent_run_id: Optional[UUID] = None, tags: Optional[List[str]] = None, metadata: Optional[Dict[str, Any]] = None, + inputs: Optional[Dict[str, Any]] = None, **kwargs: Any, ) -> Any: """Run when tool starts running.""" @@ -409,6 +410,7 @@ async def on_tool_start( parent_run_id: Optional[UUID] = None, tags: Optional[List[str]] = None, metadata: Optional[Dict[str, Any]] = None, + inputs: Optional[Dict[str, Any]] = None, **kwargs: Any, ) -> None: """Run when tool starts running.""" diff --git a/libs/core/langchain_core/callbacks/manager.py b/libs/core/langchain_core/callbacks/manager.py index b18f83c3aca97..56a8e04db05ef 100644 --- a/libs/core/langchain_core/callbacks/manager.py +++ b/libs/core/langchain_core/callbacks/manager.py @@ -1282,15 +1282,22 @@ def on_tool_start( input_str: str, run_id: Optional[UUID] = None, parent_run_id: Optional[UUID] = None, + inputs: Optional[Dict[str, Any]] = None, **kwargs: Any, ) -> CallbackManagerForToolRun: """Run when tool starts running. Args: - serialized (Dict[str, Any]): The serialized tool. - input_str (str): The input to the tool. - run_id (UUID, optional): The ID of the run. Defaults to None. - parent_run_id (UUID, optional): The ID of the parent run. Defaults to None. + serialized: Serialized representation of the tool. + input_str: The input to the tool as a string. + Non-string inputs are cast to strings. + run_id: ID for the run. Defaults to None. + parent_run_id: The ID of the parent run. Defaults to None. + inputs: The original input to the tool if provided. + Recommended for usage instead of input_str when the original + input is needed. + If provided, the inputs are expected to be formatted as a dict. + The keys will correspond to the named-arguments in the tool. Returns: CallbackManagerForToolRun: The callback manager for the tool run. @@ -1308,6 +1315,7 @@ def on_tool_start( parent_run_id=self.parent_run_id, tags=self.tags, metadata=self.metadata, + inputs=inputs, **kwargs, ) diff --git a/libs/core/langchain_core/example_selectors/semantic_similarity.py b/libs/core/langchain_core/example_selectors/semantic_similarity.py index 243c665d69035..919ad88a3b588 100644 --- a/libs/core/langchain_core/example_selectors/semantic_similarity.py +++ b/libs/core/langchain_core/example_selectors/semantic_similarity.py @@ -74,6 +74,9 @@ def from_examples( vectorstore_cls: Type[VectorStore], k: int = 4, input_keys: Optional[List[str]] = None, + *, + example_keys: Optional[List[str]] = None, + vectorstore_kwargs: Optional[dict] = None, **vectorstore_cls_kwargs: Any, ) -> SemanticSimilarityExampleSelector: """Create k-shot example selector using example list and embeddings. @@ -102,7 +105,13 @@ def from_examples( vectorstore = vectorstore_cls.from_texts( string_examples, embeddings, metadatas=examples, **vectorstore_cls_kwargs ) - return cls(vectorstore=vectorstore, k=k, input_keys=input_keys) + return cls( + vectorstore=vectorstore, + k=k, + input_keys=input_keys, + example_keys=example_keys, + vectorstore_kwargs=vectorstore_kwargs, + ) class MaxMarginalRelevanceExampleSelector(SemanticSimilarityExampleSelector): diff --git a/libs/core/langchain_core/load/load.py b/libs/core/langchain_core/load/load.py index dbf0f2c77d94e..f10e1cb680227 100644 --- a/libs/core/langchain_core/load/load.py +++ b/libs/core/langchain_core/load/load.py @@ -6,7 +6,7 @@ from langchain_core._api import beta from langchain_core.load.mapping import ( _OG_SERIALIZABLE_MAPPING, - OLD_PROMPT_TEMPLATE_FORMATS, + OLD_CORE_NAMESPACES_MAPPING, SERIALIZABLE_MAPPING, ) from langchain_core.load.serializable import Serializable @@ -15,7 +15,7 @@ ALL_SERIALIZABLE_MAPPINGS = { **SERIALIZABLE_MAPPING, - **OLD_PROMPT_TEMPLATE_FORMATS, + **OLD_CORE_NAMESPACES_MAPPING, **_OG_SERIALIZABLE_MAPPING, } diff --git a/libs/core/langchain_core/load/mapping.py b/libs/core/langchain_core/load/mapping.py index 454879230d34e..7ac3c7d3e3858 100644 --- a/libs/core/langchain_core/load/mapping.py +++ b/libs/core/langchain_core/load/mapping.py @@ -253,9 +253,8 @@ "ChatGooglePalm", ), ("langchain", "chat_models", "vertexai", "ChatVertexAI"): ( - "langchain", + "langchain_google_vertexai", "chat_models", - "vertexai", "ChatVertexAI", ), ("langchain", "schema", "output", "ChatGenerationChunk"): ( @@ -337,9 +336,8 @@ "Replicate", ), ("langchain", "llms", "vertexai", "VertexAI"): ( - "langchain", + "langchain_vertexai", "llms", - "vertexai", "VertexAI", ), ("langchain", "output_parsers", "combining", "CombiningOutputParser"): ( @@ -515,102 +513,239 @@ } # Needed for backwards compatibility for a few versions where we serialized -# with langchain_core -OLD_PROMPT_TEMPLATE_FORMATS: Dict[Tuple[str, ...], Tuple[str, ...]] = { - ( +# with langchain_core paths. +OLD_CORE_NAMESPACES_MAPPING: Dict[Tuple[str, ...], Tuple[str, ...]] = { + ("langchain_core", "messages", "ai", "AIMessage"): ( "langchain_core", - "prompts", + "messages", + "ai", + "AIMessage", + ), + ("langchain_core", "messages", "ai", "AIMessageChunk"): ( + "langchain_core", + "messages", + "ai", + "AIMessageChunk", + ), + ("langchain_core", "messages", "base", "BaseMessage"): ( + "langchain_core", + "messages", "base", - "BasePromptTemplate", - ): ( + "BaseMessage", + ), + ("langchain_core", "messages", "base", "BaseMessageChunk"): ( "langchain_core", - "prompts", + "messages", "base", - "BasePromptTemplate", + "BaseMessageChunk", ), - ( + ("langchain_core", "messages", "chat", "ChatMessage"): ( + "langchain_core", + "messages", + "chat", + "ChatMessage", + ), + ("langchain_core", "messages", "function", "FunctionMessage"): ( + "langchain_core", + "messages", + "function", + "FunctionMessage", + ), + ("langchain_core", "messages", "human", "HumanMessage"): ( + "langchain_core", + "messages", + "human", + "HumanMessage", + ), + ("langchain_core", "messages", "system", "SystemMessage"): ( + "langchain_core", + "messages", + "system", + "SystemMessage", + ), + ("langchain_core", "messages", "tool", "ToolMessage"): ( + "langchain_core", + "messages", + "tool", + "ToolMessage", + ), + ("langchain_core", "agents", "AgentAction"): ( + "langchain_core", + "agents", + "AgentAction", + ), + ("langchain_core", "agents", "AgentFinish"): ( + "langchain_core", + "agents", + "AgentFinish", + ), + ("langchain_core", "prompts", "base", "BasePromptTemplate"): ( "langchain_core", "prompts", - "prompt", - "PromptTemplate", - ): ( + "base", + "BasePromptTemplate", + ), + ("langchain_core", "prompts", "prompt", "PromptTemplate"): ( "langchain_core", "prompts", "prompt", "PromptTemplate", ), - ( - "langchain_core", - "prompts", - "chat", - "MessagesPlaceholder", - ): ( + ("langchain_core", "prompts", "chat", "MessagesPlaceholder"): ( "langchain_core", "prompts", "chat", "MessagesPlaceholder", ), - ( - "langchain_core", - "prompts", - "chat", - "ChatPromptTemplate", - ): ( + ("langchain_core", "prompts", "chat", "ChatPromptTemplate"): ( "langchain_core", "prompts", "chat", "ChatPromptTemplate", ), - ( - "langchain_core", - "prompts", - "chat", - "HumanMessagePromptTemplate", - ): ( + ("langchain_core", "prompts", "chat", "HumanMessagePromptTemplate"): ( "langchain_core", "prompts", "chat", "HumanMessagePromptTemplate", ), - ( + ("langchain_core", "prompts", "chat", "SystemMessagePromptTemplate"): ( "langchain_core", "prompts", "chat", "SystemMessagePromptTemplate", - ): ( + ), + ("langchain_core", "agents", "AgentActionMessageLog"): ( "langchain_core", - "prompts", - "chat", - "SystemMessagePromptTemplate", + "agents", + "AgentActionMessageLog", ), - ( + ("langchain_core", "prompts", "chat", "BaseMessagePromptTemplate"): ( "langchain_core", "prompts", "chat", "BaseMessagePromptTemplate", - ): ( + ), + ("langchain_core", "outputs", "chat_generation", "ChatGeneration"): ( "langchain_core", - "prompts", - "chat", - "BaseMessagePromptTemplate", + "outputs", + "chat_generation", + "ChatGeneration", ), - ( + ("langchain_core", "outputs", "generation", "Generation"): ( + "langchain_core", + "outputs", + "generation", + "Generation", + ), + ("langchain_core", "documents", "base", "Document"): ( + "langchain_core", + "documents", + "base", + "Document", + ), + ("langchain_core", "prompts", "chat", "AIMessagePromptTemplate"): ( "langchain_core", "prompts", "chat", - "BaseChatPromptTemplate", - ): ( + "AIMessagePromptTemplate", + ), + ("langchain_core", "runnables", "configurable", "DynamicRunnable"): ( "langchain_core", - "prompts", + "runnables", + "configurable", + "DynamicRunnable", + ), + ("langchain_core", "prompt_values", "PromptValue"): ( + "langchain_core", + "prompt_values", + "PromptValue", + ), + ("langchain_core", "runnables", "base", "RunnableBinding"): ( + "langchain_core", + "runnables", + "base", + "RunnableBinding", + ), + ("langchain_core", "runnables", "branch", "RunnableBranch"): ( + "langchain_core", + "runnables", + "branch", + "RunnableBranch", + ), + ("langchain_core", "runnables", "fallbacks", "RunnableWithFallbacks"): ( + "langchain_core", + "runnables", + "fallbacks", + "RunnableWithFallbacks", + ), + ("langchain_core", "output_parsers", "string", "StrOutputParser"): ( + "langchain_core", + "output_parsers", + "string", + "StrOutputParser", + ), + ("langchain_core", "output_parsers", "list", "CommaSeparatedListOutputParser"): ( + "langchain_core", + "output_parsers", + "list", + "CommaSeparatedListOutputParser", + ), + ("langchain_core", "runnables", "base", "RunnableParallel"): ( + "langchain_core", + "runnables", + "base", + "RunnableParallel", + ), + ("langchain_core", "outputs", "chat_generation", "ChatGenerationChunk"): ( + "langchain_core", + "outputs", + "chat_generation", + "ChatGenerationChunk", + ), + ("langchain_core", "messages", "chat", "ChatMessageChunk"): ( + "langchain_core", + "messages", "chat", - "BaseChatPromptTemplate", + "ChatMessageChunk", ), - ( + ("langchain_core", "messages", "human", "HumanMessageChunk"): ( + "langchain_core", + "messages", + "human", + "HumanMessageChunk", + ), + ("langchain_core", "messages", "function", "FunctionMessageChunk"): ( + "langchain_core", + "messages", + "function", + "FunctionMessageChunk", + ), + ("langchain_core", "messages", "system", "SystemMessageChunk"): ( + "langchain_core", + "messages", + "system", + "SystemMessageChunk", + ), + ("langchain_core", "messages", "tool", "ToolMessageChunk"): ( + "langchain_core", + "messages", + "tool", + "ToolMessageChunk", + ), + ("langchain_core", "outputs", "generation", "GenerationChunk"): ( + "langchain_core", + "outputs", + "generation", + "GenerationChunk", + ), + ("langchain_core", "prompts", "chat", "BaseChatPromptTemplate"): ( "langchain_core", "prompts", "chat", - "ChatMessagePromptTemplate", - ): ( + "BaseChatPromptTemplate", + ), + ("langchain_core", "prompts", "chat", "ChatMessagePromptTemplate"): ( "langchain_core", "prompts", "chat", @@ -627,48 +762,108 @@ "few_shot_with_templates", "FewShotPromptWithTemplates", ), - ( - "langchain_core", - "prompts", - "pipeline", - "PipelinePromptTemplate", - ): ( + ("langchain_core", "prompts", "pipeline", "PipelinePromptTemplate"): ( "langchain_core", "prompts", "pipeline", "PipelinePromptTemplate", ), - ( + ("langchain_core", "prompts", "string", "StringPromptTemplate"): ( "langchain_core", "prompts", "string", "StringPromptTemplate", - ): ( + ), + ("langchain_core", "prompt_values", "StringPromptValue"): ( "langchain_core", - "prompts", - "string", - "StringPromptTemplate", + "prompt_values", + "StringPromptValue", ), - ( + ("langchain_core", "prompts", "chat", "BaseStringMessagePromptTemplate"): ( "langchain_core", "prompts", "chat", "BaseStringMessagePromptTemplate", - ): ( + ), + ("langchain_core", "prompt_values", "ChatPromptValue"): ( "langchain_core", - "prompts", - "chat", - "BaseStringMessagePromptTemplate", + "prompt_values", + "ChatPromptValue", + ), + ("langchain_core", "prompt_values", "ChatPromptValueConcrete"): ( + "langchain_core", + "prompt_values", + "ChatPromptValueConcrete", + ), + ("langchain_core", "runnables", "base", "RunnableBindingBase"): ( + "langchain_core", + "runnables", + "base", + "RunnableBindingBase", + ), + ("langchain_core", "runnables", "router", "RouterRunnable"): ( + "langchain_core", + "runnables", + "router", + "RouterRunnable", + ), + ("langchain_core", "runnables", "passthrough", "RunnablePassthrough"): ( + "langchain_core", + "runnables", + "passthrough", + "RunnablePassthrough", + ), + ("langchain_core", "runnables", "base", "RunnableSequence"): ( + "langchain_core", + "runnables", + "base", + "RunnableSequence", + ), + ("langchain_core", "runnables", "base", "RunnableEach"): ( + "langchain_core", + "runnables", + "base", + "RunnableEach", + ), + ("langchain_core", "runnables", "base", "RunnableEachBase"): ( + "langchain_core", + "runnables", + "base", + "RunnableEachBase", ), ( "langchain_core", - "prompts", - "chat", - "AIMessagePromptTemplate", + "runnables", + "configurable", + "RunnableConfigurableAlternatives", ): ( "langchain_core", - "prompts", - "chat", - "AIMessagePromptTemplate", + "runnables", + "configurable", + "RunnableConfigurableAlternatives", + ), + ("langchain_core", "runnables", "configurable", "RunnableConfigurableFields"): ( + "langchain_core", + "runnables", + "configurable", + "RunnableConfigurableFields", + ), + ("langchain_core", "runnables", "history", "RunnableWithMessageHistory"): ( + "langchain_core", + "runnables", + "history", + "RunnableWithMessageHistory", + ), + ("langchain_core", "runnables", "passthrough", "RunnableAssign"): ( + "langchain_core", + "runnables", + "passthrough", + "RunnableAssign", + ), + ("langchain_core", "runnables", "retry", "RunnableRetry"): ( + "langchain_core", + "runnables", + "retry", + "RunnableRetry", ), } diff --git a/libs/core/langchain_core/outputs/chat_generation.py b/libs/core/langchain_core/outputs/chat_generation.py index fa5041c34866a..b7bd6042a2c1e 100644 --- a/libs/core/langchain_core/outputs/chat_generation.py +++ b/libs/core/langchain_core/outputs/chat_generation.py @@ -5,6 +5,7 @@ from langchain_core.messages import BaseMessage, BaseMessageChunk from langchain_core.outputs.generation import Generation from langchain_core.pydantic_v1 import root_validator +from langchain_core.utils._merge import merge_dicts class ChatGeneration(Generation): @@ -53,14 +54,13 @@ def get_lc_namespace(cls) -> List[str]: def __add__(self, other: ChatGenerationChunk) -> ChatGenerationChunk: if isinstance(other, ChatGenerationChunk): - generation_info = ( - {**(self.generation_info or {}), **(other.generation_info or {})} - if self.generation_info is not None or other.generation_info is not None - else None + generation_info = merge_dicts( + self.generation_info or {}, + other.generation_info or {}, ) return ChatGenerationChunk( message=self.message + other.message, - generation_info=generation_info, + generation_info=generation_info or None, ) else: raise TypeError( diff --git a/libs/core/langchain_core/outputs/generation.py b/libs/core/langchain_core/outputs/generation.py index 3ede28f9fc677..3f0a79ecb10b0 100644 --- a/libs/core/langchain_core/outputs/generation.py +++ b/libs/core/langchain_core/outputs/generation.py @@ -3,6 +3,7 @@ from typing import Any, Dict, List, Literal, Optional from langchain_core.load import Serializable +from langchain_core.utils._merge import merge_dicts class Generation(Serializable): @@ -40,14 +41,13 @@ def get_lc_namespace(cls) -> List[str]: def __add__(self, other: GenerationChunk) -> GenerationChunk: if isinstance(other, GenerationChunk): - generation_info = ( - {**(self.generation_info or {}), **(other.generation_info or {})} - if self.generation_info is not None or other.generation_info is not None - else None + generation_info = merge_dicts( + self.generation_info or {}, + other.generation_info or {}, ) return GenerationChunk( text=self.text + other.text, - generation_info=generation_info, + generation_info=generation_info or None, ) else: raise TypeError( diff --git a/libs/core/langchain_core/runnables/base.py b/libs/core/langchain_core/runnables/base.py index 0bb5ddb11b953..280ecd25eb3f4 100644 --- a/libs/core/langchain_core/runnables/base.py +++ b/libs/core/langchain_core/runnables/base.py @@ -7,7 +7,6 @@ from abc import ABC, abstractmethod from concurrent.futures import FIRST_COMPLETED, wait from contextvars import copy_context -from copy import deepcopy from functools import wraps from itertools import groupby, tee from operator import itemgetter @@ -36,7 +35,8 @@ from typing_extensions import Literal, get_args -from langchain_core.load.dump import dumpd, dumps +from langchain_core._api import beta_decorator +from langchain_core.load.dump import dumpd from langchain_core.load.serializable import Serializable from langchain_core.pydantic_v1 import BaseConfig, BaseModel, Field, create_model from langchain_core.runnables.config import ( @@ -54,6 +54,7 @@ var_child_runnable_config, ) from langchain_core.runnables.graph import Graph +from langchain_core.runnables.schema import EventData, StreamEvent from langchain_core.runnables.utils import ( AddableDict, AnyConfigurableField, @@ -83,7 +84,11 @@ from langchain_core.runnables.fallbacks import ( RunnableWithFallbacks as RunnableWithFallbacksT, ) - from langchain_core.tracers.log_stream import RunLog, RunLogPatch + from langchain_core.tracers.log_stream import ( + LogEntry, + RunLog, + RunLogPatch, + ) from langchain_core.tracers.root_listeners import Listener @@ -600,7 +605,7 @@ def astream_log( exclude_names: Optional[Sequence[str]] = None, exclude_types: Optional[Sequence[str]] = None, exclude_tags: Optional[Sequence[str]] = None, - **kwargs: Optional[Any], + **kwargs: Any, ) -> AsyncIterator[RunLogPatch]: ... @@ -618,7 +623,7 @@ def astream_log( exclude_names: Optional[Sequence[str]] = None, exclude_types: Optional[Sequence[str]] = None, exclude_tags: Optional[Sequence[str]] = None, - **kwargs: Optional[Any], + **kwargs: Any, ) -> AsyncIterator[RunLog]: ... @@ -635,7 +640,7 @@ async def astream_log( exclude_names: Optional[Sequence[str]] = None, exclude_types: Optional[Sequence[str]] = None, exclude_tags: Optional[Sequence[str]] = None, - **kwargs: Optional[Any], + **kwargs: Any, ) -> Union[AsyncIterator[RunLogPatch], AsyncIterator[RunLog]]: """ Stream all output from a runnable, as reported to the callback system. @@ -659,16 +664,198 @@ async def astream_log( exclude_types: Exclude logs with these types. exclude_tags: Exclude logs with these tags. """ - import jsonpatch # type: ignore[import] + from langchain_core.tracers.log_stream import ( + LogStreamCallbackHandler, + _astream_log_implementation, + ) - from langchain_core.callbacks.base import BaseCallbackManager + stream = LogStreamCallbackHandler( + auto_close=False, + include_names=include_names, + include_types=include_types, + include_tags=include_tags, + exclude_names=exclude_names, + exclude_types=exclude_types, + exclude_tags=exclude_tags, + _schema_format="original", + ) + + # Mypy isn't resolving the overloads here + # Likely an issue b/c `self` is being passed through + # and it's can't map it to Runnable[Input,Output]? + async for item in _astream_log_implementation( # type: ignore + self, + input, + config, + diff=diff, + stream=stream, + with_streamed_output_list=with_streamed_output_list, + ): + yield item + + @beta_decorator.beta(message="This API is in beta and may change in the future.") + async def astream_events( + self, + input: Any, + config: Optional[RunnableConfig] = None, + *, + version: Literal["v1"], + include_names: Optional[Sequence[str]] = None, + include_types: Optional[Sequence[str]] = None, + include_tags: Optional[Sequence[str]] = None, + exclude_names: Optional[Sequence[str]] = None, + exclude_types: Optional[Sequence[str]] = None, + exclude_tags: Optional[Sequence[str]] = None, + **kwargs: Any, + ) -> AsyncIterator[StreamEvent]: + """Generate a stream of events. + + Use to create an iterator ove StreamEvents that provide real-time information + about the progress of the runnable, including StreamEvents from intermediate + results. + + A StreamEvent is a dictionary with the following schema: + + * ``event``: str - Event names are of the + format: on_[runnable_type]_(start|stream|end). + * ``name``: str - The name of the runnable that generated the event. + * ``run_id``: str - randomly generated ID associated with the given execution of + the runnable that emitted the event. + A child runnable that gets invoked as part of the execution of a + parent runnable is assigned its own unique ID. + * ``tags``: Optional[List[str]] - The tags of the runnable that generated + the event. + * ``metadata``: Optional[Dict[str, Any]] - The metadata of the runnable + that generated the event. + * ``data``: Dict[str, Any] + + + Below is a table that illustrates some evens that might be emitted by various + chains. Metadata fields have been omitted from the table for brevity. + Chain definitions have been included after the table. + + | event | name | chunk | input | output | + |----------------------|------------------|---------------------------------|-----------------------------------------------|-------------------------------------------------| + | on_chat_model_start | [model name] | | {"messages": [[SystemMessage, HumanMessage]]} | | + | on_chat_model_stream | [model name] | AIMessageChunk(content="hello") | | | + | on_chat_model_end | [model name] | | {"messages": [[SystemMessage, HumanMessage]]} | {"generations": [...], "llm_output": None, ...} | + | on_llm_start | [model name] | | {'input': 'hello'} | | + | on_llm_stream | [model name] | 'Hello' | | | + | on_llm_end | [model name] | | 'Hello human!' | + | on_chain_start | format_docs | | | | + | on_chain_stream | format_docs | "hello world!, goodbye world!" | | | + | on_chain_end | format_docs | | [Document(...)] | "hello world!, goodbye world!" | + | on_tool_start | some_tool | | {"x": 1, "y": "2"} | | + | on_tool_stream | some_tool | {"x": 1, "y": "2"} | | | + | on_tool_end | some_tool | | | {"x": 1, "y": "2"} | + | on_retriever_start | [retriever name] | | {"query": "hello"} | | + | on_retriever_chunk | [retriever name] | {documents: [...]} | | | + | on_retriever_end | [retriever name] | | {"query": "hello"} | {documents: [...]} | + | on_prompt_start | [template_name] | | {"question": "hello"} | | + | on_prompt_end | [template_name] | | {"question": "hello"} | ChatPromptValue(messages: [SystemMessage, ...]) | + + Here are declarations associated with the events shown above: + + `format_docs`: + + ```python + def format_docs(docs: List[Document]) -> str: + '''Format the docs.''' + return ", ".join([doc.page_content for doc in docs]) + + format_docs = RunnableLambda(format_docs) + ``` + + `some_tool`: + + ```python + @tool + def some_tool(x: int, y: str) -> dict: + '''Some_tool.''' + return {"x": x, "y": y} + ``` + + `prompt`: + + ```python + template = ChatPromptTemplate.from_messages( + [("system", "You are Cat Agent 007"), ("human", "{question}")] + ).with_config({"run_name": "my_template", "tags": ["my_template"]}) + ``` + + Example: + + .. code-block:: python + + from langchain_core.runnables import RunnableLambda + + async def reverse(s: str) -> str: + return s[::-1] + + chain = RunnableLambda(func=reverse) + + events = [ + event async for event in chain.astream_events("hello", version="v1") + ] + + # will produce the following events (run_id has been omitted for brevity): + [ + { + "data": {"input": "hello"}, + "event": "on_chain_start", + "metadata": {}, + "name": "reverse", + "tags": [], + }, + { + "data": {"chunk": "olleh"}, + "event": "on_chain_stream", + "metadata": {}, + "name": "reverse", + "tags": [], + }, + { + "data": {"output": "olleh"}, + "event": "on_chain_end", + "metadata": {}, + "name": "reverse", + "tags": [], + }, + ] + + Args: + input: The input to the runnable. + config: The config to use for the runnable. + version: The version of the schema to use. + Currently only version 1 is available. + No default will be assigned until the API is stabilized. + include_names: Only include events from runnables with matching names. + include_types: Only include events from runnables with matching types. + include_tags: Only include events from runnables with matching tags. + exclude_names: Exclude events from runnables with matching names. + exclude_types: Exclude events from runnables with matching types. + exclude_tags: Exclude events from runnables with matching tags. + kwargs: Additional keyword arguments to pass to the runnable. + These will be passed to astream_log as this implementation + of astream_events is built on top of astream_log. + + Returns: + An async stream of StreamEvents. + """ # noqa: E501 + if version != "v1": + raise NotImplementedError( + 'Only version "v1" of the schema is currently supported.' + ) + + from langchain_core.runnables.utils import ( + _RootEventFilter, + ) from langchain_core.tracers.log_stream import ( LogStreamCallbackHandler, RunLog, - RunLogPatch, + _astream_log_implementation, ) - # Create a stream handler that will emit Log objects stream = LogStreamCallbackHandler( auto_close=False, include_names=include_names, @@ -677,81 +864,159 @@ async def astream_log( exclude_names=exclude_names, exclude_types=exclude_types, exclude_tags=exclude_tags, + _schema_format="streaming_events", + ) + + run_log = RunLog(state=None) # type: ignore[arg-type] + encountered_start_event = False + + _root_event_filter = _RootEventFilter( + include_names=include_names, + include_types=include_types, + include_tags=include_tags, + exclude_names=exclude_names, + exclude_types=exclude_types, + exclude_tags=exclude_tags, ) - # Assign the stream handler to the config config = ensure_config(config) - callbacks = config.get("callbacks") - if callbacks is None: - config["callbacks"] = [stream] - elif isinstance(callbacks, list): - config["callbacks"] = callbacks + [stream] - elif isinstance(callbacks, BaseCallbackManager): - callbacks = callbacks.copy() - callbacks.add_handler(stream, inherit=True) - config["callbacks"] = callbacks - else: - raise ValueError( - f"Unexpected type for callbacks: {callbacks}." - "Expected None, list or AsyncCallbackManager." - ) + root_tags = config.get("tags", []) + root_metadata = config.get("metadata", {}) + root_name = config.get("run_name", self.get_name()) + + # Ignoring mypy complaint about too many different union combinations + # This arises because many of the argument types are unions + async for log in _astream_log_implementation( # type: ignore[misc] + self, + input, + config=config, + stream=stream, + diff=True, + with_streamed_output_list=True, + **kwargs, + ): + run_log = run_log + log + + if not encountered_start_event: + # Yield the start event for the root runnable. + encountered_start_event = True + state = run_log.state.copy() + + event = StreamEvent( + event=f"on_{state['type']}_start", + run_id=state["id"], + name=root_name, + tags=root_tags, + metadata=root_metadata, + data={ + "input": input, + }, + ) - # Call the runnable in streaming mode, - # add each chunk to the output stream - async def consume_astream() -> None: - try: - prev_final_output: Optional[Output] = None - final_output: Optional[Output] = None + if _root_event_filter.include_event(event, state["type"]): + yield event - async for chunk in self.astream(input, config, **kwargs): - prev_final_output = final_output - if final_output is None: - final_output = chunk + paths = { + op["path"].split("/")[2] + for op in log.ops + if op["path"].startswith("/logs/") + } + # Elements in a set should be iterated in the same order + # as they were inserted in modern python versions. + for path in paths: + data: EventData = {} + log_entry: LogEntry = run_log.state["logs"][path] + if log_entry["end_time"] is None: + if log_entry["streamed_output"]: + event_type = "stream" else: - try: - final_output = final_output + chunk # type: ignore - except TypeError: - final_output = chunk - patches: List[Dict[str, Any]] = [] - if with_streamed_output_list: - patches.append( - { - "op": "add", - "path": "/streamed_output/-", - # chunk cannot be shared between - # streamed_output and final_output - # otherwise jsonpatch.apply will - # modify both - "value": deepcopy(chunk), - } + event_type = "start" + else: + event_type = "end" + + if event_type == "start": + # Include the inputs with the start event if they are available. + # Usually they will NOT be available for components that operate + # on streams, since those components stream the input and + # don't know its final value until the end of the stream. + inputs = log_entry["inputs"] + if inputs is not None: + data["input"] = inputs + pass + + if event_type == "end": + inputs = log_entry["inputs"] + if inputs is not None: + data["input"] = inputs + + # None is a VALID output for an end event + data["output"] = log_entry["final_output"] + + if event_type == "stream": + num_chunks = len(log_entry["streamed_output"]) + if num_chunks != 1: + raise AssertionError( + f"Expected exactly one chunk of streamed output, " + f"got {num_chunks} instead. This is impossible. " + f"Encountered in: {log_entry['name']}" ) - for op in jsonpatch.JsonPatch.from_diff( - prev_final_output, final_output, dumps=dumps - ): - patches.append({**op, "path": f"/final_output{op['path']}"}) - await stream.send_stream.send(RunLogPatch(*patches)) - finally: - await stream.send_stream.aclose() - # Start the runnable in a task, so we can start consuming output - task = asyncio.create_task(consume_astream()) + data = {"chunk": log_entry["streamed_output"][0]} + # Clean up the stream, we don't need it anymore. + # And this avoids duplicates as well! + log_entry["streamed_output"] = [] + + yield StreamEvent( + event=f"on_{log_entry['type']}_{event_type}", + name=log_entry["name"], + run_id=log_entry["id"], + tags=log_entry["tags"], + metadata=log_entry["metadata"], + data=data, + ) + + # Finally, we take care of the streaming output from the root chain + # if there is any. + state = run_log.state + if state["streamed_output"]: + num_chunks = len(state["streamed_output"]) + if num_chunks != 1: + raise AssertionError( + f"Expected exactly one chunk of streamed output, " + f"got {num_chunks} instead. This is impossible. " + f"Encountered in: {state['name']}" + ) - try: - # Yield each chunk from the output stream - if diff: - async for log in stream: - yield log - else: - state = RunLog(state=None) # type: ignore[arg-type] - async for log in stream: - state = state + log - yield state - finally: - # Wait for the runnable to finish, if not cancelled (eg. by break) - try: - await task - except asyncio.CancelledError: - pass + data = {"chunk": state["streamed_output"][0]} + # Clean up the stream, we don't need it anymore. + state["streamed_output"] = [] + + event = StreamEvent( + event=f"on_{state['type']}_stream", + run_id=state["id"], + tags=root_tags, + metadata=root_metadata, + name=root_name, + data=data, + ) + if _root_event_filter.include_event(event, state["type"]): + yield event + + state = run_log.state + + # Finally yield the end event for the root runnable. + event = StreamEvent( + event=f"on_{state['type']}_end", + name=root_name, + run_id=state["id"], + tags=root_tags, + metadata=root_metadata, + data={ + "output": state["final_output"], + }, + ) + if _root_event_filter.include_event(event, state["type"]): + yield event def transform( self, @@ -923,12 +1188,17 @@ def with_fallbacks( fallbacks: Sequence[Runnable[Input, Output]], *, exceptions_to_handle: Tuple[Type[BaseException], ...] = (Exception,), + exception_key: Optional[str] = None, ) -> RunnableWithFallbacksT[Input, Output]: """Add fallbacks to a runnable, returning a new Runnable. Args: fallbacks: A sequence of runnables to try if the original runnable fails. exceptions_to_handle: A tuple of exception types to handle. + exception_key: If string is specified then handled exceptions will be passed + to fallbacks as part of the input under the specified key. If None, + exceptions will not be passed to fallbacks. If used, the base runnable + and its fallbacks must accept a dictionary as input. Returns: A new Runnable that will try the original runnable, and then each @@ -940,6 +1210,7 @@ def with_fallbacks( runnable=self, fallbacks=fallbacks, exceptions_to_handle=exceptions_to_handle, + exception_key=exception_key, ) """ --- Helper methods for Subclasses --- """ @@ -1232,8 +1503,10 @@ def _transform_stream_with_config( try: final_output = final_output + chunk # type: ignore except TypeError: - final_output = None + final_output = chunk final_output_supported = False + else: + final_output = chunk except StopIteration: pass for ichunk in input_for_tracing: @@ -1244,8 +1517,10 @@ def _transform_stream_with_config( try: final_input = final_input + ichunk # type: ignore except TypeError: - final_input = None + final_input = ichunk final_input_supported = False + else: + final_input = ichunk except BaseException as e: run_manager.on_chain_error(e, inputs=final_input) raise @@ -1331,8 +1606,10 @@ async def _atransform_stream_with_config( try: final_output = final_output + chunk # type: ignore except TypeError: - final_output = None + final_output = chunk final_output_supported = False + else: + final_output = chunk except StopAsyncIteration: pass async for ichunk in input_for_tracing: @@ -1343,8 +1620,10 @@ async def _atransform_stream_with_config( try: final_input = final_input + ichunk # type: ignore[operator] except TypeError: - final_input = None + final_input = ichunk final_input_supported = False + else: + final_input = ichunk except BaseException as e: await run_manager.on_chain_error(e, inputs=final_input) raise @@ -1367,7 +1646,7 @@ def configurable_fields( if key not in self.__fields__: raise ValueError( f"Configuration key {key} not found in {self}: " - "available keys are {self.__fields__.keys()}" + f"available keys are {self.__fields__.keys()}" ) return RunnableConfigurableFields(default=self, fields=kwargs) @@ -3390,6 +3669,18 @@ async def ainvoke( ) -> List[Output]: return await self._acall_with_config(self._ainvoke, input, config, **kwargs) + async def astream_events( + self, + input: Input, + config: Optional[RunnableConfig] = None, + **kwargs: Optional[Any], + ) -> AsyncIterator[StreamEvent]: + for _ in range(1): + raise NotImplementedError( + "RunnableEach does not support astream_events yet." + ) + yield + class RunnableEach(RunnableEachBase[Input, Output]): """ @@ -3680,6 +3971,17 @@ async def astream( ): yield item + async def astream_events( + self, + input: Input, + config: Optional[RunnableConfig] = None, + **kwargs: Optional[Any], + ) -> AsyncIterator[StreamEvent]: + async for item in self.bound.astream_events( + input, self._merge_configs(config), **{**self.kwargs, **kwargs} + ): + yield item + def transform( self, input: Iterator[Input], diff --git a/libs/core/langchain_core/runnables/fallbacks.py b/libs/core/langchain_core/runnables/fallbacks.py index 5f6dbf11bf378..bc7128c1bf3f2 100644 --- a/libs/core/langchain_core/runnables/fallbacks.py +++ b/libs/core/langchain_core/runnables/fallbacks.py @@ -2,6 +2,9 @@ from typing import ( TYPE_CHECKING, Any, + AsyncIterator, + Awaitable, + Dict, Iterator, List, Optional, @@ -9,6 +12,7 @@ Tuple, Type, Union, + cast, ) from langchain_core.load.dump import dumpd @@ -28,6 +32,7 @@ Output, get_unique_config_specs, ) +from langchain_core.utils.aiter import py_anext if TYPE_CHECKING: from langchain_core.callbacks.manager import AsyncCallbackManagerForChainRun @@ -89,6 +94,11 @@ def when_all_is_lost(inputs): Any exception that is not a subclass of these exceptions will be raised immediately. """ + exception_key: Optional[str] = None + """If string is specified then handled exceptions will be passed to fallbacks as + part of the input under the specified key. If None, exceptions + will not be passed to fallbacks. If used, the base runnable and its fallbacks + must accept a dictionary as input.""" class Config: arbitrary_types_allowed = True @@ -136,6 +146,11 @@ def runnables(self) -> Iterator[Runnable[Input, Output]]: def invoke( self, input: Input, config: Optional[RunnableConfig] = None, **kwargs: Any ) -> Output: + if self.exception_key is not None and not isinstance(input, dict): + raise ValueError( + "If 'exception_key' is specified then input must be a dictionary." + f"However found a type of {type(input)} for input" + ) # setup callbacks config = ensure_config(config) callback_manager = get_callback_manager_for_config(config) @@ -144,8 +159,11 @@ def invoke( dumpd(self), input, name=config.get("run_name") ) first_error = None + last_error = None for runnable in self.runnables: try: + if self.exception_key and last_error is not None: + input[self.exception_key] = last_error output = runnable.invoke( input, patch_config(config, callbacks=run_manager.get_child()), @@ -154,6 +172,7 @@ def invoke( except self.exceptions_to_handle as e: if first_error is None: first_error = e + last_error = e except BaseException as e: run_manager.on_chain_error(e) raise e @@ -171,6 +190,11 @@ async def ainvoke( config: Optional[RunnableConfig] = None, **kwargs: Optional[Any], ) -> Output: + if self.exception_key is not None and not isinstance(input, dict): + raise ValueError( + "If 'exception_key' is specified then input must be a dictionary." + f"However found a type of {type(input)} for input" + ) # setup callbacks config = ensure_config(config) callback_manager = get_async_callback_manager_for_config(config) @@ -180,8 +204,11 @@ async def ainvoke( ) first_error = None + last_error = None for runnable in self.runnables: try: + if self.exception_key and last_error is not None: + input[self.exception_key] = last_error output = await runnable.ainvoke( input, patch_config(config, callbacks=run_manager.get_child()), @@ -190,6 +217,7 @@ async def ainvoke( except self.exceptions_to_handle as e: if first_error is None: first_error = e + last_error = e except BaseException as e: await run_manager.on_chain_error(e) raise e @@ -211,8 +239,13 @@ def batch( ) -> List[Output]: from langchain_core.callbacks.manager import CallbackManager - if return_exceptions: - raise NotImplementedError() + if self.exception_key is not None and not all( + isinstance(input, dict) for input in inputs + ): + raise ValueError( + "If 'exception_key' is specified then inputs must be dictionaries." + f"However found a type of {type(inputs[0])} for input" + ) if not inputs: return [] @@ -241,35 +274,51 @@ def batch( for cm, input, config in zip(callback_managers, inputs, configs) ] - first_error = None + to_return: Dict[int, Any] = {} + run_again = {i: input for i, input in enumerate(inputs)} + handled_exceptions: Dict[int, BaseException] = {} + first_to_raise = None for runnable in self.runnables: - try: - outputs = runnable.batch( - inputs, - [ - # each step a child run of the corresponding root run - patch_config(config, callbacks=rm.get_child()) - for rm, config in zip(run_managers, configs) - ], - return_exceptions=return_exceptions, - **kwargs, - ) - except self.exceptions_to_handle as e: - if first_error is None: - first_error = e - except BaseException as e: - for rm in run_managers: - rm.on_chain_error(e) - raise e - else: - for rm, output in zip(run_managers, outputs): - rm.on_chain_end(output) - return outputs - if first_error is None: - raise ValueError("No error stored at end of fallbacks.") - for rm in run_managers: - rm.on_chain_error(first_error) - raise first_error + outputs = runnable.batch( + [input for _, input in sorted(run_again.items())], + [ + # each step a child run of the corresponding root run + patch_config(configs[i], callbacks=run_managers[i].get_child()) + for i in sorted(run_again) + ], + return_exceptions=True, + **kwargs, + ) + for (i, input), output in zip(sorted(run_again.copy().items()), outputs): + if isinstance(output, BaseException) and not isinstance( + output, self.exceptions_to_handle + ): + if not return_exceptions: + first_to_raise = first_to_raise or output + else: + handled_exceptions[i] = cast(BaseException, output) + run_again.pop(i) + elif isinstance(output, self.exceptions_to_handle): + if self.exception_key: + input[self.exception_key] = output # type: ignore + handled_exceptions[i] = cast(BaseException, output) + else: + run_managers[i].on_chain_end(output) + to_return[i] = output + run_again.pop(i) + handled_exceptions.pop(i, None) + if first_to_raise: + raise first_to_raise + if not run_again: + break + + sorted_handled_exceptions = sorted(handled_exceptions.items()) + for i, error in sorted_handled_exceptions: + run_managers[i].on_chain_error(error) + if not return_exceptions and sorted_handled_exceptions: + raise sorted_handled_exceptions[0][1] + to_return.update(handled_exceptions) + return [output for _, output in sorted(to_return.items())] async def abatch( self, @@ -281,8 +330,13 @@ async def abatch( ) -> List[Output]: from langchain_core.callbacks.manager import AsyncCallbackManager - if return_exceptions: - raise NotImplementedError() + if self.exception_key is not None and not all( + isinstance(input, dict) for input in inputs + ): + raise ValueError( + "If 'exception_key' is specified then inputs must be dictionaries." + f"However found a type of {type(inputs[0])} for input" + ) if not inputs: return [] @@ -313,33 +367,169 @@ async def abatch( ) ) + to_return = {} + run_again = {i: input for i, input in enumerate(inputs)} + handled_exceptions: Dict[int, BaseException] = {} + first_to_raise = None + for runnable in self.runnables: + outputs = await runnable.abatch( + [input for _, input in sorted(run_again.items())], + [ + # each step a child run of the corresponding root run + patch_config(configs[i], callbacks=run_managers[i].get_child()) + for i in sorted(run_again) + ], + return_exceptions=True, + **kwargs, + ) + + for (i, input), output in zip(sorted(run_again.copy().items()), outputs): + if isinstance(output, BaseException) and not isinstance( + output, self.exceptions_to_handle + ): + if not return_exceptions: + first_to_raise = first_to_raise or output + else: + handled_exceptions[i] = cast(BaseException, output) + run_again.pop(i) + elif isinstance(output, self.exceptions_to_handle): + if self.exception_key: + input[self.exception_key] = output # type: ignore + handled_exceptions[i] = cast(BaseException, output) + else: + to_return[i] = output + await run_managers[i].on_chain_end(output) + run_again.pop(i) + handled_exceptions.pop(i, None) + + if first_to_raise: + raise first_to_raise + if not run_again: + break + + sorted_handled_exceptions = sorted(handled_exceptions.items()) + await asyncio.gather( + *( + run_managers[i].on_chain_error(error) + for i, error in sorted_handled_exceptions + ) + ) + if not return_exceptions and sorted_handled_exceptions: + raise sorted_handled_exceptions[0][1] + to_return.update(handled_exceptions) + return [output for _, output in sorted(to_return.items())] # type: ignore + + def stream( + self, + input: Input, + config: Optional[RunnableConfig] = None, + **kwargs: Optional[Any], + ) -> Iterator[Output]: + """""" + if self.exception_key is not None and not isinstance(input, dict): + raise ValueError( + "If 'exception_key' is specified then input must be a dictionary." + f"However found a type of {type(input)} for input" + ) + # setup callbacks + config = ensure_config(config) + callback_manager = get_callback_manager_for_config(config) + # start the root run + run_manager = callback_manager.on_chain_start( + dumpd(self), input, name=config.get("run_name") + ) first_error = None + last_error = None for runnable in self.runnables: try: - outputs = await runnable.abatch( - inputs, - [ - # each step a child run of the corresponding root run - patch_config(config, callbacks=rm.get_child()) - for rm, config in zip(run_managers, configs) - ], - return_exceptions=return_exceptions, + if self.exception_key and last_error is not None: + input[self.exception_key] = last_error + stream = runnable.stream( + input, + patch_config(config, callbacks=run_manager.get_child()), **kwargs, ) + chunk = next(stream) except self.exceptions_to_handle as e: - if first_error is None: - first_error = e + first_error = e if first_error is None else first_error + last_error = e except BaseException as e: - await asyncio.gather(*(rm.on_chain_error(e) for rm in run_managers)) + run_manager.on_chain_error(e) + raise e else: - await asyncio.gather( - *( - rm.on_chain_end(output) - for rm, output in zip(run_managers, outputs) - ) + first_error = None + break + if first_error: + run_manager.on_chain_error(first_error) + raise first_error + + yield chunk + output: Optional[Output] = chunk + try: + for chunk in stream: + yield chunk + try: + output = output + chunk # type: ignore + except TypeError: + output = None + except BaseException as e: + run_manager.on_chain_error(e) + raise e + run_manager.on_chain_end(output) + + async def astream( + self, + input: Input, + config: Optional[RunnableConfig] = None, + **kwargs: Optional[Any], + ) -> AsyncIterator[Output]: + if self.exception_key is not None and not isinstance(input, dict): + raise ValueError( + "If 'exception_key' is specified then input must be a dictionary." + f"However found a type of {type(input)} for input" + ) + # setup callbacks + config = ensure_config(config) + callback_manager = get_async_callback_manager_for_config(config) + # start the root run + run_manager = await callback_manager.on_chain_start( + dumpd(self), input, name=config.get("run_name") + ) + first_error = None + last_error = None + for runnable in self.runnables: + try: + if self.exception_key and last_error is not None: + input[self.exception_key] = last_error + stream = runnable.astream( + input, + patch_config(config, callbacks=run_manager.get_child()), + **kwargs, ) - return outputs - if first_error is None: - raise ValueError("No error stored at end of fallbacks.") - await asyncio.gather(*(rm.on_chain_error(first_error) for rm in run_managers)) - raise first_error + chunk = await cast(Awaitable[Output], py_anext(stream)) + except self.exceptions_to_handle as e: + first_error = e if first_error is None else first_error + last_error = e + except BaseException as e: + await run_manager.on_chain_error(e) + raise e + else: + first_error = None + break + if first_error: + await run_manager.on_chain_error(first_error) + raise first_error + + yield chunk + output: Optional[Output] = chunk + try: + async for chunk in stream: + yield chunk + try: + output = output + chunk # type: ignore + except TypeError: + output = None + except BaseException as e: + await run_manager.on_chain_error(e) + raise e + await run_manager.on_chain_end(output) diff --git a/libs/core/langchain_core/runnables/graph_draw.py b/libs/core/langchain_core/runnables/graph_draw.py index be78c9c540181..87facfb92ea79 100644 --- a/libs/core/langchain_core/runnables/graph_draw.py +++ b/libs/core/langchain_core/runnables/graph_draw.py @@ -165,9 +165,11 @@ def _build_sugiyama_layout( EdgeViewer, route_with_lines, ) - except ImportError: - print("Install grandalf to draw graphs. `pip install grandalf`") - raise + except ImportError as exc: + raise ImportError( + "Install grandalf to draw graphs: `pip install grandalf`." + ) from exc + # # Just a reminder about naming conventions: # +------------X diff --git a/libs/core/langchain_core/runnables/retry.py b/libs/core/langchain_core/runnables/retry.py index f619c45f592fc..36a508776ce62 100644 --- a/libs/core/langchain_core/runnables/retry.py +++ b/libs/core/langchain_core/runnables/retry.py @@ -56,14 +56,14 @@ class RunnableRetry(RunnableBindingBase[Input, Output]): def foo(input) -> None: '''Fake function that raises an exception.''' - raise ValueError("Invoking foo failed. At time {time.time()}") + raise ValueError(f"Invoking foo failed. At time {time.time()}") runnable = RunnableLambda(foo) runnable_with_retries = runnable.with_retry( - retry_exception_types=(ValueError,), # Retry only on ValueError + retry_if_exception_type=(ValueError,), # Retry only on ValueError wait_exponential_jitter=True, # Add jitter to the exponential backoff - max_attempt_number=2, # Try twice + stop_after_attempt=2, # Try twice ) # The method invocation above is equivalent to the longer form below: diff --git a/libs/core/langchain_core/runnables/schema.py b/libs/core/langchain_core/runnables/schema.py new file mode 100644 index 0000000000000..b2891b10b11c6 --- /dev/null +++ b/libs/core/langchain_core/runnables/schema.py @@ -0,0 +1,133 @@ +"""Module contains typedefs that are used with runnables.""" +from __future__ import annotations + +from typing import Any, Dict, List + +from typing_extensions import NotRequired, TypedDict + + +class EventData(TypedDict, total=False): + """Data associated with a streaming event.""" + + input: Any + """The input passed to the runnable that generated the event. + + Inputs will sometimes be available at the *START* of the runnable, and + sometimes at the *END* of the runnable. + + If a runnable is able to stream its inputs, then its input by definition + won't be known until the *END* of the runnable when it has finished streaming + its inputs. + """ + output: Any + """The output of the runnable that generated the event. + + Outputs will only be available at the *END* of the runnable. + + For most runnables, this field can be inferred from the `chunk` field, + though there might be some exceptions for special cased runnables (e.g., like + chat models), which may return more information. + """ + chunk: Any + """A streaming chunk from the output that generated the event. + + chunks support addition in general, and adding them up should result + in the output of the runnable that generated the event. + """ + + +class StreamEvent(TypedDict): + """A streaming event. + + Schema of a streaming event which is produced from the astream_events method. + + Example: + + .. code-block:: python + + from langchain_core.runnables import RunnableLambda + + async def reverse(s: str) -> str: + return s[::-1] + + chain = RunnableLambda(func=reverse) + + events = [event async for event in chain.astream_events("hello")] + + # will produce the following events (run_id has been omitted for brevity): + [ + { + "data": {"input": "hello"}, + "event": "on_chain_start", + "metadata": {}, + "name": "reverse", + "tags": [], + }, + { + "data": {"chunk": "olleh"}, + "event": "on_chain_stream", + "metadata": {}, + "name": "reverse", + "tags": [], + }, + { + "data": {"output": "olleh"}, + "event": "on_chain_end", + "metadata": {}, + "name": "reverse", + "tags": [], + }, + ] + """ + + event: str + """Event names are of the format: on_[runnable_type]_(start|stream|end). + + Runnable types are one of: + * llm - used by non chat models + * chat_model - used by chat models + * prompt -- e.g., ChatPromptTemplate + * tool -- from tools defined via @tool decorator or inheriting from Tool/BaseTool + * chain - most Runnables are of this type + + Further, the events are categorized as one of: + * start - when the runnable starts + * stream - when the runnable is streaming + * end - when the runnable ends + + start, stream and end are associated with slightly different `data` payload. + + Please see the documentation for `EventData` for more details. + """ + name: str + """The name of the runnable that generated the event.""" + run_id: str + """An randomly generated ID to keep track of the execution of the given runnable. + + Each child runnable that gets invoked as part of the execution of a parent runnable + is assigned its own unique ID. + """ + tags: NotRequired[List[str]] + """Tags associated with the runnable that generated this event. + + Tags are always inherited from parent runnables. + + Tags can either be bound to a runnable using `.with_config({"tags": ["hello"]})` + or passed at run time using `.astream_events(..., {"tags": ["hello"]})`. + """ + metadata: NotRequired[Dict[str, Any]] + """Metadata associated with the runnable that generated this event. + + Metadata can either be bound to a runnable using + + `.with_config({"metadata": { "foo": "bar" }})` + + or passed at run time using + + `.astream_events(..., {"metadata": {"foo": "bar"}})`. + """ + data: EventData + """Event data. + + The contents of the event data depend on the event type. + """ diff --git a/libs/core/langchain_core/runnables/utils.py b/libs/core/langchain_core/runnables/utils.py index bd629194b6c03..cb8f21e29ce0f 100644 --- a/libs/core/langchain_core/runnables/utils.py +++ b/libs/core/langchain_core/runnables/utils.py @@ -1,3 +1,4 @@ +"""Utility code for runnables.""" from __future__ import annotations import ast @@ -24,6 +25,8 @@ Union, ) +from langchain_core.runnables.schema import StreamEvent + Input = TypeVar("Input", contravariant=True) # Output type should implement __concat__, as eg str, list, dict do Output = TypeVar("Output", covariant=True) @@ -245,8 +248,12 @@ def get_function_nonlocals(func: Callable) -> List[Any]: if "." in kk and kk.startswith(k): vv = v for part in kk.split(".")[1:]: - vv = getattr(vv, part) - values.append(vv) + if vv is None: + break + else: + vv = getattr(vv, part) + else: + values.append(vv) return values except (SyntaxError, TypeError, OSError): return [] @@ -419,3 +426,58 @@ def get_unique_config_specs( f"for {id}: {[first] + others}" ) return unique + + +class _RootEventFilter: + def __init__( + self, + *, + include_names: Optional[Sequence[str]] = None, + include_types: Optional[Sequence[str]] = None, + include_tags: Optional[Sequence[str]] = None, + exclude_names: Optional[Sequence[str]] = None, + exclude_types: Optional[Sequence[str]] = None, + exclude_tags: Optional[Sequence[str]] = None, + ) -> None: + """Utility to filter the root event in the astream_events implementation. + + This is simply binding the arguments to the namespace to make save on + a bit of typing in the astream_events implementation. + """ + self.include_names = include_names + self.include_types = include_types + self.include_tags = include_tags + self.exclude_names = exclude_names + self.exclude_types = exclude_types + self.exclude_tags = exclude_tags + + def include_event(self, event: StreamEvent, root_type: str) -> bool: + """Determine whether to include an event.""" + if ( + self.include_names is None + and self.include_types is None + and self.include_tags is None + ): + include = True + else: + include = False + + event_tags = event.get("tags") or [] + + if self.include_names is not None: + include = include or event["name"] in self.include_names + if self.include_types is not None: + include = include or root_type in self.include_types + if self.include_tags is not None: + include = include or any(tag in self.include_tags for tag in event_tags) + + if self.exclude_names is not None: + include = include and event["name"] not in self.exclude_names + if self.exclude_types is not None: + include = include and root_type not in self.exclude_types + if self.exclude_tags is not None: + include = include and all( + tag not in self.exclude_tags for tag in event_tags + ) + + return include diff --git a/libs/core/langchain_core/sys_info.py b/libs/core/langchain_core/sys_info.py index c4ffd798614c9..467d125616fa6 100644 --- a/libs/core/langchain_core/sys_info.py +++ b/libs/core/langchain_core/sys_info.py @@ -4,16 +4,32 @@ def print_sys_info(*, additional_pkgs: Sequence[str] = tuple()) -> None: """Print information about the environment for debugging purposes.""" + import pkgutil import platform import sys from importlib import metadata, util - packages = [ - "langchain_core", - "langchain", - "langchain_community", + # Packages that do not start with "langchain" prefix. + other_langchain_packages = [ "langserve", - ] + list(additional_pkgs) + "langgraph", + ] + + langchain_pkgs = [ + name for _, name, _ in pkgutil.iter_modules() if name.startswith("langchain") + ] + + all_packages = sorted( + set(langchain_pkgs + other_langchain_packages + list(additional_pkgs)) + ) + + # Always surface these packages to the top + order_by = ["langchain_core", "langchain", "langchain_community"] + + for pkg in reversed(order_by): + if pkg in all_packages: + all_packages.remove(pkg) + all_packages = [pkg] + list(all_packages) system_info = { "OS": platform.system(), @@ -32,13 +48,15 @@ def print_sys_info(*, additional_pkgs: Sequence[str] = tuple()) -> None: print("Package Information") print("-------------------") - for pkg in packages: + not_installed = [] + + for pkg in all_packages: try: found_package = util.find_spec(pkg) except Exception: found_package = None if found_package is None: - print(f"> {pkg}: Not Found") + not_installed.append(pkg) continue # Package version @@ -51,7 +69,16 @@ def print_sys_info(*, additional_pkgs: Sequence[str] = tuple()) -> None: if package_version is not None: print(f"> {pkg}: {package_version}") else: - print(f"> {pkg}: Found") + print(f"> {pkg}: Installed. No version info available.") + + if not_installed: + print() + print("Packages not installed (Not Necessarily a Problem)") + print("--------------------------------------------------") + print("The following packages were not found:") + print() + for pkg in not_installed: + print(f"> {pkg}") if __name__ == "__main__": diff --git a/libs/core/langchain_core/tools.py b/libs/core/langchain_core/tools.py index 50cf500b93b1f..a536da4c79121 100644 --- a/libs/core/langchain_core/tools.py +++ b/libs/core/langchain_core/tools.py @@ -300,7 +300,7 @@ def _to_args_and_kwargs(self, tool_input: Union[str, Dict]) -> Tuple[Tuple, Dict def run( self, - tool_input: Union[str, Dict], + tool_input: Union[str, Dict[str, Any]], verbose: Optional[bool] = None, start_color: Optional[str] = "green", color: Optional[str] = "green", @@ -312,7 +312,6 @@ def run( **kwargs: Any, ) -> Any: """Run the tool.""" - parsed_input = self._parse_input(tool_input) if not self.verbose and verbose is not None: verbose_ = verbose else: @@ -333,9 +332,15 @@ def run( tool_input if isinstance(tool_input, str) else str(tool_input), color=start_color, name=run_name, + # Inputs by definition should always be dicts. + # For now, it's unclear whether this assumption is ever violated, + # but if it is we will send a `None` value to the callback instead + # And will need to address issue via a patch. + inputs=None if isinstance(tool_input, str) else tool_input, **kwargs, ) try: + parsed_input = self._parse_input(tool_input) tool_args, tool_kwargs = self._to_args_and_kwargs(parsed_input) observation = ( self._run(*tool_args, run_manager=run_manager, **tool_kwargs) @@ -387,7 +392,6 @@ async def arun( **kwargs: Any, ) -> Any: """Run the tool asynchronously.""" - parsed_input = self._parse_input(tool_input) if not self.verbose and verbose is not None: verbose_ = verbose else: @@ -407,9 +411,11 @@ async def arun( tool_input if isinstance(tool_input, str) else str(tool_input), color=start_color, name=run_name, + inputs=tool_input, **kwargs, ) try: + parsed_input = self._parse_input(tool_input) # We then call the tool on the tool input to get an observation tool_args, tool_kwargs = self._to_args_and_kwargs(parsed_input) observation = ( diff --git a/libs/core/langchain_core/tracers/base.py b/libs/core/langchain_core/tracers/base.py index 8b93c42d7c2b1..0d4c4213135bc 100644 --- a/libs/core/langchain_core/tracers/base.py +++ b/libs/core/langchain_core/tracers/base.py @@ -11,8 +11,10 @@ Any, Dict, List, + Literal, Optional, Sequence, + Set, Union, cast, ) @@ -23,6 +25,7 @@ from langchain_core.callbacks.base import BaseCallbackHandler from langchain_core.exceptions import TracerException from langchain_core.load import dumpd +from langchain_core.messages import BaseMessage from langchain_core.outputs import ( ChatGeneration, ChatGenerationChunk, @@ -40,8 +43,29 @@ class BaseTracer(BaseCallbackHandler, ABC): """Base interface for tracers.""" - def __init__(self, **kwargs: Any) -> None: + def __init__( + self, + *, + _schema_format: Literal["original", "streaming_events"] = "original", + **kwargs: Any, + ) -> None: + """Initialize the tracer. + + Args: + _schema_format: Primarily changes how the inputs and outputs are + handled. For internal use only. This API will change. + - 'original' is the format used by all current tracers. + This format is slightly inconsistent with respect to inputs + and outputs. + - 'streaming_events' is used for supporting streaming events, + for internal usage. It will likely change in the future, or + be deprecated entirely in favor of a dedicated async tracer + for streaming events. + kwargs: Additional keyword arguments that will be passed to + the super class. + """ super().__init__(**kwargs) + self._schema_format = _schema_format # For internal use only API will change. self.run_map: Dict[str, Run] = {} @staticmethod @@ -134,17 +158,76 @@ def _get_execution_order(self, parent_run_id: Optional[str] = None) -> int: return parent_run.child_execution_order + 1 - def _get_run(self, run_id: UUID, run_type: Optional[str] = None) -> Run: + def _get_run( + self, run_id: UUID, run_type: Union[str, Set[str], None] = None + ) -> Run: try: run = self.run_map[str(run_id)] except KeyError as exc: raise TracerException(f"No indexed run ID {run_id}.") from exc - if run_type is not None and run.run_type != run_type: + + if isinstance(run_type, str): + run_types: Union[Set[str], None] = {run_type} + else: + run_types = run_type + if run_types is not None and run.run_type not in run_types: raise TracerException( - f"Found {run.run_type} run at ID {run_id}, but expected {run_type} run." + f"Found {run.run_type} run at ID {run_id}, " + f"but expected {run_types} run." ) return run + def on_chat_model_start( + self, + serialized: Dict[str, Any], + messages: List[List[BaseMessage]], + *, + run_id: UUID, + tags: Optional[List[str]] = None, + parent_run_id: Optional[UUID] = None, + metadata: Optional[Dict[str, Any]] = None, + name: Optional[str] = None, + **kwargs: Any, + ) -> Run: + """Start a trace for an LLM run.""" + if self._schema_format != "streaming_events": + # Please keep this un-implemented for backwards compatibility. + # When it's unimplemented old tracers that use the "original" format + # fallback on the on_llm_start method implementation if they + # find that the on_chat_model_start method is not implemented. + # This can eventually be cleaned up by writing a "modern" tracer + # that has all the updated schema changes corresponding to + # the "streaming_events" format. + raise NotImplementedError( + f"Chat model tracing is not supported in " + f"for {self._schema_format} format." + ) + parent_run_id_ = str(parent_run_id) if parent_run_id else None + execution_order = self._get_execution_order(parent_run_id_) + start_time = datetime.now(timezone.utc) + if metadata: + kwargs.update({"metadata": metadata}) + chat_model_run = Run( + id=run_id, + parent_run_id=parent_run_id, + serialized=serialized, + inputs={"messages": [[dumpd(msg) for msg in batch] for batch in messages]}, + extra=kwargs, + events=[{"name": "start", "time": start_time}], + start_time=start_time, + execution_order=execution_order, + child_execution_order=execution_order, + # WARNING: This is valid ONLY for streaming_events. + # run_type="llm" is what's used by virtually all tracers. + # Changing this to "chat_model" may break triggering on_llm_start + run_type="chat_model", + tags=tags, + name=name, + ) + self._start_trace(chat_model_run) + self._on_chat_model_start(chat_model_run) + return chat_model_run + def on_llm_start( self, serialized: Dict[str, Any], @@ -167,6 +250,7 @@ def on_llm_start( id=run_id, parent_run_id=parent_run_id, serialized=serialized, + # TODO: Figure out how to expose kwargs here inputs={"prompts": prompts}, extra=kwargs, events=[{"name": "start", "time": start_time}], @@ -191,7 +275,9 @@ def on_llm_new_token( **kwargs: Any, ) -> Run: """Run on new LLM token. Only available when streaming is enabled.""" - llm_run = self._get_run(run_id, run_type="llm") + # "chat_model" is only used for the experimental new streaming_events format. + # This change should not affect any existing tracers. + llm_run = self._get_run(run_id, run_type={"llm", "chat_model"}) event_kwargs: Dict[str, Any] = {"token": token} if chunk: event_kwargs["chunk"] = chunk @@ -238,7 +324,9 @@ def on_retry( def on_llm_end(self, response: LLMResult, *, run_id: UUID, **kwargs: Any) -> Run: """End a trace for an LLM run.""" - llm_run = self._get_run(run_id, run_type="llm") + # "chat_model" is only used for the experimental new streaming_events format. + # This change should not affect any existing tracers. + llm_run = self._get_run(run_id, run_type={"llm", "chat_model"}) llm_run.outputs = response.dict() for i, generations in enumerate(response.generations): for j, generation in enumerate(generations): @@ -261,7 +349,9 @@ def on_llm_error( **kwargs: Any, ) -> Run: """Handle an error for an LLM run.""" - llm_run = self._get_run(run_id, run_type="llm") + # "chat_model" is only used for the experimental new streaming_events format. + # This change should not affect any existing tracers. + llm_run = self._get_run(run_id, run_type={"llm", "chat_model"}) llm_run.error = self._get_stacktrace(error) llm_run.end_time = datetime.now(timezone.utc) llm_run.events.append({"name": "error", "time": llm_run.end_time}) @@ -292,7 +382,7 @@ def on_chain_start( id=run_id, parent_run_id=parent_run_id, serialized=serialized, - inputs=inputs if isinstance(inputs, dict) else {"input": inputs}, + inputs=self._get_chain_inputs(inputs), extra=kwargs, events=[{"name": "start", "time": start_time}], start_time=start_time, @@ -307,6 +397,28 @@ def on_chain_start( self._on_chain_start(chain_run) return chain_run + def _get_chain_inputs(self, inputs: Any) -> Any: + """Get the inputs for a chain run.""" + if self._schema_format == "original": + return inputs if isinstance(inputs, dict) else {"input": inputs} + elif self._schema_format == "streaming_events": + return { + "input": inputs, + } + else: + raise ValueError(f"Invalid format: {self._schema_format}") + + def _get_chain_outputs(self, outputs: Any) -> Any: + """Get the outputs for a chain run.""" + if self._schema_format == "original": + return outputs if isinstance(outputs, dict) else {"output": outputs} + elif self._schema_format == "streaming_events": + return { + "output": outputs, + } + else: + raise ValueError(f"Invalid format: {self._schema_format}") + def on_chain_end( self, outputs: Dict[str, Any], @@ -317,13 +429,11 @@ def on_chain_end( ) -> Run: """End a trace for a chain run.""" chain_run = self._get_run(run_id) - chain_run.outputs = ( - outputs if isinstance(outputs, dict) else {"output": outputs} - ) + chain_run.outputs = self._get_chain_outputs(outputs) chain_run.end_time = datetime.now(timezone.utc) chain_run.events.append({"name": "end", "time": chain_run.end_time}) if inputs is not None: - chain_run.inputs = inputs if isinstance(inputs, dict) else {"input": inputs} + chain_run.inputs = self._get_chain_inputs(inputs) self._end_trace(chain_run) self._on_chain_end(chain_run) return chain_run @@ -342,7 +452,7 @@ def on_chain_error( chain_run.end_time = datetime.now(timezone.utc) chain_run.events.append({"name": "error", "time": chain_run.end_time}) if inputs is not None: - chain_run.inputs = inputs if isinstance(inputs, dict) else {"input": inputs} + chain_run.inputs = self._get_chain_inputs(inputs) self._end_trace(chain_run) self._on_chain_error(chain_run) return chain_run @@ -357,6 +467,7 @@ def on_tool_start( parent_run_id: Optional[UUID] = None, metadata: Optional[Dict[str, Any]] = None, name: Optional[str] = None, + inputs: Optional[Dict[str, Any]] = None, **kwargs: Any, ) -> Run: """Start a trace for a tool run.""" @@ -365,11 +476,20 @@ def on_tool_start( start_time = datetime.now(timezone.utc) if metadata: kwargs.update({"metadata": metadata}) + + if self._schema_format == "original": + inputs = {"input": input_str} + elif self._schema_format == "streaming_events": + inputs = {"input": inputs} + else: + raise AssertionError(f"Invalid format: {self._schema_format}") + tool_run = Run( id=run_id, parent_run_id=parent_run_id, serialized=serialized, - inputs={"input": input_str}, + # Wrapping in dict since Run requires a dict object. + inputs=inputs, extra=kwargs, events=[{"name": "start", "time": start_time}], start_time=start_time, diff --git a/libs/core/langchain_core/tracers/langchain.py b/libs/core/langchain_core/tracers/langchain.py index d5daf108085f5..947e10f7e6be1 100644 --- a/libs/core/langchain_core/tracers/langchain.py +++ b/libs/core/langchain_core/tracers/langchain.py @@ -112,7 +112,7 @@ def on_chat_model_start( metadata: Optional[Dict[str, Any]] = None, name: Optional[str] = None, **kwargs: Any, - ) -> None: + ) -> Run: """Start a trace for an LLM run.""" parent_run_id_ = str(parent_run_id) if parent_run_id else None execution_order = self._get_execution_order(parent_run_id_) @@ -135,6 +135,7 @@ def on_chat_model_start( ) self._start_trace(chat_model_run) self._on_chat_model_start(chat_model_run) + return chat_model_run def _persist_run(self, run: Run) -> None: run_ = run.copy() diff --git a/libs/core/langchain_core/tracers/log_stream.py b/libs/core/langchain_core/tracers/log_stream.py index 9287ce8875ef2..72d7f590a2fde 100644 --- a/libs/core/langchain_core/tracers/log_stream.py +++ b/libs/core/langchain_core/tracers/log_stream.py @@ -1,5 +1,6 @@ from __future__ import annotations +import asyncio import copy import math import threading @@ -9,19 +10,24 @@ AsyncIterator, Dict, List, + Literal, Optional, Sequence, - TypedDict, TypeVar, Union, + overload, ) from uuid import UUID import jsonpatch # type: ignore[import] from anyio import create_memory_object_stream +from typing_extensions import NotRequired, TypedDict +from langchain_core.load import dumps from langchain_core.load.load import load from langchain_core.outputs import ChatGenerationChunk, GenerationChunk +from langchain_core.runnables import Runnable, RunnableConfig, ensure_config +from langchain_core.runnables.utils import Input, Output from langchain_core.tracers.base import BaseTracer from langchain_core.tracers.schemas import Run @@ -46,8 +52,11 @@ class LogEntry(TypedDict): """List of LLM tokens streamed by this run, if applicable.""" streamed_output: List[Any] """List of output chunks streamed by this run, if available.""" + inputs: NotRequired[Optional[Any]] + """Inputs to this run. Not available currently via astream_log.""" final_output: Optional[Any] - """Final output of this run. + """Final output of this run. + Only available after the run has finished successfully.""" end_time: Optional[str] """ISO-8601 timestamp of when the run ended. @@ -65,6 +74,14 @@ class RunState(TypedDict): """Final output of the run, usually the result of aggregating (`+`) streamed_output. Updated throughout the run when supported by the Runnable.""" + name: str + """Name of the object being run.""" + type: str + """Type of the object being run, eg. prompt, chain, llm, etc.""" + + # Do we want tags/metadata on the root run? Client kinda knows it in most situations + # tags: List[str] + logs: Dict[str, LogEntry] """Map of run names to sub-runs. If filters were supplied, this list will contain only the runs that matched the filters.""" @@ -128,6 +145,15 @@ def __repr__(self) -> str: return f"RunLog({pformat(self.state)})" + def __eq__(self, other: object) -> bool: + # First compare that the state is the same + if not isinstance(other, RunLog): + return False + if self.state != other.state: + return False + # Then compare that the ops are the same + return super().__eq__(other) + T = TypeVar("T") @@ -145,8 +171,36 @@ def __init__( exclude_names: Optional[Sequence[str]] = None, exclude_types: Optional[Sequence[str]] = None, exclude_tags: Optional[Sequence[str]] = None, + # Schema format is for internal use only. + _schema_format: Literal["original", "streaming_events"] = "streaming_events", ) -> None: - super().__init__() + """A tracer that streams run logs to a stream. + + Args: + auto_close: Whether to close the stream when the root run finishes. + include_names: Only include runs from Runnables with matching names. + include_types: Only include runs from Runnables with matching types. + include_tags: Only include runs from Runnables with matching tags. + exclude_names: Exclude runs from Runnables with matching names. + exclude_types: Exclude runs from Runnables with matching types. + exclude_tags: Exclude runs from Runnables with matching tags. + _schema_format: Primarily changes how the inputs and outputs are + handled. + **For internal use only. This API will change.** + - 'original' is the format used by all current tracers. + This format is slightly inconsistent with respect to inputs + and outputs. + - 'streaming_events' is used for supporting streaming events, + for internal usage. It will likely change in the future, or + be deprecated entirely in favor of a dedicated async tracer + for streaming events. + """ + if _schema_format not in {"original", "streaming_events"}: + raise ValueError( + f"Invalid schema format: {_schema_format}. " + f"Expected one of 'original', 'streaming_events'." + ) + super().__init__(_schema_format=_schema_format) self.auto_close = auto_close self.include_names = include_names @@ -241,6 +295,8 @@ def _on_run_create(self, run: Run) -> None: streamed_output=[], final_output=None, logs={}, + name=run.name, + type=run.run_type, ), } ) @@ -257,24 +313,30 @@ def _on_run_create(self, run: Run) -> None: run.name if count == 1 else f"{run.name}:{count}" ) + entry = LogEntry( + id=str(run.id), + name=run.name, + type=run.run_type, + tags=run.tags or [], + metadata=(run.extra or {}).get("metadata", {}), + start_time=run.start_time.isoformat(timespec="milliseconds"), + streamed_output=[], + streamed_output_str=[], + final_output=None, + end_time=None, + ) + + if self._schema_format == "streaming_events": + # If using streaming events let's add inputs as well + entry["inputs"] = _get_standardized_inputs(run, self._schema_format) + # Add the run to the stream self.send_stream.send_nowait( RunLogPatch( { "op": "add", "path": f"/logs/{self._key_map_by_run_id[run.id]}", - "value": LogEntry( - id=str(run.id), - name=run.name, - type=run.run_type, - tags=run.tags or [], - metadata=(run.extra or {}).get("metadata", {}), - start_time=run.start_time.isoformat(timespec="milliseconds"), - streamed_output=[], - streamed_output_str=[], - final_output=None, - end_time=None, - ), + "value": entry, } ) ) @@ -287,13 +349,28 @@ def _on_run_update(self, run: Run) -> None: if index is None: return - self.send_stream.send_nowait( - RunLogPatch( + ops = [] + + if self._schema_format == "streaming_events": + ops.append( + { + "op": "replace", + "path": f"/logs/{index}/inputs", + "value": _get_standardized_inputs(run, self._schema_format), + } + ) + + ops.extend( + [ + # Replace 'inputs' with final inputs + # This is needed because in many cases the inputs are not + # known until after the run is finished and the entire + # input stream has been processed by the runnable. { "op": "add", "path": f"/logs/{index}/final_output", # to undo the dumpd done by some runnables / tracer / etc - "value": load(run.outputs), + "value": _get_standardized_outputs(run, self._schema_format), }, { "op": "add", @@ -302,8 +379,10 @@ def _on_run_update(self, run: Run) -> None: if run.end_time is not None else None, }, - ) + ] ) + + self.send_stream.send_nowait(RunLogPatch(*ops)) finally: if run.id == self.root_id: if self.auto_close: @@ -337,3 +416,197 @@ def _on_llm_new_token( }, ) ) + + +def _get_standardized_inputs( + run: Run, schema_format: Literal["original", "streaming_events"] +) -> Optional[Dict[str, Any]]: + """Extract standardized inputs from a run. + + Standardizes the inputs based on the type of the runnable used. + + Args: + run: Run object + schema_format: The schema format to use. + + Returns: + Valid inputs are only dict. By conventions, inputs always represented + invocation using named arguments. + A None means that the input is not yet known! + """ + if schema_format == "original": + raise NotImplementedError( + "Do not assign inputs with original schema drop the key for now." + "When inputs are added to astream_log they should be added with " + "standardized schema for streaming events." + ) + + inputs = load(run.inputs) + + if run.run_type in {"retriever", "llm", "chat_model"}: + return inputs + + # new style chains + # These nest an additional 'input' key inside the 'inputs' to make sure + # the input is always a dict. We need to unpack and user the inner value. + inputs = inputs["input"] + # We should try to fix this in Runnables and callbacks/tracers + # Runnables should be using a None type here not a placeholder + # dict. + if inputs == {"input": ""}: # Workaround for Runnables not using None + # The input is not known, so we don't assign data['input'] + return None + return inputs + + +def _get_standardized_outputs( + run: Run, schema_format: Literal["original", "streaming_events"] +) -> Optional[Any]: + """Extract standardized output from a run. + + Standardizes the outputs based on the type of the runnable used. + + Args: + log: The log entry. + schema_format: The schema format to use. + + Returns: + An output if returned, otherwise a None + """ + outputs = load(run.outputs) + if schema_format == "original": + # Return the old schema, without standardizing anything + return outputs + + if run.run_type in {"retriever", "llm", "chat_model"}: + return outputs + + if isinstance(outputs, dict): + return outputs.get("output", None) + + return None + + +@overload +def _astream_log_implementation( + runnable: Runnable[Input, Output], + input: Any, + config: Optional[RunnableConfig] = None, + *, + stream: LogStreamCallbackHandler, + diff: Literal[True] = True, + with_streamed_output_list: bool = True, + **kwargs: Any, +) -> AsyncIterator[RunLogPatch]: + ... + + +@overload +def _astream_log_implementation( + runnable: Runnable[Input, Output], + input: Any, + config: Optional[RunnableConfig] = None, + *, + stream: LogStreamCallbackHandler, + diff: Literal[False], + with_streamed_output_list: bool = True, + **kwargs: Any, +) -> AsyncIterator[RunLog]: + ... + + +async def _astream_log_implementation( + runnable: Runnable[Input, Output], + input: Any, + config: Optional[RunnableConfig] = None, + *, + stream: LogStreamCallbackHandler, + diff: bool = True, + with_streamed_output_list: bool = True, + **kwargs: Any, +) -> Union[AsyncIterator[RunLogPatch], AsyncIterator[RunLog]]: + """Implementation of astream_log for a given runnable. + + The implementation has been factored out (at least temporarily) as both + astream_log and astream_events relies on it. + """ + import jsonpatch # type: ignore[import] + + from langchain_core.callbacks.base import BaseCallbackManager + from langchain_core.tracers.log_stream import ( + RunLog, + RunLogPatch, + ) + + # Assign the stream handler to the config + config = ensure_config(config) + callbacks = config.get("callbacks") + if callbacks is None: + config["callbacks"] = [stream] + elif isinstance(callbacks, list): + config["callbacks"] = callbacks + [stream] + elif isinstance(callbacks, BaseCallbackManager): + callbacks = callbacks.copy() + callbacks.add_handler(stream, inherit=True) + config["callbacks"] = callbacks + else: + raise ValueError( + f"Unexpected type for callbacks: {callbacks}." + "Expected None, list or AsyncCallbackManager." + ) + + # Call the runnable in streaming mode, + # add each chunk to the output stream + async def consume_astream() -> None: + try: + prev_final_output: Optional[Output] = None + final_output: Optional[Output] = None + + async for chunk in runnable.astream(input, config, **kwargs): + prev_final_output = final_output + if final_output is None: + final_output = chunk + else: + try: + final_output = final_output + chunk # type: ignore + except TypeError: + final_output = chunk + patches: List[Dict[str, Any]] = [] + if with_streamed_output_list: + patches.append( + { + "op": "add", + "path": "/streamed_output/-", + # chunk cannot be shared between + # streamed_output and final_output + # otherwise jsonpatch.apply will + # modify both + "value": copy.deepcopy(chunk), + } + ) + for op in jsonpatch.JsonPatch.from_diff( + prev_final_output, final_output, dumps=dumps + ): + patches.append({**op, "path": f"/final_output{op['path']}"}) + await stream.send_stream.send(RunLogPatch(*patches)) + finally: + await stream.send_stream.aclose() + + # Start the runnable in a task, so we can start consuming output + task = asyncio.create_task(consume_astream()) + try: + # Yield each chunk from the output stream + if diff: + async for log in stream: + yield log + else: + state = RunLog(state=None) # type: ignore[arg-type] + async for log in stream: + state = state + log + yield state + finally: + # Wait for the runnable to finish, if not cancelled (eg. by break) + try: + await task + except asyncio.CancelledError: + pass diff --git a/libs/core/langchain_core/utils/_merge.py b/libs/core/langchain_core/utils/_merge.py new file mode 100644 index 0000000000000..e21fdd96621fe --- /dev/null +++ b/libs/core/langchain_core/utils/_merge.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +from typing import Any, Dict + + +def merge_dicts(left: Dict[str, Any], right: Dict[str, Any]) -> Dict[str, Any]: + """Merge two dicts, handling specific scenarios where a key exists in both + dictionaries but has a value of None in 'left'. In such cases, the method uses the + value from 'right' for that key in the merged dictionary. + + Example: + If left = {"function_call": {"arguments": None}} and + right = {"function_call": {"arguments": "{\n"}} + then, after merging, for the key "function_call", + the value from 'right' is used, + resulting in merged = {"function_call": {"arguments": "{\n"}}. + """ + merged = left.copy() + for k, v in right.items(): + if k not in merged: + merged[k] = v + elif merged[k] is None and v: + merged[k] = v + elif v is None: + continue + elif merged[k] == v: + continue + elif type(merged[k]) != type(v): + raise TypeError( + f'additional_kwargs["{k}"] already exists in this message,' + " but with a different type." + ) + elif isinstance(merged[k], str): + merged[k] += v + elif isinstance(merged[k], dict): + merged[k] = merge_dicts(merged[k], v) + elif isinstance(merged[k], list): + merged[k] = merged[k] + v + else: + raise TypeError( + f"Additional kwargs key {k} already exists in left dict and value has " + f"unsupported type {type(merged[k])}." + ) + return merged diff --git a/libs/core/langchain_core/utils/function_calling.py b/libs/core/langchain_core/utils/function_calling.py index b943eeaef5e8e..0646a8aa43241 100644 --- a/libs/core/langchain_core/utils/function_calling.py +++ b/libs/core/langchain_core/utils/function_calling.py @@ -28,7 +28,7 @@ class FunctionDescription(TypedDict): - """Representation of a callable function to the OpenAI API.""" + """Representation of a callable function to send to an LLM.""" name: str """The name of the function.""" diff --git a/libs/core/poetry.lock b/libs/core/poetry.lock index 6fa23fd5b4f79..8d8b5159d2e04 100644 --- a/libs/core/poetry.lock +++ b/libs/core/poetry.lock @@ -1124,13 +1124,13 @@ files = [ [[package]] name = "langsmith" -version = "0.0.65" +version = "0.0.83" description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform." optional = false python-versions = ">=3.8.1,<4.0" files = [ - {file = "langsmith-0.0.65-py3-none-any.whl", hash = "sha256:92450957d1c6b6be814f9b726f3bc751deca684535fb404508ccad7aec1bb049"}, - {file = "langsmith-0.0.65.tar.gz", hash = "sha256:ef20e2e32392fb1a0fc5d171e8de595d868b4153a10cc119d7bf8418192c06b6"}, + {file = "langsmith-0.0.83-py3-none-any.whl", hash = "sha256:a5bb7ac58c19a415a9d5f51db56dd32ee2cd7343a00825bbc2018312eb3d122a"}, + {file = "langsmith-0.0.83.tar.gz", hash = "sha256:94427846b334ad9bdbec3266fee12903fe9f5448f628667689d0412012aaf392"}, ] [package.dependencies] @@ -2745,4 +2745,4 @@ extended-testing = ["jinja2"] [metadata] lock-version = "2.0" python-versions = ">=3.8.1,<4.0" -content-hash = "941239bad030d610728f204c17ea49ddbb6fc72e55dadf1691f317f4795c7b1f" +content-hash = "6cd163ca8c15acc3053e17fa86acce4c27c2d737ebcad633db93c0d7aa3a4a53" diff --git a/libs/core/pyproject.toml b/libs/core/pyproject.toml index 0c7206145a746..7c12a8ea300f4 100644 --- a/libs/core/pyproject.toml +++ b/libs/core/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "langchain-core" -version = "0.1.10" +version = "0.1.15" description = "Building applications with LLMs through composability" authors = [] license = "MIT" @@ -11,7 +11,7 @@ repository = "https://github.com/langchain-ai/langchain" [tool.poetry.dependencies] python = ">=3.8.1,<4.0" pydantic = ">=1,<3" -langsmith = "~0.0.63" +langsmith = ">=0.0.83,<0.1" tenacity = "^8.1.0" jsonpatch = "^1.33" anyio = ">=3,<5" diff --git a/libs/core/tests/unit_tests/_api/test_deprecation.py b/libs/core/tests/unit_tests/_api/test_deprecation.py index dc7f40de133d8..d26b18c3ad448 100644 --- a/libs/core/tests/unit_tests/_api/test_deprecation.py +++ b/libs/core/tests/unit_tests/_api/test_deprecation.py @@ -1,3 +1,4 @@ +import inspect import warnings from typing import Any, Dict @@ -74,6 +75,12 @@ def deprecated_function() -> str: return "This is a deprecated function." +@deprecated(since="2.0.0", removal="3.0.0", pending=False) +async def deprecated_async_function() -> str: + """original doc""" + return "This is a deprecated async function." + + class ClassWithDeprecatedMethods: def __init__(self) -> None: """original doc""" @@ -84,6 +91,11 @@ def deprecated_method(self) -> str: """original doc""" return "This is a deprecated method." + @deprecated(since="2.0.0", removal="3.0.0") + async def deprecated_async_method(self) -> str: + """original doc""" + return "This is a deprecated async method." + @classmethod @deprecated(since="2.0.0", removal="3.0.0") def deprecated_classmethod(cls) -> str: @@ -119,6 +131,30 @@ def test_deprecated_function() -> None: assert isinstance(doc, str) assert doc.startswith("[*Deprecated*] original doc") + assert not inspect.iscoroutinefunction(deprecated_function) + + +@pytest.mark.asyncio +async def test_deprecated_async_function() -> None: + """Test deprecated async function.""" + with warnings.catch_warnings(record=True) as warning_list: + warnings.simplefilter("always") + assert ( + await deprecated_async_function() == "This is a deprecated async function." + ) + assert len(warning_list) == 1 + warning = warning_list[0].message + assert str(warning) == ( + "The function `deprecated_async_function` was deprecated " + "in LangChain 2.0.0 and will be removed in 3.0.0" + ) + + doc = deprecated_function.__doc__ + assert isinstance(doc, str) + assert doc.startswith("[*Deprecated*] original doc") + + assert inspect.iscoroutinefunction(deprecated_async_function) + def test_deprecated_method() -> None: """Test deprecated method.""" @@ -137,6 +173,31 @@ def test_deprecated_method() -> None: assert isinstance(doc, str) assert doc.startswith("[*Deprecated*] original doc") + assert not inspect.iscoroutinefunction(obj.deprecated_method) + + +@pytest.mark.asyncio +async def test_deprecated_async_method() -> None: + """Test deprecated async method.""" + with warnings.catch_warnings(record=True) as warning_list: + warnings.simplefilter("always") + obj = ClassWithDeprecatedMethods() + assert ( + await obj.deprecated_async_method() == "This is a deprecated async method." + ) + assert len(warning_list) == 1 + warning = warning_list[0].message + assert str(warning) == ( + "The function `deprecated_async_method` was deprecated in " + "LangChain 2.0.0 and will be removed in 3.0.0" + ) + + doc = obj.deprecated_method.__doc__ + assert isinstance(doc, str) + assert doc.startswith("[*Deprecated*] original doc") + + assert inspect.iscoroutinefunction(obj.deprecated_async_method) + def test_deprecated_classmethod() -> None: """Test deprecated classmethod.""" diff --git a/libs/core/tests/unit_tests/fake/chat_model.py b/libs/core/tests/unit_tests/fake/chat_model.py index 717ab02533f37..98f05b6ca6060 100644 --- a/libs/core/tests/unit_tests/fake/chat_model.py +++ b/libs/core/tests/unit_tests/fake/chat_model.py @@ -1,15 +1,21 @@ -"""Fake ChatModel for testing purposes.""" +"""Fake Chat Model wrapper for testing purposes.""" import asyncio +import re import time -from typing import Any, AsyncIterator, Dict, Iterator, List, Optional, Union +from typing import Any, AsyncIterator, Dict, Iterator, List, Optional, Union, cast from langchain_core.callbacks.manager import ( AsyncCallbackManagerForLLMRun, CallbackManagerForLLMRun, ) from langchain_core.language_models.chat_models import BaseChatModel, SimpleChatModel -from langchain_core.messages import AIMessageChunk, BaseMessage +from langchain_core.messages import ( + AIMessage, + AIMessageChunk, + BaseMessage, +) from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, ChatResult +from langchain_core.runnables import run_in_executor class FakeMessagesListChatModel(BaseChatModel): @@ -114,3 +120,184 @@ async def _astream( @property def _identifying_params(self) -> Dict[str, Any]: return {"responses": self.responses} + + +class FakeChatModel(SimpleChatModel): + """Fake Chat Model wrapper for testing purposes.""" + + def _call( + self, + messages: List[BaseMessage], + stop: Optional[List[str]] = None, + run_manager: Optional[CallbackManagerForLLMRun] = None, + **kwargs: Any, + ) -> str: + return "fake response" + + async def _agenerate( + self, + messages: List[BaseMessage], + stop: Optional[List[str]] = None, + run_manager: Optional[AsyncCallbackManagerForLLMRun] = None, + **kwargs: Any, + ) -> ChatResult: + output_str = "fake response" + message = AIMessage(content=output_str) + generation = ChatGeneration(message=message) + return ChatResult(generations=[generation]) + + @property + def _llm_type(self) -> str: + return "fake-chat-model" + + @property + def _identifying_params(self) -> Dict[str, Any]: + return {"key": "fake"} + + +class GenericFakeChatModel(BaseChatModel): + """A generic fake chat model that can be used to test the chat model interface. + + * Chat model should be usable in both sync and async tests + * Invokes on_llm_new_token to allow for testing of callback related code for new + tokens. + * Includes logic to break messages into message chunk to facilitate testing of + streaming. + """ + + messages: Iterator[AIMessage] + """Get an iterator over messages. + + This can be expanded to accept other types like Callables / dicts / strings + to make the interface more generic if needed. + + Note: if you want to pass a list, you can use `iter` to convert it to an iterator. + + Please note that streaming is not implemented yet. We should try to implement it + in the future by delegating to invoke and then breaking the resulting output + into message chunks. + """ + + def _generate( + self, + messages: List[BaseMessage], + stop: Optional[List[str]] = None, + run_manager: Optional[CallbackManagerForLLMRun] = None, + **kwargs: Any, + ) -> ChatResult: + """Top Level call""" + message = next(self.messages) + generation = ChatGeneration(message=message) + return ChatResult(generations=[generation]) + + def _stream( + self, + messages: List[BaseMessage], + stop: Optional[List[str]] = None, + run_manager: Optional[CallbackManagerForLLMRun] = None, + **kwargs: Any, + ) -> Iterator[ChatGenerationChunk]: + """Stream the output of the model.""" + chat_result = self._generate( + messages, stop=stop, run_manager=run_manager, **kwargs + ) + if not isinstance(chat_result, ChatResult): + raise ValueError( + f"Expected generate to return a ChatResult, " + f"but got {type(chat_result)} instead." + ) + + message = chat_result.generations[0].message + + if not isinstance(message, AIMessage): + raise ValueError( + f"Expected invoke to return an AIMessage, " + f"but got {type(message)} instead." + ) + + content = message.content + + if content: + # Use a regular expression to split on whitespace with a capture group + # so that we can preserve the whitespace in the output. + assert isinstance(content, str) + content_chunks = cast(List[str], re.split(r"(\s)", content)) + + for token in content_chunks: + chunk = ChatGenerationChunk(message=AIMessageChunk(content=token)) + yield chunk + if run_manager: + run_manager.on_llm_new_token(token, chunk=chunk) + + if message.additional_kwargs: + for key, value in message.additional_kwargs.items(): + # We should further break down the additional kwargs into chunks + # Special case for function call + if key == "function_call": + for fkey, fvalue in value.items(): + if isinstance(fvalue, str): + # Break function call by `,` + fvalue_chunks = cast(List[str], re.split(r"(,)", fvalue)) + for fvalue_chunk in fvalue_chunks: + chunk = ChatGenerationChunk( + message=AIMessageChunk( + content="", + additional_kwargs={ + "function_call": {fkey: fvalue_chunk} + }, + ) + ) + yield chunk + if run_manager: + run_manager.on_llm_new_token( + "", + chunk=chunk, # No token for function call + ) + else: + chunk = ChatGenerationChunk( + message=AIMessageChunk( + content="", + additional_kwargs={"function_call": {fkey: fvalue}}, + ) + ) + yield chunk + if run_manager: + run_manager.on_llm_new_token( + "", + chunk=chunk, # No token for function call + ) + else: + chunk = ChatGenerationChunk( + message=AIMessageChunk( + content="", additional_kwargs={key: value} + ) + ) + yield chunk + if run_manager: + run_manager.on_llm_new_token( + "", + chunk=chunk, # No token for function call + ) + + async def _astream( + self, + messages: List[BaseMessage], + stop: Optional[List[str]] = None, + run_manager: Optional[AsyncCallbackManagerForLLMRun] = None, + **kwargs: Any, + ) -> AsyncIterator[ChatGenerationChunk]: + """Stream the output of the model.""" + result = await run_in_executor( + None, + self._stream, + messages, + stop=stop, + run_manager=run_manager.get_sync() if run_manager else None, + **kwargs, + ) + for chunk in result: + yield chunk + + @property + def _llm_type(self) -> str: + return "generic-fake-chat-model" diff --git a/libs/core/tests/unit_tests/fake/test_fake_chat_model.py b/libs/core/tests/unit_tests/fake/test_fake_chat_model.py new file mode 100644 index 0000000000000..8700f0751caa3 --- /dev/null +++ b/libs/core/tests/unit_tests/fake/test_fake_chat_model.py @@ -0,0 +1,184 @@ +"""Tests for verifying that testing utility code works as expected.""" +from itertools import cycle +from typing import Any, Dict, List, Optional, Union +from uuid import UUID + +from langchain_core.callbacks.base import AsyncCallbackHandler +from langchain_core.messages import AIMessage, AIMessageChunk, BaseMessage +from langchain_core.outputs import ChatGenerationChunk, GenerationChunk +from tests.unit_tests.fake.chat_model import GenericFakeChatModel + + +def test_generic_fake_chat_model_invoke() -> None: + # Will alternate between responding with hello and goodbye + infinite_cycle = cycle([AIMessage(content="hello"), AIMessage(content="goodbye")]) + model = GenericFakeChatModel(messages=infinite_cycle) + response = model.invoke("meow") + assert response == AIMessage(content="hello") + response = model.invoke("kitty") + assert response == AIMessage(content="goodbye") + response = model.invoke("meow") + assert response == AIMessage(content="hello") + + +async def test_generic_fake_chat_model_ainvoke() -> None: + # Will alternate between responding with hello and goodbye + infinite_cycle = cycle([AIMessage(content="hello"), AIMessage(content="goodbye")]) + model = GenericFakeChatModel(messages=infinite_cycle) + response = await model.ainvoke("meow") + assert response == AIMessage(content="hello") + response = await model.ainvoke("kitty") + assert response == AIMessage(content="goodbye") + response = await model.ainvoke("meow") + assert response == AIMessage(content="hello") + + +async def test_generic_fake_chat_model_stream() -> None: + """Test streaming.""" + infinite_cycle = cycle( + [ + AIMessage(content="hello goodbye"), + ] + ) + model = GenericFakeChatModel(messages=infinite_cycle) + chunks = [chunk async for chunk in model.astream("meow")] + assert chunks == [ + AIMessageChunk(content="hello"), + AIMessageChunk(content=" "), + AIMessageChunk(content="goodbye"), + ] + + chunks = [chunk for chunk in model.stream("meow")] + assert chunks == [ + AIMessageChunk(content="hello"), + AIMessageChunk(content=" "), + AIMessageChunk(content="goodbye"), + ] + + # Test streaming of additional kwargs. + # Relying on insertion order of the additional kwargs dict + message = AIMessage(content="", additional_kwargs={"foo": 42, "bar": 24}) + model = GenericFakeChatModel(messages=cycle([message])) + chunks = [chunk async for chunk in model.astream("meow")] + assert chunks == [ + AIMessageChunk(content="", additional_kwargs={"foo": 42}), + AIMessageChunk(content="", additional_kwargs={"bar": 24}), + ] + + message = AIMessage( + content="", + additional_kwargs={ + "function_call": { + "name": "move_file", + "arguments": '{\n "source_path": "foo",\n "' + 'destination_path": "bar"\n}', + } + }, + ) + model = GenericFakeChatModel(messages=cycle([message])) + chunks = [chunk async for chunk in model.astream("meow")] + + assert chunks == [ + AIMessageChunk( + content="", additional_kwargs={"function_call": {"name": "move_file"}} + ), + AIMessageChunk( + content="", + additional_kwargs={ + "function_call": {"arguments": '{\n "source_path": "foo"'} + }, + ), + AIMessageChunk( + content="", additional_kwargs={"function_call": {"arguments": ","}} + ), + AIMessageChunk( + content="", + additional_kwargs={ + "function_call": {"arguments": '\n "destination_path": "bar"\n}'} + }, + ), + ] + + accumulate_chunks = None + for chunk in chunks: + if accumulate_chunks is None: + accumulate_chunks = chunk + else: + accumulate_chunks += chunk + + assert accumulate_chunks == AIMessageChunk( + content="", + additional_kwargs={ + "function_call": { + "name": "move_file", + "arguments": '{\n "source_path": "foo",\n "' + 'destination_path": "bar"\n}', + } + }, + ) + + +async def test_generic_fake_chat_model_astream_log() -> None: + """Test streaming.""" + infinite_cycle = cycle([AIMessage(content="hello goodbye")]) + model = GenericFakeChatModel(messages=infinite_cycle) + log_patches = [ + log_patch async for log_patch in model.astream_log("meow", diff=False) + ] + final = log_patches[-1] + assert final.state["streamed_output"] == [ + AIMessageChunk(content="hello"), + AIMessageChunk(content=" "), + AIMessageChunk(content="goodbye"), + ] + + +async def test_callback_handlers() -> None: + """Verify that model is implemented correctly with handlers working.""" + + class MyCustomAsyncHandler(AsyncCallbackHandler): + def __init__(self, store: List[str]) -> None: + self.store = store + + async def on_chat_model_start( + self, + serialized: Dict[str, Any], + messages: List[List[BaseMessage]], + *, + run_id: UUID, + parent_run_id: Optional[UUID] = None, + tags: Optional[List[str]] = None, + metadata: Optional[Dict[str, Any]] = None, + **kwargs: Any, + ) -> Any: + # Do nothing + # Required to implement since this is an abstract method + pass + + async def on_llm_new_token( + self, + token: str, + *, + chunk: Optional[Union[GenerationChunk, ChatGenerationChunk]] = None, + run_id: UUID, + parent_run_id: Optional[UUID] = None, + tags: Optional[List[str]] = None, + **kwargs: Any, + ) -> None: + self.store.append(token) + + infinite_cycle = cycle( + [ + AIMessage(content="hello goodbye"), + ] + ) + model = GenericFakeChatModel(messages=infinite_cycle) + tokens: List[str] = [] + # New model + results = list(model.stream("meow", {"callbacks": [MyCustomAsyncHandler(tokens)]})) + assert results == [ + AIMessageChunk(content="hello"), + AIMessageChunk(content=" "), + AIMessageChunk(content="goodbye"), + ] + assert tokens == ["hello", " ", "goodbye"] diff --git a/libs/core/tests/unit_tests/prompts/test_loading.py b/libs/core/tests/unit_tests/prompts/test_loading.py index afad88766a836..59b5d95cb62ea 100644 --- a/libs/core/tests/unit_tests/prompts/test_loading.py +++ b/libs/core/tests/unit_tests/prompts/test_loading.py @@ -10,7 +10,7 @@ from langchain_core.prompts.loading import load_prompt from langchain_core.prompts.prompt import PromptTemplate -EXAMPLE_DIR = Path("tests/unit_tests/examples").absolute() +EXAMPLE_DIR = (Path(__file__).parent.parent / "examples").absolute() @contextmanager diff --git a/libs/core/tests/unit_tests/runnables/__snapshots__/test_fallbacks.ambr b/libs/core/tests/unit_tests/runnables/__snapshots__/test_fallbacks.ambr new file mode 100644 index 0000000000000..751274bf7682c --- /dev/null +++ b/libs/core/tests/unit_tests/runnables/__snapshots__/test_fallbacks.ambr @@ -0,0 +1,373 @@ +# serializer version: 1 +# name: test_fallbacks[chain] + ''' + { + "lc": 1, + "type": "constructor", + "id": [ + "langchain", + "schema", + "runnable", + "RunnableSequence" + ], + "kwargs": { + "first": { + "lc": 1, + "type": "constructor", + "id": [ + "langchain", + "schema", + "runnable", + "RunnableParallel" + ], + "kwargs": { + "steps": { + "buz": { + "lc": 1, + "type": "not_implemented", + "id": [ + "langchain_core", + "runnables", + "base", + "RunnableLambda" + ], + "repr": "RunnableLambda(lambda x: x)" + } + } + } + }, + "middle": [], + "last": { + "lc": 1, + "type": "constructor", + "id": [ + "langchain", + "schema", + "runnable", + "RunnableWithFallbacks" + ], + "kwargs": { + "runnable": { + "lc": 1, + "type": "constructor", + "id": [ + "langchain", + "schema", + "runnable", + "RunnableSequence" + ], + "kwargs": { + "first": { + "lc": 1, + "type": "constructor", + "id": [ + "langchain", + "prompts", + "prompt", + "PromptTemplate" + ], + "kwargs": { + "input_variables": [ + "buz" + ], + "template": "what did baz say to {buz}", + "template_format": "f-string", + "partial_variables": {} + } + }, + "middle": [], + "last": { + "lc": 1, + "type": "not_implemented", + "id": [ + "tests", + "unit_tests", + "fake", + "llm", + "FakeListLLM" + ], + "repr": "FakeListLLM(responses=['foo'], i=1)" + }, + "name": null + } + }, + "fallbacks": [ + { + "lc": 1, + "type": "constructor", + "id": [ + "langchain", + "schema", + "runnable", + "RunnableSequence" + ], + "kwargs": { + "first": { + "lc": 1, + "type": "constructor", + "id": [ + "langchain", + "prompts", + "prompt", + "PromptTemplate" + ], + "kwargs": { + "input_variables": [ + "buz" + ], + "template": "what did baz say to {buz}", + "template_format": "f-string", + "partial_variables": {} + } + }, + "middle": [], + "last": { + "lc": 1, + "type": "not_implemented", + "id": [ + "tests", + "unit_tests", + "fake", + "llm", + "FakeListLLM" + ], + "repr": "FakeListLLM(responses=['bar'])" + }, + "name": null + } + } + ], + "exceptions_to_handle": [ + { + "lc": 1, + "type": "not_implemented", + "id": [ + "builtins", + "Exception" + ], + "repr": "" + } + ], + "exception_key": null + } + }, + "name": null + } + } + ''' +# --- +# name: test_fallbacks[chain_pass_exceptions] + ''' + { + "lc": 1, + "type": "constructor", + "id": [ + "langchain", + "schema", + "runnable", + "RunnableSequence" + ], + "kwargs": { + "first": { + "lc": 1, + "type": "constructor", + "id": [ + "langchain", + "schema", + "runnable", + "RunnableParallel" + ], + "kwargs": { + "steps": { + "text": { + "lc": 1, + "type": "constructor", + "id": [ + "langchain", + "schema", + "runnable", + "RunnablePassthrough" + ], + "kwargs": { + "func": null, + "afunc": null, + "input_type": null + } + } + } + } + }, + "middle": [], + "last": { + "lc": 1, + "type": "constructor", + "id": [ + "langchain", + "schema", + "runnable", + "RunnableWithFallbacks" + ], + "kwargs": { + "runnable": { + "lc": 1, + "type": "not_implemented", + "id": [ + "langchain_core", + "runnables", + "base", + "RunnableLambda" + ], + "repr": "RunnableLambda(_raise_error)" + }, + "fallbacks": [ + { + "lc": 1, + "type": "not_implemented", + "id": [ + "langchain_core", + "runnables", + "base", + "RunnableLambda" + ], + "repr": "RunnableLambda(_dont_raise_error)" + } + ], + "exceptions_to_handle": [ + { + "lc": 1, + "type": "not_implemented", + "id": [ + "builtins", + "Exception" + ], + "repr": "" + } + ], + "exception_key": "exception" + } + }, + "name": null + } + } + ''' +# --- +# name: test_fallbacks[llm] + ''' + { + "lc": 1, + "type": "constructor", + "id": [ + "langchain", + "schema", + "runnable", + "RunnableWithFallbacks" + ], + "kwargs": { + "runnable": { + "lc": 1, + "type": "not_implemented", + "id": [ + "tests", + "unit_tests", + "fake", + "llm", + "FakeListLLM" + ], + "repr": "FakeListLLM(responses=['foo'], i=1)" + }, + "fallbacks": [ + { + "lc": 1, + "type": "not_implemented", + "id": [ + "tests", + "unit_tests", + "fake", + "llm", + "FakeListLLM" + ], + "repr": "FakeListLLM(responses=['bar'])" + } + ], + "exceptions_to_handle": [ + { + "lc": 1, + "type": "not_implemented", + "id": [ + "builtins", + "Exception" + ], + "repr": "" + } + ], + "exception_key": null + } + } + ''' +# --- +# name: test_fallbacks[llm_multi] + ''' + { + "lc": 1, + "type": "constructor", + "id": [ + "langchain", + "schema", + "runnable", + "RunnableWithFallbacks" + ], + "kwargs": { + "runnable": { + "lc": 1, + "type": "not_implemented", + "id": [ + "tests", + "unit_tests", + "fake", + "llm", + "FakeListLLM" + ], + "repr": "FakeListLLM(responses=['foo'], i=1)" + }, + "fallbacks": [ + { + "lc": 1, + "type": "not_implemented", + "id": [ + "tests", + "unit_tests", + "fake", + "llm", + "FakeListLLM" + ], + "repr": "FakeListLLM(responses=['baz'], i=1)" + }, + { + "lc": 1, + "type": "not_implemented", + "id": [ + "tests", + "unit_tests", + "fake", + "llm", + "FakeListLLM" + ], + "repr": "FakeListLLM(responses=['bar'])" + } + ], + "exceptions_to_handle": [ + { + "lc": 1, + "type": "not_implemented", + "id": [ + "builtins", + "Exception" + ], + "repr": "" + } + ], + "exception_key": null + } + } + ''' +# --- diff --git a/libs/core/tests/unit_tests/runnables/__snapshots__/test_runnable.ambr b/libs/core/tests/unit_tests/runnables/__snapshots__/test_runnable.ambr index 8ff1fe98dbb91..051520c0457f6 100644 --- a/libs/core/tests/unit_tests/runnables/__snapshots__/test_runnable.ambr +++ b/libs/core/tests/unit_tests/runnables/__snapshots__/test_runnable.ambr @@ -696,280 +696,6 @@ } ''' # --- -# name: test_llm_with_fallbacks[llm_chain_with_fallbacks] - ''' - { - "lc": 1, - "type": "constructor", - "id": [ - "langchain", - "schema", - "runnable", - "RunnableSequence" - ], - "kwargs": { - "first": { - "lc": 1, - "type": "constructor", - "id": [ - "langchain", - "schema", - "runnable", - "RunnableParallel" - ], - "kwargs": { - "steps": { - "buz": { - "lc": 1, - "type": "not_implemented", - "id": [ - "langchain_core", - "runnables", - "base", - "RunnableLambda" - ], - "repr": "RunnableLambda(lambda x: x)" - } - } - } - }, - "middle": [], - "last": { - "lc": 1, - "type": "constructor", - "id": [ - "langchain", - "schema", - "runnable", - "RunnableWithFallbacks" - ], - "kwargs": { - "runnable": { - "lc": 1, - "type": "constructor", - "id": [ - "langchain", - "schema", - "runnable", - "RunnableSequence" - ], - "kwargs": { - "first": { - "lc": 1, - "type": "constructor", - "id": [ - "langchain", - "prompts", - "prompt", - "PromptTemplate" - ], - "kwargs": { - "input_variables": [ - "buz" - ], - "template": "what did baz say to {buz}", - "template_format": "f-string", - "partial_variables": {} - } - }, - "middle": [], - "last": { - "lc": 1, - "type": "not_implemented", - "id": [ - "tests", - "unit_tests", - "fake", - "llm", - "FakeListLLM" - ], - "repr": "FakeListLLM(responses=['foo'], i=1)" - }, - "name": null - } - }, - "fallbacks": [ - { - "lc": 1, - "type": "constructor", - "id": [ - "langchain", - "schema", - "runnable", - "RunnableSequence" - ], - "kwargs": { - "first": { - "lc": 1, - "type": "constructor", - "id": [ - "langchain", - "prompts", - "prompt", - "PromptTemplate" - ], - "kwargs": { - "input_variables": [ - "buz" - ], - "template": "what did baz say to {buz}", - "template_format": "f-string", - "partial_variables": {} - } - }, - "middle": [], - "last": { - "lc": 1, - "type": "not_implemented", - "id": [ - "tests", - "unit_tests", - "fake", - "llm", - "FakeListLLM" - ], - "repr": "FakeListLLM(responses=['bar'])" - }, - "name": null - } - } - ], - "exceptions_to_handle": [ - { - "lc": 1, - "type": "not_implemented", - "id": [ - "builtins", - "Exception" - ], - "repr": "" - } - ] - } - }, - "name": null - } - } - ''' -# --- -# name: test_llm_with_fallbacks[llm_with_fallbacks] - ''' - { - "lc": 1, - "type": "constructor", - "id": [ - "langchain", - "schema", - "runnable", - "RunnableWithFallbacks" - ], - "kwargs": { - "runnable": { - "lc": 1, - "type": "not_implemented", - "id": [ - "tests", - "unit_tests", - "fake", - "llm", - "FakeListLLM" - ], - "repr": "FakeListLLM(responses=['foo'], i=1)" - }, - "fallbacks": [ - { - "lc": 1, - "type": "not_implemented", - "id": [ - "tests", - "unit_tests", - "fake", - "llm", - "FakeListLLM" - ], - "repr": "FakeListLLM(responses=['bar'])" - } - ], - "exceptions_to_handle": [ - { - "lc": 1, - "type": "not_implemented", - "id": [ - "builtins", - "Exception" - ], - "repr": "" - } - ] - } - } - ''' -# --- -# name: test_llm_with_fallbacks[llm_with_multi_fallbacks] - ''' - { - "lc": 1, - "type": "constructor", - "id": [ - "langchain", - "schema", - "runnable", - "RunnableWithFallbacks" - ], - "kwargs": { - "runnable": { - "lc": 1, - "type": "not_implemented", - "id": [ - "tests", - "unit_tests", - "fake", - "llm", - "FakeListLLM" - ], - "repr": "FakeListLLM(responses=['foo'], i=1)" - }, - "fallbacks": [ - { - "lc": 1, - "type": "not_implemented", - "id": [ - "tests", - "unit_tests", - "fake", - "llm", - "FakeListLLM" - ], - "repr": "FakeListLLM(responses=['baz'], i=1)" - }, - { - "lc": 1, - "type": "not_implemented", - "id": [ - "tests", - "unit_tests", - "fake", - "llm", - "FakeListLLM" - ], - "repr": "FakeListLLM(responses=['bar'])" - } - ], - "exceptions_to_handle": [ - { - "lc": 1, - "type": "not_implemented", - "id": [ - "builtins", - "Exception" - ], - "repr": "" - } - ] - } - } - ''' -# --- # name: test_prompt_with_chat_model ''' ChatPromptTemplate(input_variables=['question'], messages=[SystemMessagePromptTemplate(prompt=PromptTemplate(input_variables=[], template='You are a nice assistant.')), HumanMessagePromptTemplate(prompt=PromptTemplate(input_variables=['question'], template='{question}'))]) diff --git a/libs/core/tests/unit_tests/runnables/test_fallbacks.py b/libs/core/tests/unit_tests/runnables/test_fallbacks.py new file mode 100644 index 0000000000000..de1447a7267a1 --- /dev/null +++ b/libs/core/tests/unit_tests/runnables/test_fallbacks.py @@ -0,0 +1,290 @@ +import sys +from typing import Any, AsyncIterator, Iterator + +import pytest +from syrupy import SnapshotAssertion + +from langchain_core.load import dumps +from langchain_core.prompts import PromptTemplate +from langchain_core.runnables import ( + Runnable, + RunnableGenerator, + RunnableLambda, + RunnableParallel, + RunnablePassthrough, + RunnableWithFallbacks, +) +from tests.unit_tests.fake.llm import FakeListLLM + + +@pytest.fixture() +def llm() -> RunnableWithFallbacks: + error_llm = FakeListLLM(responses=["foo"], i=1) + pass_llm = FakeListLLM(responses=["bar"]) + + return error_llm.with_fallbacks([pass_llm]) + + +@pytest.fixture() +def llm_multi() -> RunnableWithFallbacks: + error_llm = FakeListLLM(responses=["foo"], i=1) + error_llm_2 = FakeListLLM(responses=["baz"], i=1) + pass_llm = FakeListLLM(responses=["bar"]) + + return error_llm.with_fallbacks([error_llm_2, pass_llm]) + + +@pytest.fixture() +def chain() -> Runnable: + error_llm = FakeListLLM(responses=["foo"], i=1) + pass_llm = FakeListLLM(responses=["bar"]) + + prompt = PromptTemplate.from_template("what did baz say to {buz}") + return RunnableParallel({"buz": lambda x: x}) | (prompt | error_llm).with_fallbacks( + [prompt | pass_llm] + ) + + +def _raise_error(inputs: dict) -> str: + raise ValueError() + + +def _dont_raise_error(inputs: dict) -> str: + if "exception" in inputs: + return "bar" + raise ValueError() + + +@pytest.fixture() +def chain_pass_exceptions() -> Runnable: + fallback = RunnableLambda(_dont_raise_error) + return {"text": RunnablePassthrough()} | RunnableLambda( + _raise_error + ).with_fallbacks([fallback], exception_key="exception") + + +@pytest.mark.parametrize( + "runnable", + ["llm", "llm_multi", "chain", "chain_pass_exceptions"], +) +async def test_fallbacks( + runnable: RunnableWithFallbacks, request: Any, snapshot: SnapshotAssertion +) -> None: + runnable = request.getfixturevalue(runnable) + assert runnable.invoke("hello") == "bar" + assert runnable.batch(["hi", "hey", "bye"]) == ["bar"] * 3 + assert list(runnable.stream("hello")) == ["bar"] + assert await runnable.ainvoke("hello") == "bar" + assert await runnable.abatch(["hi", "hey", "bye"]) == ["bar"] * 3 + assert list(await runnable.ainvoke("hello")) == list("bar") + if sys.version_info >= (3, 9): + assert dumps(runnable, pretty=True) == snapshot + + +def _runnable(inputs: dict) -> str: + if inputs["text"] == "foo": + return "first" + if "exception" not in inputs: + raise ValueError() + if inputs["text"] == "bar": + return "second" + if isinstance(inputs["exception"], ValueError): + raise RuntimeError() + return "third" + + +def _assert_potential_error(actual: list, expected: list) -> None: + for x, y in zip(actual, expected): + if isinstance(x, Exception): + assert isinstance(y, type(x)) + else: + assert x == y + + +def test_invoke_with_exception_key() -> None: + runnable = RunnableLambda(_runnable) + runnable_with_single = runnable.with_fallbacks( + [runnable], exception_key="exception" + ) + with pytest.raises(ValueError): + runnable_with_single.invoke({"text": "baz"}) + + actual = runnable_with_single.invoke({"text": "bar"}) + expected = "second" + _assert_potential_error([actual], [expected]) + + runnable_with_double = runnable.with_fallbacks( + [runnable, runnable], exception_key="exception" + ) + actual = runnable_with_double.invoke({"text": "baz"}) + + expected = "third" + _assert_potential_error([actual], [expected]) + + +async def test_ainvoke_with_exception_key() -> None: + runnable = RunnableLambda(_runnable) + runnable_with_single = runnable.with_fallbacks( + [runnable], exception_key="exception" + ) + with pytest.raises(ValueError): + await runnable_with_single.ainvoke({"text": "baz"}) + + actual = await runnable_with_single.ainvoke({"text": "bar"}) + expected = "second" + _assert_potential_error([actual], [expected]) + + runnable_with_double = runnable.with_fallbacks( + [runnable, runnable], exception_key="exception" + ) + actual = await runnable_with_double.ainvoke({"text": "baz"}) + expected = "third" + _assert_potential_error([actual], [expected]) + + +def test_batch() -> None: + runnable = RunnableLambda(_runnable) + with pytest.raises(ValueError): + runnable.batch([{"text": "foo"}, {"text": "bar"}, {"text": "baz"}]) + actual = runnable.batch( + [{"text": "foo"}, {"text": "bar"}, {"text": "baz"}], return_exceptions=True + ) + expected = ["first", ValueError(), ValueError()] + _assert_potential_error(actual, expected) + + runnable_with_single = runnable.with_fallbacks( + [runnable], exception_key="exception" + ) + with pytest.raises(RuntimeError): + runnable_with_single.batch([{"text": "foo"}, {"text": "bar"}, {"text": "baz"}]) + actual = runnable_with_single.batch( + [{"text": "foo"}, {"text": "bar"}, {"text": "baz"}], return_exceptions=True + ) + expected = ["first", "second", RuntimeError()] + _assert_potential_error(actual, expected) + + runnable_with_double = runnable.with_fallbacks( + [runnable, runnable], exception_key="exception" + ) + actual = runnable_with_double.batch( + [{"text": "foo"}, {"text": "bar"}, {"text": "baz"}], return_exceptions=True + ) + + expected = ["first", "second", "third"] + _assert_potential_error(actual, expected) + + runnable_with_double = runnable.with_fallbacks( + [runnable, runnable], + exception_key="exception", + exceptions_to_handle=(ValueError,), + ) + actual = runnable_with_double.batch( + [{"text": "foo"}, {"text": "bar"}, {"text": "baz"}], return_exceptions=True + ) + + expected = ["first", "second", RuntimeError()] + _assert_potential_error(actual, expected) + + +async def test_abatch() -> None: + runnable = RunnableLambda(_runnable) + with pytest.raises(ValueError): + await runnable.abatch([{"text": "foo"}, {"text": "bar"}, {"text": "baz"}]) + actual = await runnable.abatch( + [{"text": "foo"}, {"text": "bar"}, {"text": "baz"}], return_exceptions=True + ) + expected = ["first", ValueError(), ValueError()] + _assert_potential_error(actual, expected) + + runnable_with_single = runnable.with_fallbacks( + [runnable], exception_key="exception" + ) + with pytest.raises(RuntimeError): + await runnable_with_single.abatch( + [{"text": "foo"}, {"text": "bar"}, {"text": "baz"}] + ) + actual = await runnable_with_single.abatch( + [{"text": "foo"}, {"text": "bar"}, {"text": "baz"}], return_exceptions=True + ) + expected = ["first", "second", RuntimeError()] + _assert_potential_error(actual, expected) + + runnable_with_double = runnable.with_fallbacks( + [runnable, runnable], exception_key="exception" + ) + actual = await runnable_with_double.abatch( + [{"text": "foo"}, {"text": "bar"}, {"text": "baz"}], return_exceptions=True + ) + + expected = ["first", "second", "third"] + _assert_potential_error(actual, expected) + + runnable_with_double = runnable.with_fallbacks( + [runnable, runnable], + exception_key="exception", + exceptions_to_handle=(ValueError,), + ) + actual = await runnable_with_double.abatch( + [{"text": "foo"}, {"text": "bar"}, {"text": "baz"}], return_exceptions=True + ) + + expected = ["first", "second", RuntimeError()] + _assert_potential_error(actual, expected) + + +def _generate(input: Iterator) -> Iterator[str]: + yield from "foo bar" + + +def _generate_immediate_error(input: Iterator) -> Iterator[str]: + raise ValueError() + yield "" + + +def _generate_delayed_error(input: Iterator) -> Iterator[str]: + yield "" + raise ValueError() + + +def test_fallbacks_stream() -> None: + runnable = RunnableGenerator(_generate_immediate_error).with_fallbacks( + [RunnableGenerator(_generate)] + ) + assert list(runnable.stream({})) == [c for c in "foo bar"] + + with pytest.raises(ValueError): + runnable = RunnableGenerator(_generate_delayed_error).with_fallbacks( + [RunnableGenerator(_generate)] + ) + list(runnable.stream({})) + + +async def _agenerate(input: AsyncIterator) -> AsyncIterator[str]: + for c in "foo bar": + yield c + + +async def _agenerate_immediate_error(input: AsyncIterator) -> AsyncIterator[str]: + raise ValueError() + yield "" + + +async def _agenerate_delayed_error(input: AsyncIterator) -> AsyncIterator[str]: + yield "" + raise ValueError() + + +async def test_fallbacks_astream() -> None: + runnable = RunnableGenerator(_agenerate_immediate_error).with_fallbacks( + [RunnableGenerator(_agenerate)] + ) + expected = (c for c in "foo bar") + async for c in runnable.astream({}): + assert c == next(expected) + + with pytest.raises(ValueError): + runnable = RunnableGenerator(_agenerate_delayed_error).with_fallbacks( + [RunnableGenerator(_agenerate)] + ) + async for c in runnable.astream({}): + pass diff --git a/libs/core/tests/unit_tests/runnables/test_runnable.py b/libs/core/tests/unit_tests/runnables/test_runnable.py index 2a94cf2469bc3..a6af8ada3a22c 100644 --- a/libs/core/tests/unit_tests/runnables/test_runnable.py +++ b/libs/core/tests/unit_tests/runnables/test_runnable.py @@ -66,7 +66,6 @@ RunnablePassthrough, RunnablePick, RunnableSequence, - RunnableWithFallbacks, add, chain, ) @@ -1648,6 +1647,8 @@ async def test_prompt() -> None: ] ) ], + "type": "prompt", + "name": "ChatPromptTemplate", }, ) @@ -2096,6 +2097,8 @@ async def test_prompt_with_llm( "logs": {}, "final_output": None, "streamed_output": [], + "name": "RunnableSequence", + "type": "chain", }, } ), @@ -2298,6 +2301,8 @@ async def test_prompt_with_llm_parser( "logs": {}, "final_output": None, "streamed_output": [], + "name": "RunnableSequence", + "type": "chain", }, } ), @@ -2509,7 +2514,13 @@ async def list_producer(input: AsyncIterator[Any]) -> AsyncIterator[AddableDict] { "op": "replace", "path": "", - "value": {"final_output": None, "logs": {}, "streamed_output": []}, + "value": { + "final_output": None, + "logs": {}, + "streamed_output": [], + "name": "list_producer", + "type": "chain", + }, } ), RunLogPatch( @@ -2537,12 +2548,14 @@ async def list_producer(input: AsyncIterator[Any]) -> AsyncIterator[AddableDict] assert state.state == { "final_output": {"alist": ["0", "1", "2", "3"]}, "logs": {}, + "name": "list_producer", "streamed_output": [ {"alist": ["0"]}, {"alist": ["1"]}, {"alist": ["2"]}, {"alist": ["3"]}, ], + "type": "chain", } @@ -3683,52 +3696,6 @@ async def test_runnable_sequence_atransform() -> None: assert "".join(chunks) == "foo-lish" -@pytest.fixture() -def llm_with_fallbacks() -> RunnableWithFallbacks: - error_llm = FakeListLLM(responses=["foo"], i=1) - pass_llm = FakeListLLM(responses=["bar"]) - - return error_llm.with_fallbacks([pass_llm]) - - -@pytest.fixture() -def llm_with_multi_fallbacks() -> RunnableWithFallbacks: - error_llm = FakeListLLM(responses=["foo"], i=1) - error_llm_2 = FakeListLLM(responses=["baz"], i=1) - pass_llm = FakeListLLM(responses=["bar"]) - - return error_llm.with_fallbacks([error_llm_2, pass_llm]) - - -@pytest.fixture() -def llm_chain_with_fallbacks() -> Runnable: - error_llm = FakeListLLM(responses=["foo"], i=1) - pass_llm = FakeListLLM(responses=["bar"]) - - prompt = PromptTemplate.from_template("what did baz say to {buz}") - return RunnableParallel({"buz": lambda x: x}) | (prompt | error_llm).with_fallbacks( - [prompt | pass_llm] - ) - - -@pytest.mark.parametrize( - "runnable", - ["llm_with_fallbacks", "llm_with_multi_fallbacks", "llm_chain_with_fallbacks"], -) -async def test_llm_with_fallbacks( - runnable: RunnableWithFallbacks, request: Any, snapshot: SnapshotAssertion -) -> None: - runnable = request.getfixturevalue(runnable) - assert runnable.invoke("hello") == "bar" - assert runnable.batch(["hi", "hey", "bye"]) == ["bar"] * 3 - assert list(runnable.stream("hello")) == ["bar"] - assert await runnable.ainvoke("hello") == "bar" - assert await runnable.abatch(["hi", "hey", "bye"]) == ["bar"] * 3 - assert list(await runnable.ainvoke("hello")) == list("bar") - if sys.version_info >= (3, 9): - assert dumps(runnable, pretty=True) == snapshot - - class FakeSplitIntoListParser(BaseOutputParser[List[str]]): """Parse the output of an LLM call to a comma-separated list.""" @@ -5186,4 +5153,6 @@ def add_one(x: int) -> int: "final_output": 2, "logs": {}, "streamed_output": [2], + "name": "add_one", + "type": "chain", } diff --git a/libs/core/tests/unit_tests/runnables/test_runnable_events.py b/libs/core/tests/unit_tests/runnables/test_runnable_events.py new file mode 100644 index 0000000000000..6d5de1094107d --- /dev/null +++ b/libs/core/tests/unit_tests/runnables/test_runnable_events.py @@ -0,0 +1,1081 @@ +"""Module that contains tests for runnable.astream_events API.""" +from itertools import cycle +from typing import AsyncIterator, List, Sequence, cast + +import pytest + +from langchain_core.callbacks import CallbackManagerForRetrieverRun, Callbacks +from langchain_core.documents import Document +from langchain_core.messages import ( + AIMessage, + AIMessageChunk, + HumanMessage, + SystemMessage, +) +from langchain_core.prompt_values import ChatPromptValue +from langchain_core.prompts import ChatPromptTemplate +from langchain_core.retrievers import BaseRetriever +from langchain_core.runnables import ( + RunnableLambda, +) +from langchain_core.runnables.schema import StreamEvent +from langchain_core.tools import tool +from tests.unit_tests.fake.chat_model import GenericFakeChatModel +from tests.unit_tests.fake.llm import FakeStreamingListLLM + + +def _with_nulled_run_id(events: Sequence[StreamEvent]) -> List[StreamEvent]: + """Removes the run ids from events.""" + return cast(List[StreamEvent], [{**event, "run_id": ""} for event in events]) + + +async def _as_async_iterator(iterable: List) -> AsyncIterator: + """Converts an iterable into an async iterator.""" + for item in iterable: + yield item + + +async def _collect_events(events: AsyncIterator[StreamEvent]) -> List[StreamEvent]: + """Collect the events and remove the run ids.""" + materialized_events = [event async for event in events] + events_ = _with_nulled_run_id(materialized_events) + for event in events_: + event["tags"] = sorted(event["tags"]) + return events_ + + +async def test_event_stream_with_single_lambda() -> None: + """Test the event stream with a tool.""" + + def reverse(s: str) -> str: + """Reverse a string.""" + return s[::-1] + + chain = RunnableLambda(func=reverse) + + events = await _collect_events(chain.astream_events("hello", version="v1")) + assert events == [ + { + "data": {"input": "hello"}, + "event": "on_chain_start", + "metadata": {}, + "name": "reverse", + "run_id": "", + "tags": [], + }, + { + "data": {"chunk": "olleh"}, + "event": "on_chain_stream", + "metadata": {}, + "name": "reverse", + "run_id": "", + "tags": [], + }, + { + "data": {"output": "olleh"}, + "event": "on_chain_end", + "metadata": {}, + "name": "reverse", + "run_id": "", + "tags": [], + }, + ] + + +async def test_event_stream_with_triple_lambda() -> None: + def reverse(s: str) -> str: + """Reverse a string.""" + return s[::-1] + + r = RunnableLambda(func=reverse) + + chain = ( + r.with_config({"run_name": "1"}) + | r.with_config({"run_name": "2"}) + | r.with_config({"run_name": "3"}) + ) + events = await _collect_events(chain.astream_events("hello", version="v1")) + assert events == [ + { + "data": {"input": "hello"}, + "event": "on_chain_start", + "metadata": {}, + "name": "RunnableSequence", + "run_id": "", + "tags": [], + }, + { + "data": {}, + "event": "on_chain_start", + "metadata": {}, + "name": "1", + "run_id": "", + "tags": ["seq:step:1"], + }, + { + "data": {"chunk": "olleh"}, + "event": "on_chain_stream", + "metadata": {}, + "name": "1", + "run_id": "", + "tags": ["seq:step:1"], + }, + { + "data": {}, + "event": "on_chain_start", + "metadata": {}, + "name": "2", + "run_id": "", + "tags": ["seq:step:2"], + }, + { + "data": {"input": "hello", "output": "olleh"}, + "event": "on_chain_end", + "metadata": {}, + "name": "1", + "run_id": "", + "tags": ["seq:step:1"], + }, + { + "data": {"chunk": "hello"}, + "event": "on_chain_stream", + "metadata": {}, + "name": "2", + "run_id": "", + "tags": ["seq:step:2"], + }, + { + "data": {}, + "event": "on_chain_start", + "metadata": {}, + "name": "3", + "run_id": "", + "tags": ["seq:step:3"], + }, + { + "data": {"input": "olleh", "output": "hello"}, + "event": "on_chain_end", + "metadata": {}, + "name": "2", + "run_id": "", + "tags": ["seq:step:2"], + }, + { + "data": {"chunk": "olleh"}, + "event": "on_chain_stream", + "metadata": {}, + "name": "3", + "run_id": "", + "tags": ["seq:step:3"], + }, + { + "data": {"chunk": "olleh"}, + "event": "on_chain_stream", + "metadata": {}, + "name": "RunnableSequence", + "run_id": "", + "tags": [], + }, + { + "data": {"input": "hello", "output": "olleh"}, + "event": "on_chain_end", + "metadata": {}, + "name": "3", + "run_id": "", + "tags": ["seq:step:3"], + }, + { + "data": {"output": "olleh"}, + "event": "on_chain_end", + "metadata": {}, + "name": "RunnableSequence", + "run_id": "", + "tags": [], + }, + ] + + +async def test_event_stream_with_triple_lambda_test_filtering() -> None: + """Test filtering based on tags / names""" + + def reverse(s: str) -> str: + """Reverse a string.""" + return s[::-1] + + r = RunnableLambda(func=reverse) + + chain = ( + r.with_config({"run_name": "1"}) + | r.with_config({"run_name": "2", "tags": ["my_tag"]}) + | r.with_config({"run_name": "3", "tags": ["my_tag"]}) + ) + events = await _collect_events( + chain.astream_events("hello", include_names=["1"], version="v1") + ) + assert events == [ + { + "data": {}, + "event": "on_chain_start", + "metadata": {}, + "name": "1", + "run_id": "", + "tags": ["seq:step:1"], + }, + { + "data": {"chunk": "olleh"}, + "event": "on_chain_stream", + "metadata": {}, + "name": "1", + "run_id": "", + "tags": ["seq:step:1"], + }, + { + "data": {"input": "hello", "output": "olleh"}, + "event": "on_chain_end", + "metadata": {}, + "name": "1", + "run_id": "", + "tags": ["seq:step:1"], + }, + ] + + events = await _collect_events( + chain.astream_events( + "hello", include_tags=["my_tag"], exclude_names=["2"], version="v1" + ) + ) + assert events == [ + { + "data": {}, + "event": "on_chain_start", + "metadata": {}, + "name": "3", + "run_id": "", + "tags": ["my_tag", "seq:step:3"], + }, + { + "data": {"chunk": "olleh"}, + "event": "on_chain_stream", + "metadata": {}, + "name": "3", + "run_id": "", + "tags": ["my_tag", "seq:step:3"], + }, + { + "data": {"input": "hello", "output": "olleh"}, + "event": "on_chain_end", + "metadata": {}, + "name": "3", + "run_id": "", + "tags": ["my_tag", "seq:step:3"], + }, + ] + + +async def test_event_stream_with_lambdas_from_lambda() -> None: + as_lambdas = RunnableLambda(lambda x: {"answer": "goodbye"}).with_config( + {"run_name": "my_lambda"} + ) + events = await _collect_events( + as_lambdas.astream_events({"question": "hello"}, version="v1") + ) + assert events == [ + { + "data": {"input": {"question": "hello"}}, + "event": "on_chain_start", + "metadata": {}, + "name": "my_lambda", + "run_id": "", + "tags": [], + }, + { + "data": {"chunk": {"answer": "goodbye"}}, + "event": "on_chain_stream", + "metadata": {}, + "name": "my_lambda", + "run_id": "", + "tags": [], + }, + { + "data": {"output": {"answer": "goodbye"}}, + "event": "on_chain_end", + "metadata": {}, + "name": "my_lambda", + "run_id": "", + "tags": [], + }, + ] + + +async def test_event_stream_with_simple_chain() -> None: + """Test as event stream.""" + template = ChatPromptTemplate.from_messages( + [("system", "You are Cat Agent 007"), ("human", "{question}")] + ).with_config({"run_name": "my_template", "tags": ["my_template"]}) + + infinite_cycle = cycle( + [AIMessage(content="hello world!"), AIMessage(content="goodbye world!")] + ) + # When streaming GenericFakeChatModel breaks AIMessage into chunks based on spaces + model = ( + GenericFakeChatModel(messages=infinite_cycle) + .with_config( + { + "metadata": {"a": "b"}, + "tags": ["my_model"], + "run_name": "my_model", + } + ) + .bind(stop="") + ) + + chain = (template | model).with_config( + { + "metadata": {"foo": "bar"}, + "tags": ["my_chain"], + "run_name": "my_chain", + } + ) + + events = await _collect_events( + chain.astream_events({"question": "hello"}, version="v1") + ) + assert events == [ + { + "data": {"input": {"question": "hello"}}, + "event": "on_chain_start", + "metadata": {"foo": "bar"}, + "name": "my_chain", + "run_id": "", + "tags": ["my_chain"], + }, + { + "data": {"input": {"question": "hello"}}, + "event": "on_prompt_start", + "metadata": {"foo": "bar"}, + "name": "my_template", + "run_id": "", + "tags": ["my_chain", "my_template", "seq:step:1"], + }, + { + "data": { + "input": {"question": "hello"}, + "output": ChatPromptValue( + messages=[ + SystemMessage(content="You are Cat Agent 007"), + HumanMessage(content="hello"), + ] + ), + }, + "event": "on_prompt_end", + "metadata": {"foo": "bar"}, + "name": "my_template", + "run_id": "", + "tags": ["my_chain", "my_template", "seq:step:1"], + }, + { + "data": { + "input": { + "messages": [ + [ + SystemMessage(content="You are Cat Agent 007"), + HumanMessage(content="hello"), + ] + ] + } + }, + "event": "on_chat_model_start", + "metadata": {"a": "b", "foo": "bar"}, + "name": "my_model", + "run_id": "", + "tags": ["my_chain", "my_model", "seq:step:2"], + }, + { + "data": {"chunk": AIMessageChunk(content="hello")}, + "event": "on_chain_stream", + "metadata": {"foo": "bar"}, + "name": "my_chain", + "run_id": "", + "tags": ["my_chain"], + }, + { + "data": {"chunk": AIMessageChunk(content="hello")}, + "event": "on_chat_model_stream", + "metadata": {"a": "b", "foo": "bar"}, + "name": "my_model", + "run_id": "", + "tags": ["my_chain", "my_model", "seq:step:2"], + }, + { + "data": {"chunk": AIMessageChunk(content=" ")}, + "event": "on_chain_stream", + "metadata": {"foo": "bar"}, + "name": "my_chain", + "run_id": "", + "tags": ["my_chain"], + }, + { + "data": {"chunk": AIMessageChunk(content=" ")}, + "event": "on_chat_model_stream", + "metadata": {"a": "b", "foo": "bar"}, + "name": "my_model", + "run_id": "", + "tags": ["my_chain", "my_model", "seq:step:2"], + }, + { + "data": {"chunk": AIMessageChunk(content="world!")}, + "event": "on_chain_stream", + "metadata": {"foo": "bar"}, + "name": "my_chain", + "run_id": "", + "tags": ["my_chain"], + }, + { + "data": {"chunk": AIMessageChunk(content="world!")}, + "event": "on_chat_model_stream", + "metadata": {"a": "b", "foo": "bar"}, + "name": "my_model", + "run_id": "", + "tags": ["my_chain", "my_model", "seq:step:2"], + }, + { + "data": { + "input": { + "messages": [ + [ + SystemMessage(content="You are Cat Agent 007"), + HumanMessage(content="hello"), + ] + ] + }, + "output": { + "generations": [ + [ + { + "generation_info": None, + "message": AIMessageChunk(content="hello world!"), + "text": "hello world!", + "type": "ChatGenerationChunk", + } + ] + ], + "llm_output": None, + "run": None, + }, + }, + "event": "on_chat_model_end", + "metadata": {"a": "b", "foo": "bar"}, + "name": "my_model", + "run_id": "", + "tags": ["my_chain", "my_model", "seq:step:2"], + }, + { + "data": {"output": AIMessageChunk(content="hello world!")}, + "event": "on_chain_end", + "metadata": {"foo": "bar"}, + "name": "my_chain", + "run_id": "", + "tags": ["my_chain"], + }, + ] + + +async def test_event_streaming_with_tools() -> None: + """Test streaming events with different tool definitions.""" + + @tool + def parameterless() -> str: + """A tool that does nothing.""" + return "hello" + + @tool + def with_callbacks(callbacks: Callbacks) -> str: + """A tool that does nothing.""" + return "world" + + @tool + def with_parameters(x: int, y: str) -> dict: + """A tool that does nothing.""" + return {"x": x, "y": y} + + @tool + def with_parameters_and_callbacks(x: int, y: str, callbacks: Callbacks) -> dict: + """A tool that does nothing.""" + return {"x": x, "y": y} + + # type ignores below because the tools don't appear to be runnables to type checkers + # we can remove as soon as that's fixed + events = await _collect_events(parameterless.astream_events({}, version="v1")) # type: ignore + assert events == [ + { + "data": {"input": {}}, + "event": "on_tool_start", + "metadata": {}, + "name": "parameterless", + "run_id": "", + "tags": [], + }, + { + "data": {"chunk": "hello"}, + "event": "on_tool_stream", + "metadata": {}, + "name": "parameterless", + "run_id": "", + "tags": [], + }, + { + "data": {"output": "hello"}, + "event": "on_tool_end", + "metadata": {}, + "name": "parameterless", + "run_id": "", + "tags": [], + }, + ] + + events = await _collect_events(with_callbacks.astream_events({}, version="v1")) # type: ignore + assert events == [ + { + "data": {"input": {}}, + "event": "on_tool_start", + "metadata": {}, + "name": "with_callbacks", + "run_id": "", + "tags": [], + }, + { + "data": {"chunk": "world"}, + "event": "on_tool_stream", + "metadata": {}, + "name": "with_callbacks", + "run_id": "", + "tags": [], + }, + { + "data": {"output": "world"}, + "event": "on_tool_end", + "metadata": {}, + "name": "with_callbacks", + "run_id": "", + "tags": [], + }, + ] + events = await _collect_events( + with_parameters.astream_events({"x": 1, "y": "2"}, version="v1") # type: ignore + ) + assert events == [ + { + "data": {"input": {"x": 1, "y": "2"}}, + "event": "on_tool_start", + "metadata": {}, + "name": "with_parameters", + "run_id": "", + "tags": [], + }, + { + "data": {"chunk": {"x": 1, "y": "2"}}, + "event": "on_tool_stream", + "metadata": {}, + "name": "with_parameters", + "run_id": "", + "tags": [], + }, + { + "data": {"output": {"x": 1, "y": "2"}}, + "event": "on_tool_end", + "metadata": {}, + "name": "with_parameters", + "run_id": "", + "tags": [], + }, + ] + + events = await _collect_events( + with_parameters_and_callbacks.astream_events({"x": 1, "y": "2"}, version="v1") # type: ignore + ) + assert events == [ + { + "data": {"input": {"x": 1, "y": "2"}}, + "event": "on_tool_start", + "metadata": {}, + "name": "with_parameters_and_callbacks", + "run_id": "", + "tags": [], + }, + { + "data": {"chunk": {"x": 1, "y": "2"}}, + "event": "on_tool_stream", + "metadata": {}, + "name": "with_parameters_and_callbacks", + "run_id": "", + "tags": [], + }, + { + "data": {"output": {"x": 1, "y": "2"}}, + "event": "on_tool_end", + "metadata": {}, + "name": "with_parameters_and_callbacks", + "run_id": "", + "tags": [], + }, + ] + + +class HardCodedRetriever(BaseRetriever): + documents: List[Document] + + def _get_relevant_documents( + self, query: str, *, run_manager: CallbackManagerForRetrieverRun + ) -> List[Document]: + return self.documents + + +async def test_event_stream_with_retriever() -> None: + """Test the event stream with a retriever.""" + retriever = HardCodedRetriever( + documents=[ + Document( + page_content="hello world!", + metadata={"foo": "bar"}, + ), + Document( + page_content="goodbye world!", + metadata={"food": "spare"}, + ), + ] + ) + events = await _collect_events( + retriever.astream_events({"query": "hello"}, version="v1") + ) + assert events == [ + { + "data": { + "input": {"query": "hello"}, + }, + "event": "on_retriever_start", + "metadata": {}, + "name": "HardCodedRetriever", + "run_id": "", + "tags": [], + }, + { + "data": { + "chunk": [ + Document(page_content="hello world!", metadata={"foo": "bar"}), + Document(page_content="goodbye world!", metadata={"food": "spare"}), + ] + }, + "event": "on_retriever_stream", + "metadata": {}, + "name": "HardCodedRetriever", + "run_id": "", + "tags": [], + }, + { + "data": { + "output": [ + Document(page_content="hello world!", metadata={"foo": "bar"}), + Document(page_content="goodbye world!", metadata={"food": "spare"}), + ], + }, + "event": "on_retriever_end", + "metadata": {}, + "name": "HardCodedRetriever", + "run_id": "", + "tags": [], + }, + ] + + +async def test_event_stream_with_retriever_and_formatter() -> None: + """Test the event stream with a retriever.""" + retriever = HardCodedRetriever( + documents=[ + Document( + page_content="hello world!", + metadata={"foo": "bar"}, + ), + Document( + page_content="goodbye world!", + metadata={"food": "spare"}, + ), + ] + ) + + def format_docs(docs: List[Document]) -> str: + """Format the docs.""" + return ", ".join([doc.page_content for doc in docs]) + + chain = retriever | format_docs + events = await _collect_events(chain.astream_events("hello", version="v1")) + assert events == [ + { + "data": {"input": "hello"}, + "event": "on_chain_start", + "metadata": {}, + "name": "RunnableSequence", + "run_id": "", + "tags": [], + }, + { + "data": {"input": {"query": "hello"}}, + "event": "on_retriever_start", + "metadata": {}, + "name": "Retriever", + "run_id": "", + "tags": ["seq:step:1"], + }, + { + "data": { + "input": {"query": "hello"}, + "output": { + "documents": [ + Document(page_content="hello world!", metadata={"foo": "bar"}), + Document( + page_content="goodbye world!", metadata={"food": "spare"} + ), + ] + }, + }, + "event": "on_retriever_end", + "metadata": {}, + "name": "Retriever", + "run_id": "", + "tags": ["seq:step:1"], + }, + { + "data": {}, + "event": "on_chain_start", + "metadata": {}, + "name": "format_docs", + "run_id": "", + "tags": ["seq:step:2"], + }, + { + "data": {"chunk": "hello world!, goodbye world!"}, + "event": "on_chain_stream", + "metadata": {}, + "name": "format_docs", + "run_id": "", + "tags": ["seq:step:2"], + }, + { + "data": {"chunk": "hello world!, goodbye world!"}, + "event": "on_chain_stream", + "metadata": {}, + "name": "RunnableSequence", + "run_id": "", + "tags": [], + }, + { + "data": { + "input": [ + Document(page_content="hello world!", metadata={"foo": "bar"}), + Document(page_content="goodbye world!", metadata={"food": "spare"}), + ], + "output": "hello world!, goodbye world!", + }, + "event": "on_chain_end", + "metadata": {}, + "name": "format_docs", + "run_id": "", + "tags": ["seq:step:2"], + }, + { + "data": {"output": "hello world!, goodbye world!"}, + "event": "on_chain_end", + "metadata": {}, + "name": "RunnableSequence", + "run_id": "", + "tags": [], + }, + ] + + +async def test_event_stream_on_chain_with_tool() -> None: + """Test the event stream with a tool.""" + + @tool + def concat(a: str, b: str) -> str: + """A tool that does nothing.""" + return a + b + + def reverse(s: str) -> str: + """Reverse a string.""" + return s[::-1] + + # For whatever reason type annotations fail here because reverse + # does not appear to be a runnable + chain = concat | reverse # type: ignore + + events = await _collect_events( + chain.astream_events({"a": "hello", "b": "world"}, version="v1") + ) + assert events == [ + { + "data": {"input": {"a": "hello", "b": "world"}}, + "event": "on_chain_start", + "metadata": {}, + "name": "RunnableSequence", + "run_id": "", + "tags": [], + }, + { + "data": {"input": {"a": "hello", "b": "world"}}, + "event": "on_tool_start", + "metadata": {}, + "name": "concat", + "run_id": "", + "tags": ["seq:step:1"], + }, + { + "data": {"input": {"a": "hello", "b": "world"}, "output": "helloworld"}, + "event": "on_tool_end", + "metadata": {}, + "name": "concat", + "run_id": "", + "tags": ["seq:step:1"], + }, + { + "data": {}, + "event": "on_chain_start", + "metadata": {}, + "name": "reverse", + "run_id": "", + "tags": ["seq:step:2"], + }, + { + "data": {"chunk": "dlrowolleh"}, + "event": "on_chain_stream", + "metadata": {}, + "name": "reverse", + "run_id": "", + "tags": ["seq:step:2"], + }, + { + "data": {"chunk": "dlrowolleh"}, + "event": "on_chain_stream", + "metadata": {}, + "name": "RunnableSequence", + "run_id": "", + "tags": [], + }, + { + "data": {"input": "helloworld", "output": "dlrowolleh"}, + "event": "on_chain_end", + "metadata": {}, + "name": "reverse", + "run_id": "", + "tags": ["seq:step:2"], + }, + { + "data": {"output": "dlrowolleh"}, + "event": "on_chain_end", + "metadata": {}, + "name": "RunnableSequence", + "run_id": "", + "tags": [], + }, + ] + + +async def test_event_stream_with_retry() -> None: + """Test the event stream with a tool.""" + + def success(inputs: str) -> str: + return "success" + + def fail(inputs: str) -> None: + """Simple func.""" + raise Exception("fail") + + chain = RunnableLambda(success) | RunnableLambda(fail).with_retry( + stop_after_attempt=1, + ) + iterable = chain.astream_events("q", version="v1") + + events = [] + + for _ in range(10): + try: + next_chunk = await iterable.__anext__() + events.append(next_chunk) + except Exception: + break + + events = _with_nulled_run_id(events) + for event in events: + event["tags"] = sorted(event["tags"]) + + assert events == [ + { + "data": {"input": "q"}, + "event": "on_chain_start", + "metadata": {}, + "name": "RunnableSequence", + "run_id": "", + "tags": [], + }, + { + "data": {}, + "event": "on_chain_start", + "metadata": {}, + "name": "success", + "run_id": "", + "tags": ["seq:step:1"], + }, + { + "data": {"chunk": "success"}, + "event": "on_chain_stream", + "metadata": {}, + "name": "success", + "run_id": "", + "tags": ["seq:step:1"], + }, + { + "data": {}, + "event": "on_chain_start", + "metadata": {}, + "name": "fail", + "run_id": "", + "tags": ["seq:step:2"], + }, + { + "data": {"input": "q", "output": "success"}, + "event": "on_chain_end", + "metadata": {}, + "name": "success", + "run_id": "", + "tags": ["seq:step:1"], + }, + { + "data": {"input": "success", "output": None}, + "event": "on_chain_end", + "metadata": {}, + "name": "fail", + "run_id": "", + "tags": ["seq:step:2"], + }, + ] + + +async def test_with_llm() -> None: + """Test with regular llm.""" + prompt = ChatPromptTemplate.from_messages( + [("system", "You are Cat Agent 007"), ("human", "{question}")] + ).with_config({"run_name": "my_template", "tags": ["my_template"]}) + llm = FakeStreamingListLLM(responses=["abc"]) + + chain = prompt | llm + events = await _collect_events( + chain.astream_events({"question": "hello"}, version="v1") + ) + assert events == [ + { + "data": {"input": {"question": "hello"}}, + "event": "on_chain_start", + "metadata": {}, + "name": "RunnableSequence", + "run_id": "", + "tags": [], + }, + { + "data": {"input": {"question": "hello"}}, + "event": "on_prompt_start", + "metadata": {}, + "name": "my_template", + "run_id": "", + "tags": ["my_template", "seq:step:1"], + }, + { + "data": { + "input": {"question": "hello"}, + "output": ChatPromptValue( + messages=[ + SystemMessage(content="You are Cat Agent 007"), + HumanMessage(content="hello"), + ] + ), + }, + "event": "on_prompt_end", + "metadata": {}, + "name": "my_template", + "run_id": "", + "tags": ["my_template", "seq:step:1"], + }, + { + "data": { + "input": {"prompts": ["System: You are Cat Agent 007\n" "Human: hello"]} + }, + "event": "on_llm_start", + "metadata": {}, + "name": "FakeStreamingListLLM", + "run_id": "", + "tags": ["seq:step:2"], + }, + { + "data": { + "input": { + "prompts": ["System: You are Cat Agent 007\n" "Human: hello"] + }, + "output": { + "generations": [ + [{"generation_info": None, "text": "abc", "type": "Generation"}] + ], + "llm_output": None, + "run": None, + }, + }, + "event": "on_llm_end", + "metadata": {}, + "name": "FakeStreamingListLLM", + "run_id": "", + "tags": ["seq:step:2"], + }, + { + "data": {"chunk": "a"}, + "event": "on_chain_stream", + "metadata": {}, + "name": "RunnableSequence", + "run_id": "", + "tags": [], + }, + { + "data": {"chunk": "b"}, + "event": "on_chain_stream", + "metadata": {}, + "name": "RunnableSequence", + "run_id": "", + "tags": [], + }, + { + "data": {"chunk": "c"}, + "event": "on_chain_stream", + "metadata": {}, + "name": "RunnableSequence", + "run_id": "", + "tags": [], + }, + { + "data": {"output": "abc"}, + "event": "on_chain_end", + "metadata": {}, + "name": "RunnableSequence", + "run_id": "", + "tags": [], + }, + ] + + +async def test_runnable_each() -> None: + """Test runnable each astream_events.""" + + async def add_one(x: int) -> int: + return x + 1 + + add_one_map = RunnableLambda(add_one).map() # type: ignore + assert await add_one_map.ainvoke([1, 2, 3]) == [2, 3, 4] + + with pytest.raises(NotImplementedError): + async for _ in add_one_map.astream_events([1, 2, 3], version="v1"): + pass diff --git a/libs/langchain/_test_minimum_requirements.txt b/libs/langchain/_test_minimum_requirements.txt index aed919a615010..464405b7ec9ff 100644 --- a/libs/langchain/_test_minimum_requirements.txt +++ b/libs/langchain/_test_minimum_requirements.txt @@ -1,2 +1,2 @@ -langchain-core==0.1.7 -langchain-community==0.0.9 +langchain-core==0.1.14 +langchain-community==0.0.14 diff --git a/libs/langchain/langchain/agents/agent.py b/libs/langchain/langchain/agents/agent.py index 8fbdf976a1155..8bd7b6d478ab5 100644 --- a/libs/langchain/langchain/agents/agent.py +++ b/libs/langchain/langchain/agents/agent.py @@ -23,6 +23,14 @@ import yaml from langchain_core._api import deprecated from langchain_core.agents import AgentAction, AgentFinish, AgentStep +from langchain_core.callbacks import ( + AsyncCallbackManagerForChainRun, + AsyncCallbackManagerForToolRun, + BaseCallbackManager, + CallbackManagerForChainRun, + CallbackManagerForToolRun, + Callbacks, +) from langchain_core.exceptions import OutputParserException from langchain_core.language_models import BaseLanguageModel from langchain_core.messages import BaseMessage @@ -39,14 +47,6 @@ from langchain.agents.agent_iterator import AgentExecutorIterator from langchain.agents.agent_types import AgentType from langchain.agents.tools import InvalidTool -from langchain.callbacks.base import BaseCallbackManager -from langchain.callbacks.manager import ( - AsyncCallbackManagerForChainRun, - AsyncCallbackManagerForToolRun, - CallbackManagerForChainRun, - CallbackManagerForToolRun, - Callbacks, -) from langchain.chains.base import Chain from langchain.chains.llm import LLMChain from langchain.utilities.asyncio import asyncio_timeout @@ -343,8 +343,8 @@ class RunnableAgent(BaseSingleActionAgent): runnable: Runnable[dict, Union[AgentAction, AgentFinish]] """Runnable to call to get agent action.""" - _input_keys: List[str] = [] - """Input keys.""" + input_keys_arg: List[str] = [] + return_keys_arg: List[str] = [] class Config: """Configuration for this pydantic object.""" @@ -354,16 +354,11 @@ class Config: @property def return_values(self) -> List[str]: """Return values of the agent.""" - return [] + return self.return_keys_arg @property def input_keys(self) -> List[str]: - """Return the input keys. - - Returns: - List of input keys. - """ - return self._input_keys + return self.input_keys_arg def plan( self, @@ -439,8 +434,8 @@ class RunnableMultiActionAgent(BaseMultiActionAgent): runnable: Runnable[dict, Union[List[AgentAction], AgentFinish]] """Runnable to call to get agent actions.""" - _input_keys: List[str] = [] - """Input keys.""" + input_keys_arg: List[str] = [] + return_keys_arg: List[str] = [] class Config: """Configuration for this pydantic object.""" @@ -450,7 +445,7 @@ class Config: @property def return_values(self) -> List[str]: """Return values of the agent.""" - return [] + return self.return_keys_arg @property def input_keys(self) -> List[str]: @@ -459,7 +454,7 @@ def input_keys(self) -> List[str]: Returns: List of input keys. """ - return self._input_keys + return self.input_keys_arg def plan( self, @@ -1184,37 +1179,48 @@ def _iter_next_step( for agent_action in actions: yield agent_action for agent_action in actions: - if run_manager: - run_manager.on_agent_action(agent_action, color="green") - # Otherwise we lookup the tool - if agent_action.tool in name_to_tool_map: - tool = name_to_tool_map[agent_action.tool] - return_direct = tool.return_direct - color = color_mapping[agent_action.tool] - tool_run_kwargs = self.agent.tool_run_logging_kwargs() - if return_direct: - tool_run_kwargs["llm_prefix"] = "" - # We then call the tool on the tool input to get an observation - observation = tool.run( - agent_action.tool_input, - verbose=self.verbose, - color=color, - callbacks=run_manager.get_child() if run_manager else None, - **tool_run_kwargs, - ) - else: - tool_run_kwargs = self.agent.tool_run_logging_kwargs() - observation = InvalidTool().run( - { - "requested_tool_name": agent_action.tool, - "available_tool_names": list(name_to_tool_map.keys()), - }, - verbose=self.verbose, - color=None, - callbacks=run_manager.get_child() if run_manager else None, - **tool_run_kwargs, - ) - yield AgentStep(action=agent_action, observation=observation) + yield self._perform_agent_action( + name_to_tool_map, color_mapping, agent_action, run_manager + ) + + def _perform_agent_action( + self, + name_to_tool_map: Dict[str, BaseTool], + color_mapping: Dict[str, str], + agent_action: AgentAction, + run_manager: Optional[CallbackManagerForChainRun] = None, + ) -> AgentStep: + if run_manager: + run_manager.on_agent_action(agent_action, color="green") + # Otherwise we lookup the tool + if agent_action.tool in name_to_tool_map: + tool = name_to_tool_map[agent_action.tool] + return_direct = tool.return_direct + color = color_mapping[agent_action.tool] + tool_run_kwargs = self.agent.tool_run_logging_kwargs() + if return_direct: + tool_run_kwargs["llm_prefix"] = "" + # We then call the tool on the tool input to get an observation + observation = tool.run( + agent_action.tool_input, + verbose=self.verbose, + color=color, + callbacks=run_manager.get_child() if run_manager else None, + **tool_run_kwargs, + ) + else: + tool_run_kwargs = self.agent.tool_run_logging_kwargs() + observation = InvalidTool().run( + { + "requested_tool_name": agent_action.tool, + "available_tool_names": list(name_to_tool_map.keys()), + }, + verbose=self.verbose, + color=None, + callbacks=run_manager.get_child() if run_manager else None, + **tool_run_kwargs, + ) + return AgentStep(action=agent_action, observation=observation) async def _atake_next_step( self, @@ -1308,52 +1314,61 @@ async def _aiter_next_step( for agent_action in actions: yield agent_action - async def _aperform_agent_action( - agent_action: AgentAction, - ) -> AgentStep: - if run_manager: - await run_manager.on_agent_action( - agent_action, verbose=self.verbose, color="green" - ) - # Otherwise we lookup the tool - if agent_action.tool in name_to_tool_map: - tool = name_to_tool_map[agent_action.tool] - return_direct = tool.return_direct - color = color_mapping[agent_action.tool] - tool_run_kwargs = self.agent.tool_run_logging_kwargs() - if return_direct: - tool_run_kwargs["llm_prefix"] = "" - # We then call the tool on the tool input to get an observation - observation = await tool.arun( - agent_action.tool_input, - verbose=self.verbose, - color=color, - callbacks=run_manager.get_child() if run_manager else None, - **tool_run_kwargs, - ) - else: - tool_run_kwargs = self.agent.tool_run_logging_kwargs() - observation = await InvalidTool().arun( - { - "requested_tool_name": agent_action.tool, - "available_tool_names": list(name_to_tool_map.keys()), - }, - verbose=self.verbose, - color=None, - callbacks=run_manager.get_child() if run_manager else None, - **tool_run_kwargs, - ) - return AgentStep(action=agent_action, observation=observation) - # Use asyncio.gather to run multiple tool.arun() calls concurrently result = await asyncio.gather( - *[_aperform_agent_action(agent_action) for agent_action in actions] + *[ + self._aperform_agent_action( + name_to_tool_map, color_mapping, agent_action, run_manager + ) + for agent_action in actions + ], ) # TODO This could yield each result as it becomes available for chunk in result: yield chunk + async def _aperform_agent_action( + self, + name_to_tool_map: Dict[str, BaseTool], + color_mapping: Dict[str, str], + agent_action: AgentAction, + run_manager: Optional[AsyncCallbackManagerForChainRun] = None, + ) -> AgentStep: + if run_manager: + await run_manager.on_agent_action( + agent_action, verbose=self.verbose, color="green" + ) + # Otherwise we lookup the tool + if agent_action.tool in name_to_tool_map: + tool = name_to_tool_map[agent_action.tool] + return_direct = tool.return_direct + color = color_mapping[agent_action.tool] + tool_run_kwargs = self.agent.tool_run_logging_kwargs() + if return_direct: + tool_run_kwargs["llm_prefix"] = "" + # We then call the tool on the tool input to get an observation + observation = await tool.arun( + agent_action.tool_input, + verbose=self.verbose, + color=color, + callbacks=run_manager.get_child() if run_manager else None, + **tool_run_kwargs, + ) + else: + tool_run_kwargs = self.agent.tool_run_logging_kwargs() + observation = await InvalidTool().arun( + { + "requested_tool_name": agent_action.tool, + "available_tool_names": list(name_to_tool_map.keys()), + }, + verbose=self.verbose, + color=None, + callbacks=run_manager.get_child() if run_manager else None, + **tool_run_kwargs, + ) + return AgentStep(action=agent_action, observation=observation) + def _call( self, inputs: Dict[str, str], diff --git a/libs/langchain/langchain/agents/agent_iterator.py b/libs/langchain/langchain/agents/agent_iterator.py index 52b5bd2c1bc10..12c995f2e9708 100644 --- a/libs/langchain/langchain/agents/agent_iterator.py +++ b/libs/langchain/langchain/agents/agent_iterator.py @@ -20,20 +20,20 @@ AgentFinish, AgentStep, ) -from langchain_core.load.dump import dumpd -from langchain_core.outputs import RunInfo -from langchain_core.runnables.utils import AddableDict -from langchain_core.utils.input import get_color_mapping - -from langchain.callbacks.manager import ( +from langchain_core.callbacks import ( AsyncCallbackManager, AsyncCallbackManagerForChainRun, CallbackManager, CallbackManagerForChainRun, Callbacks, ) +from langchain_core.load.dump import dumpd +from langchain_core.outputs import RunInfo +from langchain_core.runnables.utils import AddableDict +from langchain_core.tools import BaseTool +from langchain_core.utils.input import get_color_mapping + from langchain.schema import RUN_KEY -from langchain.tools import BaseTool from langchain.utilities.asyncio import asyncio_timeout if TYPE_CHECKING: diff --git a/libs/langchain/langchain/agents/chat/base.py b/libs/langchain/langchain/agents/chat/base.py index 00a320a543ce1..ffd36852e1eff 100644 --- a/libs/langchain/langchain/agents/chat/base.py +++ b/libs/langchain/langchain/agents/chat/base.py @@ -2,6 +2,7 @@ from langchain_core._api import deprecated from langchain_core.agents import AgentAction +from langchain_core.callbacks import BaseCallbackManager from langchain_core.language_models import BaseLanguageModel from langchain_core.prompts import BasePromptTemplate from langchain_core.prompts.chat import ( @@ -21,7 +22,6 @@ SYSTEM_MESSAGE_SUFFIX, ) from langchain.agents.utils import validate_tools_single_input -from langchain.callbacks.base import BaseCallbackManager from langchain.chains.llm import LLMChain diff --git a/libs/langchain/langchain/agents/conversational/base.py b/libs/langchain/langchain/agents/conversational/base.py index fcc03ad1be353..11e2fc12470bf 100644 --- a/libs/langchain/langchain/agents/conversational/base.py +++ b/libs/langchain/langchain/agents/conversational/base.py @@ -4,6 +4,7 @@ from typing import Any, List, Optional, Sequence from langchain_core._api import deprecated +from langchain_core.callbacks import BaseCallbackManager from langchain_core.language_models import BaseLanguageModel from langchain_core.prompts import PromptTemplate from langchain_core.pydantic_v1 import Field @@ -14,7 +15,6 @@ from langchain.agents.conversational.output_parser import ConvoOutputParser from langchain.agents.conversational.prompt import FORMAT_INSTRUCTIONS, PREFIX, SUFFIX from langchain.agents.utils import validate_tools_single_input -from langchain.callbacks.base import BaseCallbackManager from langchain.chains import LLMChain diff --git a/libs/langchain/langchain/agents/conversational_chat/base.py b/libs/langchain/langchain/agents/conversational_chat/base.py index 00f0137cda0b2..8ee2218780bdc 100644 --- a/libs/langchain/langchain/agents/conversational_chat/base.py +++ b/libs/langchain/langchain/agents/conversational_chat/base.py @@ -5,6 +5,7 @@ from langchain_core._api import deprecated from langchain_core.agents import AgentAction +from langchain_core.callbacks import BaseCallbackManager from langchain_core.language_models import BaseLanguageModel from langchain_core.messages import AIMessage, BaseMessage, HumanMessage from langchain_core.output_parsers import BaseOutputParser @@ -26,7 +27,6 @@ TEMPLATE_TOOL_RESPONSE, ) from langchain.agents.utils import validate_tools_single_input -from langchain.callbacks.base import BaseCallbackManager from langchain.chains import LLMChain diff --git a/libs/langchain/langchain/agents/conversational_chat/output_parser.py b/libs/langchain/langchain/agents/conversational_chat/output_parser.py index 4265ee553341a..46ccce6d57189 100644 --- a/libs/langchain/langchain/agents/conversational_chat/output_parser.py +++ b/libs/langchain/langchain/agents/conversational_chat/output_parser.py @@ -4,10 +4,10 @@ from langchain_core.agents import AgentAction, AgentFinish from langchain_core.exceptions import OutputParserException +from langchain_core.output_parsers.json import parse_json_markdown from langchain.agents import AgentOutputParser from langchain.agents.conversational_chat.prompt import FORMAT_INSTRUCTIONS -from langchain.output_parsers.json import parse_json_markdown # Define a class that parses output for conversational agents diff --git a/libs/langchain/langchain/agents/initialize.py b/libs/langchain/langchain/agents/initialize.py index 65cfbd200398a..890bc90e68f01 100644 --- a/libs/langchain/langchain/agents/initialize.py +++ b/libs/langchain/langchain/agents/initialize.py @@ -2,13 +2,13 @@ from typing import Any, Optional, Sequence from langchain_core._api import deprecated +from langchain_core.callbacks import BaseCallbackManager from langchain_core.language_models import BaseLanguageModel from langchain_core.tools import BaseTool from langchain.agents.agent import AgentExecutor from langchain.agents.agent_types import AgentType from langchain.agents.loading import AGENT_TO_CLASS, load_agent -from langchain.callbacks.base import BaseCallbackManager @deprecated( diff --git a/libs/langchain/langchain/agents/json_chat/base.py b/libs/langchain/langchain/agents/json_chat/base.py index 3e34568a9721b..ffabf68921b0b 100644 --- a/libs/langchain/langchain/agents/json_chat/base.py +++ b/libs/langchain/langchain/agents/json_chat/base.py @@ -19,10 +19,7 @@ def create_json_chat_agent( Args: llm: LLM to use as the agent. tools: Tools this agent has access to. - prompt: The prompt to use, must have input keys: - `tools`: contains descriptions and arguments for each tool. - `tool_names`: contains all tool names. - `agent_scratchpad`: contains previous agent actions and tool outputs. + prompt: The prompt to use. See Prompt section below for more. Returns: A Runnable sequence representing an agent. It takes as input all the same input @@ -58,7 +55,14 @@ def create_json_chat_agent( } ) - Creating prompt example: + Prompt: + + The prompt must have input keys: + * `tools`: contains descriptions and arguments for each tool. + * `tool_names`: contains all tool names. + * `agent_scratchpad`: must be a MessagesPlaceholder. Contains previous agent actions and tool outputs as messages. + + Here's an example: .. code-block:: python diff --git a/libs/langchain/langchain/agents/load_tools.py b/libs/langchain/langchain/agents/load_tools.py index 192048b9fc119..24adaf867df71 100644 --- a/libs/langchain/langchain/agents/load_tools.py +++ b/libs/langchain/langchain/agents/load_tools.py @@ -18,10 +18,10 @@ from typing import Any, Dict, List, Optional, Callable, Tuple from mypy_extensions import Arg, KwArg -from langchain.agents.tools import Tool +from langchain_core.tools import Tool from langchain_core.language_models import BaseLanguageModel -from langchain.callbacks.base import BaseCallbackManager -from langchain.callbacks.manager import Callbacks +from langchain_core.callbacks import BaseCallbackManager +from langchain_core.callbacks import Callbacks from langchain.chains.api import news_docs, open_meteo_docs, podcast_docs, tmdb_docs from langchain.chains.api.base import APIChain from langchain.chains.llm_math.base import LLMMathChain @@ -353,9 +353,7 @@ def _get_scenexplain(**kwargs: Any) -> BaseTool: def _get_graphql_tool(**kwargs: Any) -> BaseTool: - graphql_endpoint = kwargs["graphql_endpoint"] - wrapper = GraphQLAPIWrapper(graphql_endpoint=graphql_endpoint) - return BaseGraphQLTool(graphql_wrapper=wrapper) + return BaseGraphQLTool(graphql_wrapper=GraphQLAPIWrapper(**kwargs)) def _get_openweathermap(**kwargs: Any) -> BaseTool: @@ -455,7 +453,7 @@ def _get_reddit_search(**kwargs: Any) -> BaseTool: ), "stackexchange": (_get_stackexchange, []), "sceneXplain": (_get_scenexplain, []), - "graphql": (_get_graphql_tool, ["graphql_endpoint"]), + "graphql": (_get_graphql_tool, ["graphql_endpoint", "custom_headers"]), "openweathermap-api": (_get_openweathermap, ["openweathermap_api_key"]), "dataforseo-api-search": ( _get_dataforseo_api_search, diff --git a/libs/langchain/langchain/agents/loading.py b/libs/langchain/langchain/agents/loading.py index 28540fcfd4a16..e1d9747df2d5f 100644 --- a/libs/langchain/langchain/agents/loading.py +++ b/libs/langchain/langchain/agents/loading.py @@ -7,10 +7,10 @@ import yaml from langchain_core._api import deprecated from langchain_core.language_models import BaseLanguageModel +from langchain_core.tools import Tool from langchain_core.utils.loading import try_load_from_hub from langchain.agents.agent import BaseMultiActionAgent, BaseSingleActionAgent -from langchain.agents.tools import Tool from langchain.agents.types import AGENT_TO_CLASS from langchain.chains.loading import load_chain, load_chain_from_config diff --git a/libs/langchain/langchain/agents/mrkl/base.py b/libs/langchain/langchain/agents/mrkl/base.py index 51e3ef2dbae3c..976ecf66aaedd 100644 --- a/libs/langchain/langchain/agents/mrkl/base.py +++ b/libs/langchain/langchain/agents/mrkl/base.py @@ -4,6 +4,7 @@ from typing import Any, Callable, List, NamedTuple, Optional, Sequence from langchain_core._api import deprecated +from langchain_core.callbacks import BaseCallbackManager from langchain_core.language_models import BaseLanguageModel from langchain_core.prompts import PromptTemplate from langchain_core.pydantic_v1 import Field @@ -15,7 +16,6 @@ from langchain.agents.mrkl.prompt import FORMAT_INSTRUCTIONS, PREFIX, SUFFIX from langchain.agents.tools import Tool from langchain.agents.utils import validate_tools_single_input -from langchain.callbacks.base import BaseCallbackManager from langchain.chains import LLMChain @@ -83,9 +83,9 @@ def create_prompt( tool_names = ", ".join([tool.name for tool in tools]) format_instructions = format_instructions.format(tool_names=tool_names) template = "\n\n".join([prefix, tool_strings, format_instructions, suffix]) - if input_variables is None: - input_variables = ["input", "agent_scratchpad"] - return PromptTemplate(template=template, input_variables=input_variables) + if input_variables: + return PromptTemplate(template=template, input_variables=input_variables) + return PromptTemplate.from_template(template) @classmethod def from_llm_and_tools( diff --git a/libs/langchain/langchain/agents/openai_assistant/base.py b/libs/langchain/langchain/agents/openai_assistant/base.py index 498957ad8bcac..67e361b02eb8e 100644 --- a/libs/langchain/langchain/agents/openai_assistant/base.py +++ b/libs/langchain/langchain/agents/openai_assistant/base.py @@ -7,13 +7,12 @@ from langchain_community.tools.convert_to_openai import format_tool_to_openai_tool from langchain_core.agents import AgentAction, AgentFinish +from langchain_core.callbacks import CallbackManager from langchain_core.load import dumpd from langchain_core.pydantic_v1 import Field from langchain_core.runnables import RunnableConfig, RunnableSerializable, ensure_config from langchain_core.tools import BaseTool -from langchain.callbacks.manager import CallbackManager - if TYPE_CHECKING: import openai from openai.types.beta.threads import ThreadMessage @@ -147,8 +146,8 @@ def execute_agent(agent, tools, input): """ # noqa: E501 - client: openai.OpenAI = Field(default_factory=_get_openai_client) - """OpenAI client.""" + client: Any = Field(default_factory=_get_openai_client) + """OpenAI or AzureOpenAI client.""" assistant_id: str """OpenAI assistant id.""" check_every_ms: float = 1_000.0 @@ -164,7 +163,7 @@ def create_assistant( tools: Sequence[Union[BaseTool, dict]], model: str, *, - client: Optional[openai.OpenAI] = None, + client: Optional[Union[openai.OpenAI, openai.AzureOpenAI]] = None, **kwargs: Any, ) -> OpenAIAssistantRunnable: """Create an OpenAI Assistant and instantiate the Runnable. @@ -174,7 +173,8 @@ def create_assistant( instructions: Assistant instructions. tools: Assistant tools. Can be passed in OpenAI format or as BaseTools. model: Assistant model to use. - client: OpenAI client. Will create default client if not specified. + client: OpenAI or AzureOpenAI client. + Will create default OpenAI client if not specified. Returns: OpenAIAssistantRunnable configured to run using the created assistant. @@ -192,7 +192,7 @@ def create_assistant( tools=openai_tools, model=model, ) - return cls(assistant_id=assistant.id, **kwargs) + return cls(assistant_id=assistant.id, client=client, **kwargs) def invoke( self, input: dict, config: Optional[RunnableConfig] = None diff --git a/libs/langchain/langchain/agents/openai_functions_agent/base.py b/libs/langchain/langchain/agents/openai_functions_agent/base.py index 1d486dc685631..633d7a27cd697 100644 --- a/libs/langchain/langchain/agents/openai_functions_agent/base.py +++ b/libs/langchain/langchain/agents/openai_functions_agent/base.py @@ -4,6 +4,7 @@ from langchain_community.tools.convert_to_openai import format_tool_to_openai_function from langchain_core._api import deprecated from langchain_core.agents import AgentAction, AgentFinish +from langchain_core.callbacks import BaseCallbackManager, Callbacks from langchain_core.language_models import BaseLanguageModel from langchain_core.messages import ( BaseMessage, @@ -27,8 +28,6 @@ from langchain.agents.output_parsers.openai_functions import ( OpenAIFunctionsAgentOutputParser, ) -from langchain.callbacks.base import BaseCallbackManager -from langchain.callbacks.manager import Callbacks @deprecated("0.1.0", alternative="create_openai_functions_agent", removal="0.2.0") @@ -240,8 +239,7 @@ def create_openai_functions_agent( so either be an OpenAI model that supports that or a wrapper of a different model that adds in equivalent support. tools: Tools this agent has access to. - prompt: The prompt to use, must have input key `agent_scratchpad`, which will - contain agent action and tool output messages. + prompt: The prompt to use. See Prompt section below for more. Returns: A Runnable sequence representing an agent. It takes as input all the same input @@ -279,7 +277,13 @@ def create_openai_functions_agent( } ) - Creating prompt example: + Prompt: + + The agent prompt must have an `agent_scratchpad` key that is a + ``MessagesPlaceholder``. Intermediate agent actions and tool output + messages will be passed in here. + + Here's an example: .. code-block:: python diff --git a/libs/langchain/langchain/agents/openai_functions_multi_agent/base.py b/libs/langchain/langchain/agents/openai_functions_multi_agent/base.py index 168f852c591c6..03d804c60fd6b 100644 --- a/libs/langchain/langchain/agents/openai_functions_multi_agent/base.py +++ b/libs/langchain/langchain/agents/openai_functions_multi_agent/base.py @@ -5,6 +5,7 @@ from langchain_core._api import deprecated from langchain_core.agents import AgentAction, AgentActionMessageLog, AgentFinish +from langchain_core.callbacks import BaseCallbackManager, Callbacks from langchain_core.exceptions import OutputParserException from langchain_core.language_models import BaseLanguageModel from langchain_core.messages import ( @@ -20,14 +21,12 @@ MessagesPlaceholder, ) from langchain_core.pydantic_v1 import root_validator +from langchain_core.tools import BaseTool from langchain.agents import BaseMultiActionAgent from langchain.agents.format_scratchpad.openai_functions import ( format_to_openai_function_messages, ) -from langchain.callbacks.base import BaseCallbackManager -from langchain.callbacks.manager import Callbacks -from langchain.tools import BaseTool # For backwards compatibility _FunctionsAgentAction = AgentActionMessageLog diff --git a/libs/langchain/langchain/agents/openai_tools/base.py b/libs/langchain/langchain/agents/openai_tools/base.py index 4395fb32bbd2d..e76ccd994ef5c 100644 --- a/libs/langchain/langchain/agents/openai_tools/base.py +++ b/libs/langchain/langchain/agents/openai_tools/base.py @@ -20,8 +20,8 @@ def create_openai_tools_agent( Args: llm: LLM to use as the agent. tools: Tools this agent has access to. - prompt: The prompt to use, must have input key `agent_scratchpad`, which will - contain agent action and tool output messages. + prompt: The prompt to use. See Prompt section below for more on the expected + input variables. Returns: A Runnable sequence representing an agent. It takes as input all the same input @@ -57,7 +57,13 @@ def create_openai_tools_agent( } ) - Creating prompt example: + Prompt: + + The agent prompt must have an `agent_scratchpad` key that is a + ``MessagesPlaceholder``. Intermediate agent actions and tool output + messages will be passed in here. + + Here's an example: .. code-block:: python diff --git a/libs/langchain/langchain/agents/output_parsers/json.py b/libs/langchain/langchain/agents/output_parsers/json.py index 5fa543ea9dfea..2dbc56178ff0a 100644 --- a/libs/langchain/langchain/agents/output_parsers/json.py +++ b/libs/langchain/langchain/agents/output_parsers/json.py @@ -5,9 +5,9 @@ from langchain_core.agents import AgentAction, AgentFinish from langchain_core.exceptions import OutputParserException +from langchain_core.output_parsers.json import parse_json_markdown from langchain.agents.agent import AgentOutputParser -from langchain.output_parsers.json import parse_json_markdown logger = logging.getLogger(__name__) diff --git a/libs/langchain/langchain/agents/react/agent.py b/libs/langchain/langchain/agents/react/agent.py index 23277c36540db..93709fca65bbd 100644 --- a/libs/langchain/langchain/agents/react/agent.py +++ b/libs/langchain/langchain/agents/react/agent.py @@ -20,11 +20,7 @@ def create_react_agent( Args: llm: LLM to use as the agent. tools: Tools this agent has access to. - prompt: The prompt to use, must have input keys: - `tools`: contains descriptions and arguments for each tool. - `tool_names`: contains all tool names. - `agent_scratchpad`: contains previous agent actions and tool outputs. - + prompt: The prompt to use. See Prompt section below for more. Returns: A Runnable sequence representing an agent. It takes as input all the same input @@ -59,7 +55,14 @@ def create_react_agent( } ) - Creating prompt example: + Prompt: + + The prompt must have input keys: + * `tools`: contains descriptions and arguments for each tool. + * `tool_names`: contains all tool names. + * `agent_scratchpad`: contains previous agent actions and tool outputs as a string. + + Here's an example: .. code-block:: python diff --git a/libs/langchain/langchain/agents/react/base.py b/libs/langchain/langchain/agents/react/base.py index fba742ac458f6..437eb80e72087 100644 --- a/libs/langchain/langchain/agents/react/base.py +++ b/libs/langchain/langchain/agents/react/base.py @@ -6,14 +6,13 @@ from langchain_core.language_models import BaseLanguageModel from langchain_core.prompts import BasePromptTemplate from langchain_core.pydantic_v1 import Field -from langchain_core.tools import BaseTool +from langchain_core.tools import BaseTool, Tool from langchain.agents.agent import Agent, AgentExecutor, AgentOutputParser from langchain.agents.agent_types import AgentType from langchain.agents.react.output_parser import ReActOutputParser from langchain.agents.react.textworld_prompt import TEXTWORLD_PROMPT from langchain.agents.react.wiki_prompt import WIKI_PROMPT -from langchain.agents.tools import Tool from langchain.agents.utils import validate_tools_single_input from langchain.docstore.base import Docstore diff --git a/libs/langchain/langchain/agents/self_ask_with_search/base.py b/libs/langchain/langchain/agents/self_ask_with_search/base.py index 9df14cb433574..26447f0239a7a 100644 --- a/libs/langchain/langchain/agents/self_ask_with_search/base.py +++ b/libs/langchain/langchain/agents/self_ask_with_search/base.py @@ -9,14 +9,13 @@ from langchain_core.prompts import BasePromptTemplate from langchain_core.pydantic_v1 import Field from langchain_core.runnables import Runnable, RunnablePassthrough -from langchain_core.tools import BaseTool +from langchain_core.tools import BaseTool, Tool from langchain.agents.agent import Agent, AgentExecutor, AgentOutputParser from langchain.agents.agent_types import AgentType from langchain.agents.format_scratchpad import format_log_to_str from langchain.agents.self_ask_with_search.output_parser import SelfAskOutputParser from langchain.agents.self_ask_with_search.prompt import PROMPT -from langchain.agents.tools import Tool from langchain.agents.utils import validate_tools_single_input @@ -122,7 +121,12 @@ def create_self_ask_with_search_agent( agent_executor.invoke({"input": "hi"}) - Create prompt example: + Prompt: + + The prompt must have input key `agent_scratchpad` which will + contain agent actions and tool outputs as a string. + + Here's an example: .. code-block:: python diff --git a/libs/langchain/langchain/agents/structured_chat/base.py b/libs/langchain/langchain/agents/structured_chat/base.py index 53e11b33c01d8..3dc29b754bd05 100644 --- a/libs/langchain/langchain/agents/structured_chat/base.py +++ b/libs/langchain/langchain/agents/structured_chat/base.py @@ -3,6 +3,7 @@ from langchain_core._api import deprecated from langchain_core.agents import AgentAction +from langchain_core.callbacks import BaseCallbackManager from langchain_core.language_models import BaseLanguageModel from langchain_core.prompts import BasePromptTemplate from langchain_core.prompts.chat import ( @@ -12,6 +13,7 @@ ) from langchain_core.pydantic_v1 import Field from langchain_core.runnables import Runnable, RunnablePassthrough +from langchain_core.tools import BaseTool from langchain.agents.agent import Agent, AgentOutputParser from langchain.agents.format_scratchpad import format_log_to_str @@ -20,9 +22,7 @@ StructuredChatOutputParserWithRetries, ) from langchain.agents.structured_chat.prompt import FORMAT_INSTRUCTIONS, PREFIX, SUFFIX -from langchain.callbacks.base import BaseCallbackManager from langchain.chains.llm import LLMChain -from langchain.tools import BaseTool from langchain.tools.render import render_text_description_and_args HUMAN_MESSAGE_TEMPLATE = "{input}\n\n{agent_scratchpad}" @@ -158,10 +158,7 @@ def create_structured_chat_agent( Args: llm: LLM to use as the agent. tools: Tools this agent has access to. - prompt: The prompt to use, must have input keys - `tools`: contains descriptions and arguments for each tool. - `tool_names`: contains all tool names. - `agent_scratchpad`: contains previous agent actions and tool outputs. + prompt: The prompt to use. See Prompt section below for more. Returns: A Runnable sequence representing an agent. It takes as input all the same input @@ -197,7 +194,14 @@ def create_structured_chat_agent( } ) - Creating prompt example: + Prompt: + + The prompt must have input keys: + * `tools`: contains descriptions and arguments for each tool. + * `tool_names`: contains all tool names. + * `agent_scratchpad`: contains previous agent actions and tool outputs as a string. + + Here's an example: .. code-block:: python diff --git a/libs/langchain/langchain/agents/tools.py b/libs/langchain/langchain/agents/tools.py index aaf360afbc5d5..e804ab25025e2 100644 --- a/libs/langchain/langchain/agents/tools.py +++ b/libs/langchain/langchain/agents/tools.py @@ -1,12 +1,11 @@ """Interface for tools.""" from typing import List, Optional -from langchain_core.tools import BaseTool, Tool, tool - -from langchain.callbacks.manager import ( +from langchain_core.callbacks import ( AsyncCallbackManagerForToolRun, CallbackManagerForToolRun, ) +from langchain_core.tools import BaseTool, Tool, tool class InvalidTool(BaseTool): diff --git a/libs/langchain/langchain/agents/xml/base.py b/libs/langchain/langchain/agents/xml/base.py index bd678979f746d..fe3f4883ece5a 100644 --- a/libs/langchain/langchain/agents/xml/base.py +++ b/libs/langchain/langchain/agents/xml/base.py @@ -2,6 +2,7 @@ from langchain_core._api import deprecated from langchain_core.agents import AgentAction, AgentFinish +from langchain_core.callbacks import Callbacks from langchain_core.language_models import BaseLanguageModel from langchain_core.prompts.base import BasePromptTemplate from langchain_core.prompts.chat import AIMessagePromptTemplate, ChatPromptTemplate @@ -12,7 +13,6 @@ from langchain.agents.format_scratchpad import format_xml from langchain.agents.output_parsers import XMLAgentOutputParser from langchain.agents.xml.prompt import agent_instructions -from langchain.callbacks.base import Callbacks from langchain.chains.llm import LLMChain from langchain.tools.render import render_text_description @@ -152,7 +152,13 @@ def create_xml_agent( } ) - Creating prompt example: + Prompt: + + The prompt must have input keys: + * `tools`: contains descriptions for each tool. + * `agent_scratchpad`: contains previous agent actions and tool outputs as an XML string. + + Here's an example: .. code-block:: python diff --git a/libs/langchain/langchain/callbacks/base.py b/libs/langchain/langchain/callbacks/base.py index 9b9d189d3f612..7eaedce6fc89c 100644 --- a/libs/langchain/langchain/callbacks/base.py +++ b/libs/langchain/langchain/callbacks/base.py @@ -1,7 +1,7 @@ """Base callback handler that can be used to handle callbacks in langchain.""" from __future__ import annotations -from langchain_core.callbacks.base import ( +from langchain_core.callbacks import ( AsyncCallbackHandler, BaseCallbackHandler, BaseCallbackManager, diff --git a/libs/langchain/langchain/callbacks/file.py b/libs/langchain/langchain/callbacks/file.py index 9768a9f031604..06bcecb027d0d 100644 --- a/libs/langchain/langchain/callbacks/file.py +++ b/libs/langchain/langchain/callbacks/file.py @@ -2,10 +2,9 @@ from typing import Any, Dict, Optional, TextIO, cast from langchain_core.agents import AgentAction, AgentFinish +from langchain_core.callbacks import BaseCallbackHandler from langchain_core.utils.input import print_text -from langchain.callbacks.base import BaseCallbackHandler - class FileCallbackHandler(BaseCallbackHandler): """Callback Handler that writes to a file.""" diff --git a/libs/langchain/langchain/callbacks/streaming_aiter.py b/libs/langchain/langchain/callbacks/streaming_aiter.py index 92218af87bbc2..2df5849db8f77 100644 --- a/libs/langchain/langchain/callbacks/streaming_aiter.py +++ b/libs/langchain/langchain/callbacks/streaming_aiter.py @@ -3,10 +3,9 @@ import asyncio from typing import Any, AsyncIterator, Dict, List, Literal, Union, cast +from langchain_core.callbacks import AsyncCallbackHandler from langchain_core.outputs import LLMResult -from langchain.callbacks.base import AsyncCallbackHandler - # TODO If used by two LLM runs in parallel this won't work as expected diff --git a/libs/langchain/langchain/callbacks/streaming_stdout.py b/libs/langchain/langchain/callbacks/streaming_stdout.py index e2a22232b5798..1870f79210aa3 100644 --- a/libs/langchain/langchain/callbacks/streaming_stdout.py +++ b/libs/langchain/langchain/callbacks/streaming_stdout.py @@ -1,4 +1,4 @@ """Callback Handler streams to stdout on new llm token.""" -from langchain_core.callbacks.streaming_stdout import StreamingStdOutCallbackHandler +from langchain_core.callbacks import StreamingStdOutCallbackHandler __all__ = ["StreamingStdOutCallbackHandler"] diff --git a/libs/langchain/langchain/callbacks/streaming_stdout_final_only.py b/libs/langchain/langchain/callbacks/streaming_stdout_final_only.py index 6f3593d4d7335..2dce9266a7b29 100644 --- a/libs/langchain/langchain/callbacks/streaming_stdout_final_only.py +++ b/libs/langchain/langchain/callbacks/streaming_stdout_final_only.py @@ -2,7 +2,7 @@ import sys from typing import Any, Dict, List, Optional -from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler +from langchain_core.callbacks import StreamingStdOutCallbackHandler DEFAULT_ANSWER_PREFIX_TOKENS = ["Final", "Answer", ":"] diff --git a/libs/langchain/langchain/chains/api/base.py b/libs/langchain/langchain/chains/api/base.py index 0afd7bd383cf5..212c04845c7a6 100644 --- a/libs/langchain/langchain/chains/api/base.py +++ b/libs/langchain/langchain/chains/api/base.py @@ -5,14 +5,14 @@ from urllib.parse import urlparse from langchain_community.utilities.requests import TextRequestsWrapper +from langchain_core.callbacks import ( + AsyncCallbackManagerForChainRun, + CallbackManagerForChainRun, +) from langchain_core.language_models import BaseLanguageModel from langchain_core.prompts import BasePromptTemplate from langchain_core.pydantic_v1 import Field, root_validator -from langchain.callbacks.manager import ( - AsyncCallbackManagerForChainRun, - CallbackManagerForChainRun, -) from langchain.chains.api.prompt import API_RESPONSE_PROMPT, API_URL_PROMPT from langchain.chains.base import Chain from langchain.chains.llm import LLMChain @@ -163,7 +163,7 @@ def _call( ) api_response = self.requests_wrapper.get(api_url) _run_manager.on_text( - api_response, color="yellow", end="\n", verbose=self.verbose + str(api_response), color="yellow", end="\n", verbose=self.verbose ) answer = self.api_answer_chain.predict( question=question, @@ -198,7 +198,7 @@ async def _acall( ) api_response = await self.requests_wrapper.aget(api_url) await _run_manager.on_text( - api_response, color="yellow", end="\n", verbose=self.verbose + str(api_response), color="yellow", end="\n", verbose=self.verbose ) answer = await self.api_answer_chain.apredict( question=question, diff --git a/libs/langchain/langchain/chains/api/openapi/chain.py b/libs/langchain/langchain/chains/api/openapi/chain.py index ead27fbf37da7..7fad47fedd8b3 100644 --- a/libs/langchain/langchain/chains/api/openapi/chain.py +++ b/libs/langchain/langchain/chains/api/openapi/chain.py @@ -6,11 +6,11 @@ from langchain_community.tools.openapi.utils.api_models import APIOperation from langchain_community.utilities.requests import Requests +from langchain_core.callbacks import CallbackManagerForChainRun, Callbacks from langchain_core.language_models import BaseLanguageModel from langchain_core.pydantic_v1 import BaseModel, Field from requests import Response -from langchain.callbacks.manager import CallbackManagerForChainRun, Callbacks from langchain.chains.api.openapi.requests_chain import APIRequesterChain from langchain.chains.api.openapi.response_chain import APIResponderChain from langchain.chains.base import Chain diff --git a/libs/langchain/langchain/chains/base.py b/libs/langchain/langchain/chains/base.py index 1d02272c01ea2..dc665ae9978c4 100644 --- a/libs/langchain/langchain/chains/base.py +++ b/libs/langchain/langchain/chains/base.py @@ -9,6 +9,14 @@ import yaml from langchain_core._api import deprecated +from langchain_core.callbacks import ( + AsyncCallbackManager, + AsyncCallbackManagerForChainRun, + BaseCallbackManager, + CallbackManager, + CallbackManagerForChainRun, + Callbacks, +) from langchain_core.load.dump import dumpd from langchain_core.memory import BaseMemory from langchain_core.outputs import RunInfo @@ -26,14 +34,6 @@ run_in_executor, ) -from langchain.callbacks.base import BaseCallbackManager -from langchain.callbacks.manager import ( - AsyncCallbackManager, - AsyncCallbackManagerForChainRun, - CallbackManager, - CallbackManagerForChainRun, - Callbacks, -) from langchain.schema import RUN_KEY logger = logging.getLogger(__name__) @@ -131,7 +131,7 @@ def invoke( callbacks = config.get("callbacks") tags = config.get("tags") metadata = config.get("metadata") - run_name = config.get("run_name") + run_name = config.get("run_name") or self.get_name() include_run_info = kwargs.get("include_run_info", False) return_only_outputs = kwargs.get("return_only_outputs", False) @@ -178,7 +178,7 @@ async def ainvoke( callbacks = config.get("callbacks") tags = config.get("tags") metadata = config.get("metadata") - run_name = config.get("run_name") + run_name = config.get("run_name") or self.get_name() include_run_info = kwargs.get("include_run_info", False) return_only_outputs = kwargs.get("return_only_outputs", False) diff --git a/libs/langchain/langchain/chains/combine_documents/base.py b/libs/langchain/langchain/chains/combine_documents/base.py index 019d9a6e2b3a8..7285fced33f3d 100644 --- a/libs/langchain/langchain/chains/combine_documents/base.py +++ b/libs/langchain/langchain/chains/combine_documents/base.py @@ -3,15 +3,15 @@ from abc import ABC, abstractmethod from typing import Any, Dict, List, Optional, Tuple, Type +from langchain_core.callbacks import ( + AsyncCallbackManagerForChainRun, + CallbackManagerForChainRun, +) from langchain_core.documents import Document from langchain_core.prompts import BasePromptTemplate, PromptTemplate from langchain_core.pydantic_v1 import BaseModel, Field, create_model from langchain_core.runnables.config import RunnableConfig -from langchain.callbacks.manager import ( - AsyncCallbackManagerForChainRun, - CallbackManagerForChainRun, -) from langchain.chains.base import Chain from langchain.text_splitter import RecursiveCharacterTextSplitter, TextSplitter diff --git a/libs/langchain/langchain/chains/combine_documents/map_reduce.py b/libs/langchain/langchain/chains/combine_documents/map_reduce.py index ef3a0fb7bf383..18be6d4cf279b 100644 --- a/libs/langchain/langchain/chains/combine_documents/map_reduce.py +++ b/libs/langchain/langchain/chains/combine_documents/map_reduce.py @@ -4,11 +4,11 @@ from typing import Any, Dict, List, Optional, Tuple, Type +from langchain_core.callbacks import Callbacks from langchain_core.documents import Document from langchain_core.pydantic_v1 import BaseModel, Extra, create_model, root_validator from langchain_core.runnables.config import RunnableConfig -from langchain.callbacks.manager import Callbacks from langchain.chains.combine_documents.base import BaseCombineDocumentsChain from langchain.chains.combine_documents.reduce import ReduceDocumentsChain from langchain.chains.llm import LLMChain diff --git a/libs/langchain/langchain/chains/combine_documents/map_rerank.py b/libs/langchain/langchain/chains/combine_documents/map_rerank.py index d3ac0410942a8..0466aac56b941 100644 --- a/libs/langchain/langchain/chains/combine_documents/map_rerank.py +++ b/libs/langchain/langchain/chains/combine_documents/map_rerank.py @@ -4,11 +4,11 @@ from typing import Any, Dict, List, Optional, Sequence, Tuple, Type, Union, cast +from langchain_core.callbacks import Callbacks from langchain_core.documents import Document from langchain_core.pydantic_v1 import BaseModel, Extra, create_model, root_validator from langchain_core.runnables.config import RunnableConfig -from langchain.callbacks.manager import Callbacks from langchain.chains.combine_documents.base import BaseCombineDocumentsChain from langchain.chains.llm import LLMChain from langchain.output_parsers.regex import RegexParser diff --git a/libs/langchain/langchain/chains/combine_documents/reduce.py b/libs/langchain/langchain/chains/combine_documents/reduce.py index 74e53c1fbd829..00e3efd02e076 100644 --- a/libs/langchain/langchain/chains/combine_documents/reduce.py +++ b/libs/langchain/langchain/chains/combine_documents/reduce.py @@ -4,10 +4,10 @@ from typing import Any, Callable, List, Optional, Protocol, Tuple +from langchain_core.callbacks import Callbacks from langchain_core.documents import Document from langchain_core.pydantic_v1 import Extra -from langchain.callbacks.manager import Callbacks from langchain.chains.combine_documents.base import BaseCombineDocumentsChain diff --git a/libs/langchain/langchain/chains/combine_documents/refine.py b/libs/langchain/langchain/chains/combine_documents/refine.py index 7a6c495328f07..dc34e1594b0a9 100644 --- a/libs/langchain/langchain/chains/combine_documents/refine.py +++ b/libs/langchain/langchain/chains/combine_documents/refine.py @@ -4,12 +4,12 @@ from typing import Any, Dict, List, Tuple +from langchain_core.callbacks import Callbacks from langchain_core.documents import Document from langchain_core.prompts import BasePromptTemplate, format_document from langchain_core.prompts.prompt import PromptTemplate from langchain_core.pydantic_v1 import Extra, Field, root_validator -from langchain.callbacks.manager import Callbacks from langchain.chains.combine_documents.base import ( BaseCombineDocumentsChain, ) diff --git a/libs/langchain/langchain/chains/combine_documents/stuff.py b/libs/langchain/langchain/chains/combine_documents/stuff.py index 73183687e984f..2dfdf80cf8406 100644 --- a/libs/langchain/langchain/chains/combine_documents/stuff.py +++ b/libs/langchain/langchain/chains/combine_documents/stuff.py @@ -1,6 +1,7 @@ """Chain that combines documents by stuffing into context.""" from typing import Any, Dict, List, Optional, Tuple +from langchain_core.callbacks import Callbacks from langchain_core.documents import Document from langchain_core.language_models import LanguageModelLike from langchain_core.output_parsers import BaseOutputParser, StrOutputParser @@ -8,7 +9,6 @@ from langchain_core.pydantic_v1 import Extra, Field, root_validator from langchain_core.runnables import Runnable, RunnablePassthrough -from langchain.callbacks.manager import Callbacks from langchain.chains.combine_documents.base import ( DEFAULT_DOCUMENT_PROMPT, DEFAULT_DOCUMENT_SEPARATOR, @@ -58,7 +58,7 @@ def create_stuff_documents_chain( from langchain.chains.combine_documents import create_stuff_documents_chain prompt = ChatPromptTemplate.from_messages( - [("system", "What are everyone's favorite colors:\n\n{context}")] + [("system", "What are everyone's favorite colors:\\n\\n{context}")] ) llm = ChatOpenAI(model_name="gpt-3.5-turbo") chain = create_stuff_documents_chain(llm, prompt) diff --git a/libs/langchain/langchain/chains/constitutional_ai/base.py b/libs/langchain/langchain/chains/constitutional_ai/base.py index 33b3886f40d97..90017cc7b53a1 100644 --- a/libs/langchain/langchain/chains/constitutional_ai/base.py +++ b/libs/langchain/langchain/chains/constitutional_ai/base.py @@ -1,10 +1,10 @@ """Chain for applying constitutional principles to the outputs of another chain.""" from typing import Any, Dict, List, Optional +from langchain_core.callbacks import CallbackManagerForChainRun from langchain_core.language_models import BaseLanguageModel from langchain_core.prompts import BasePromptTemplate -from langchain.callbacks.manager import CallbackManagerForChainRun from langchain.chains.base import Chain from langchain.chains.constitutional_ai.models import ConstitutionalPrinciple from langchain.chains.constitutional_ai.principles import PRINCIPLES diff --git a/libs/langchain/langchain/chains/conversational_retrieval/base.py b/libs/langchain/langchain/chains/conversational_retrieval/base.py index 456186f977830..c92f18f7e8570 100644 --- a/libs/langchain/langchain/chains/conversational_retrieval/base.py +++ b/libs/langchain/langchain/chains/conversational_retrieval/base.py @@ -7,6 +7,11 @@ from pathlib import Path from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union +from langchain_core.callbacks import ( + AsyncCallbackManagerForChainRun, + CallbackManagerForChainRun, + Callbacks, +) from langchain_core.documents import Document from langchain_core.language_models import BaseLanguageModel from langchain_core.messages import BaseMessage @@ -16,11 +21,6 @@ from langchain_core.runnables import RunnableConfig from langchain_core.vectorstores import VectorStore -from langchain.callbacks.manager import ( - AsyncCallbackManagerForChainRun, - CallbackManagerForChainRun, - Callbacks, -) from langchain.chains.base import Chain from langchain.chains.combine_documents.base import BaseCombineDocumentsChain from langchain.chains.combine_documents.stuff import StuffDocumentsChain diff --git a/libs/langchain/langchain/chains/elasticsearch_database/base.py b/libs/langchain/langchain/chains/elasticsearch_database/base.py index 46c336808f1d6..f4e839125dd29 100644 --- a/libs/langchain/langchain/chains/elasticsearch_database/base.py +++ b/libs/langchain/langchain/chains/elasticsearch_database/base.py @@ -3,16 +3,16 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional +from langchain_core.callbacks import CallbackManagerForChainRun from langchain_core.language_models import BaseLanguageModel from langchain_core.output_parsers import BaseLLMOutputParser +from langchain_core.output_parsers.json import SimpleJsonOutputParser from langchain_core.prompts import BasePromptTemplate from langchain_core.pydantic_v1 import Extra, root_validator -from langchain.callbacks.manager import CallbackManagerForChainRun from langchain.chains.base import Chain from langchain.chains.elasticsearch_database.prompts import ANSWER_PROMPT, DSL_PROMPT from langchain.chains.llm import LLMChain -from langchain.output_parsers.json import SimpleJsonOutputParser if TYPE_CHECKING: from elasticsearch import Elasticsearch diff --git a/libs/langchain/langchain/chains/ernie_functions/base.py b/libs/langchain/langchain/chains/ernie_functions/base.py index afed9d4257db2..9a12b70c97a3c 100644 --- a/libs/langchain/langchain/chains/ernie_functions/base.py +++ b/libs/langchain/langchain/chains/ernie_functions/base.py @@ -13,19 +13,22 @@ cast, ) -from langchain_core.output_parsers import BaseGenerationOutputParser, BaseOutputParser +from langchain_core.language_models import BaseLanguageModel +from langchain_core.output_parsers import ( + BaseGenerationOutputParser, + BaseLLMOutputParser, + BaseOutputParser, +) +from langchain_core.prompts import BasePromptTemplate +from langchain_core.pydantic_v1 import BaseModel from langchain_core.runnables import Runnable -from langchain.base_language import BaseLanguageModel from langchain.chains import LLMChain from langchain.output_parsers.ernie_functions import ( JsonOutputFunctionsParser, PydanticAttrOutputFunctionsParser, PydanticOutputFunctionsParser, ) -from langchain.prompts import BasePromptTemplate -from langchain.pydantic_v1 import BaseModel -from langchain.schema import BaseLLMOutputParser from langchain.utils.ernie_functions import convert_pydantic_to_ernie_function PYTHON_TO_JSON_TYPES = { diff --git a/libs/langchain/langchain/chains/flare/base.py b/libs/langchain/langchain/chains/flare/base.py index 06b58ca6d06be..c2ef8f2da60ca 100644 --- a/libs/langchain/langchain/chains/flare/base.py +++ b/libs/langchain/langchain/chains/flare/base.py @@ -6,15 +6,15 @@ import numpy as np from langchain_community.llms.openai import OpenAI +from langchain_core.callbacks import ( + CallbackManagerForChainRun, +) from langchain_core.language_models import BaseLanguageModel from langchain_core.outputs import Generation from langchain_core.prompts import BasePromptTemplate from langchain_core.pydantic_v1 import Field from langchain_core.retrievers import BaseRetriever -from langchain.callbacks.manager import ( - CallbackManagerForChainRun, -) from langchain.chains.base import Chain from langchain.chains.flare.prompts import ( PROMPT, diff --git a/libs/langchain/langchain/chains/graph_qa/arangodb.py b/libs/langchain/langchain/chains/graph_qa/arangodb.py index ac797f71953ae..4c723e2046fab 100644 --- a/libs/langchain/langchain/chains/graph_qa/arangodb.py +++ b/libs/langchain/langchain/chains/graph_qa/arangodb.py @@ -5,11 +5,11 @@ from typing import Any, Dict, List, Optional from langchain_community.graphs.arangodb_graph import ArangoGraph +from langchain_core.callbacks import CallbackManagerForChainRun +from langchain_core.language_models import BaseLanguageModel from langchain_core.prompts import BasePromptTemplate from langchain_core.pydantic_v1 import Field -from langchain.base_language import BaseLanguageModel -from langchain.callbacks.manager import CallbackManagerForChainRun from langchain.chains.base import Chain from langchain.chains.graph_qa.prompts import ( AQL_FIX_PROMPT, diff --git a/libs/langchain/langchain/chains/graph_qa/cypher.py b/libs/langchain/langchain/chains/graph_qa/cypher.py index c15837cce66a1..3376e56730803 100644 --- a/libs/langchain/langchain/chains/graph_qa/cypher.py +++ b/libs/langchain/langchain/chains/graph_qa/cypher.py @@ -5,11 +5,11 @@ from typing import Any, Dict, List, Optional from langchain_community.graphs.graph_store import GraphStore +from langchain_core.callbacks import CallbackManagerForChainRun from langchain_core.language_models import BaseLanguageModel from langchain_core.prompts import BasePromptTemplate from langchain_core.pydantic_v1 import Field -from langchain.callbacks.manager import CallbackManagerForChainRun from langchain.chains.base import Chain from langchain.chains.graph_qa.cypher_utils import CypherQueryCorrector, Schema from langchain.chains.graph_qa.prompts import CYPHER_GENERATION_PROMPT, CYPHER_QA_PROMPT diff --git a/libs/langchain/langchain/chains/graph_qa/falkordb.py b/libs/langchain/langchain/chains/graph_qa/falkordb.py index 6c9f7110df708..b7ead3d8714e0 100644 --- a/libs/langchain/langchain/chains/graph_qa/falkordb.py +++ b/libs/langchain/langchain/chains/graph_qa/falkordb.py @@ -5,11 +5,11 @@ from typing import Any, Dict, List, Optional from langchain_community.graphs import FalkorDBGraph +from langchain_core.callbacks import CallbackManagerForChainRun +from langchain_core.language_models import BaseLanguageModel from langchain_core.prompts import BasePromptTemplate from langchain_core.pydantic_v1 import Field -from langchain.base_language import BaseLanguageModel -from langchain.callbacks.manager import CallbackManagerForChainRun from langchain.chains.base import Chain from langchain.chains.graph_qa.prompts import CYPHER_GENERATION_PROMPT, CYPHER_QA_PROMPT from langchain.chains.llm import LLMChain diff --git a/libs/langchain/langchain/chains/graph_qa/hugegraph.py b/libs/langchain/langchain/chains/graph_qa/hugegraph.py index 0ca54111cb0d0..9e1f02493798b 100644 --- a/libs/langchain/langchain/chains/graph_qa/hugegraph.py +++ b/libs/langchain/langchain/chains/graph_qa/hugegraph.py @@ -4,11 +4,11 @@ from typing import Any, Dict, List, Optional from langchain_community.graphs.hugegraph import HugeGraph +from langchain_core.callbacks import CallbackManagerForChainRun from langchain_core.language_models import BaseLanguageModel from langchain_core.prompts import BasePromptTemplate from langchain_core.pydantic_v1 import Field -from langchain.callbacks.manager import CallbackManagerForChainRun from langchain.chains.base import Chain from langchain.chains.graph_qa.prompts import ( CYPHER_QA_PROMPT, diff --git a/libs/langchain/langchain/chains/graph_qa/kuzu.py b/libs/langchain/langchain/chains/graph_qa/kuzu.py index 3735e822833f8..7df4cdc846584 100644 --- a/libs/langchain/langchain/chains/graph_qa/kuzu.py +++ b/libs/langchain/langchain/chains/graph_qa/kuzu.py @@ -4,11 +4,11 @@ from typing import Any, Dict, List, Optional from langchain_community.graphs.kuzu_graph import KuzuGraph +from langchain_core.callbacks import CallbackManagerForChainRun from langchain_core.language_models import BaseLanguageModel from langchain_core.prompts import BasePromptTemplate from langchain_core.pydantic_v1 import Field -from langchain.callbacks.manager import CallbackManagerForChainRun from langchain.chains.base import Chain from langchain.chains.graph_qa.prompts import CYPHER_QA_PROMPT, KUZU_GENERATION_PROMPT from langchain.chains.llm import LLMChain diff --git a/libs/langchain/langchain/chains/graph_qa/nebulagraph.py b/libs/langchain/langchain/chains/graph_qa/nebulagraph.py index d722c3a8510d6..e53eaefb35e13 100644 --- a/libs/langchain/langchain/chains/graph_qa/nebulagraph.py +++ b/libs/langchain/langchain/chains/graph_qa/nebulagraph.py @@ -4,11 +4,11 @@ from typing import Any, Dict, List, Optional from langchain_community.graphs.nebula_graph import NebulaGraph +from langchain_core.callbacks import CallbackManagerForChainRun from langchain_core.language_models import BaseLanguageModel from langchain_core.prompts import BasePromptTemplate from langchain_core.pydantic_v1 import Field -from langchain.callbacks.manager import CallbackManagerForChainRun from langchain.chains.base import Chain from langchain.chains.graph_qa.prompts import CYPHER_QA_PROMPT, NGQL_GENERATION_PROMPT from langchain.chains.llm import LLMChain diff --git a/libs/langchain/langchain/chains/graph_qa/neptune_cypher.py b/libs/langchain/langchain/chains/graph_qa/neptune_cypher.py index 74985029d43be..8fec19f5e1d8d 100644 --- a/libs/langchain/langchain/chains/graph_qa/neptune_cypher.py +++ b/libs/langchain/langchain/chains/graph_qa/neptune_cypher.py @@ -4,11 +4,11 @@ from typing import Any, Dict, List, Optional from langchain_community.graphs import NeptuneGraph +from langchain_core.callbacks import CallbackManagerForChainRun +from langchain_core.language_models import BaseLanguageModel from langchain_core.prompts.base import BasePromptTemplate from langchain_core.pydantic_v1 import Field -from langchain.base_language import BaseLanguageModel -from langchain.callbacks.manager import CallbackManagerForChainRun from langchain.chains.base import Chain from langchain.chains.graph_qa.prompts import ( CYPHER_QA_PROMPT, diff --git a/libs/langchain/langchain/chains/graph_qa/sparql.py b/libs/langchain/langchain/chains/graph_qa/sparql.py index 1d8150b4bab3d..f9d4897090070 100644 --- a/libs/langchain/langchain/chains/graph_qa/sparql.py +++ b/libs/langchain/langchain/chains/graph_qa/sparql.py @@ -6,11 +6,11 @@ from typing import Any, Dict, List, Optional from langchain_community.graphs.rdf_graph import RdfGraph +from langchain_core.callbacks import CallbackManagerForChainRun from langchain_core.language_models import BaseLanguageModel from langchain_core.prompts.base import BasePromptTemplate from langchain_core.pydantic_v1 import Field -from langchain.callbacks.manager import CallbackManagerForChainRun from langchain.chains.base import Chain from langchain.chains.graph_qa.prompts import ( SPARQL_GENERATION_SELECT_PROMPT, diff --git a/libs/langchain/langchain/chains/hyde/base.py b/libs/langchain/langchain/chains/hyde/base.py index a9573f6b06804..ca658aea93c01 100644 --- a/libs/langchain/langchain/chains/hyde/base.py +++ b/libs/langchain/langchain/chains/hyde/base.py @@ -7,12 +7,12 @@ from typing import Any, Dict, List, Optional import numpy as np +from langchain_core.callbacks import CallbackManagerForChainRun from langchain_core.embeddings import Embeddings from langchain_core.language_models import BaseLanguageModel from langchain_core.prompts import BasePromptTemplate from langchain_core.pydantic_v1 import Extra -from langchain.callbacks.manager import CallbackManagerForChainRun from langchain.chains.base import Chain from langchain.chains.hyde.prompts import PROMPT_MAP from langchain.chains.llm import LLMChain diff --git a/libs/langchain/langchain/chains/llm.py b/libs/langchain/langchain/chains/llm.py index 2d9908643a185..f142902840593 100644 --- a/libs/langchain/langchain/chains/llm.py +++ b/libs/langchain/langchain/chains/llm.py @@ -4,6 +4,13 @@ import warnings from typing import Any, Dict, List, Optional, Sequence, Tuple, Union, cast +from langchain_core.callbacks import ( + AsyncCallbackManager, + AsyncCallbackManagerForChainRun, + CallbackManager, + CallbackManagerForChainRun, + Callbacks, +) from langchain_core.language_models import ( BaseLanguageModel, LanguageModelInput, @@ -24,13 +31,6 @@ from langchain_core.runnables.configurable import DynamicRunnable from langchain_core.utils.input import get_colored_text -from langchain.callbacks.manager import ( - AsyncCallbackManager, - AsyncCallbackManagerForChainRun, - CallbackManager, - CallbackManagerForChainRun, - Callbacks, -) from langchain.chains.base import Chain diff --git a/libs/langchain/langchain/chains/llm_checker/base.py b/libs/langchain/langchain/chains/llm_checker/base.py index 4e718c73457b1..1d9166364e696 100644 --- a/libs/langchain/langchain/chains/llm_checker/base.py +++ b/libs/langchain/langchain/chains/llm_checker/base.py @@ -4,11 +4,11 @@ import warnings from typing import Any, Dict, List, Optional +from langchain_core.callbacks import CallbackManagerForChainRun from langchain_core.language_models import BaseLanguageModel from langchain_core.prompts import PromptTemplate from langchain_core.pydantic_v1 import Extra, root_validator -from langchain.callbacks.manager import CallbackManagerForChainRun from langchain.chains.base import Chain from langchain.chains.llm import LLMChain from langchain.chains.llm_checker.prompt import ( diff --git a/libs/langchain/langchain/chains/llm_math/base.py b/libs/langchain/langchain/chains/llm_math/base.py index 7e520ef0c4f26..cada7dc3fa38c 100644 --- a/libs/langchain/langchain/chains/llm_math/base.py +++ b/libs/langchain/langchain/chains/llm_math/base.py @@ -6,14 +6,14 @@ import warnings from typing import Any, Dict, List, Optional +from langchain_core.callbacks import ( + AsyncCallbackManagerForChainRun, + CallbackManagerForChainRun, +) from langchain_core.language_models import BaseLanguageModel from langchain_core.prompts import BasePromptTemplate from langchain_core.pydantic_v1 import Extra, root_validator -from langchain.callbacks.manager import ( - AsyncCallbackManagerForChainRun, - CallbackManagerForChainRun, -) from langchain.chains.base import Chain from langchain.chains.llm import LLMChain from langchain.chains.llm_math.prompt import PROMPT diff --git a/libs/langchain/langchain/chains/llm_requests.py b/libs/langchain/langchain/chains/llm_requests.py index 8daf49f4eee37..ed79f92f97105 100644 --- a/libs/langchain/langchain/chains/llm_requests.py +++ b/libs/langchain/langchain/chains/llm_requests.py @@ -4,9 +4,9 @@ from typing import Any, Dict, List, Optional from langchain_community.utilities.requests import TextRequestsWrapper +from langchain_core.callbacks import CallbackManagerForChainRun from langchain_core.pydantic_v1 import Extra, Field, root_validator -from langchain.callbacks.manager import CallbackManagerForChainRun from langchain.chains import LLMChain from langchain.chains.base import Chain diff --git a/libs/langchain/langchain/chains/llm_summarization_checker/base.py b/libs/langchain/langchain/chains/llm_summarization_checker/base.py index a3659a53dbe57..b5d1c1d504a5e 100644 --- a/libs/langchain/langchain/chains/llm_summarization_checker/base.py +++ b/libs/langchain/langchain/chains/llm_summarization_checker/base.py @@ -6,11 +6,11 @@ from pathlib import Path from typing import Any, Dict, List, Optional +from langchain_core.callbacks import CallbackManagerForChainRun from langchain_core.language_models import BaseLanguageModel from langchain_core.prompts.prompt import PromptTemplate from langchain_core.pydantic_v1 import Extra, root_validator -from langchain.callbacks.manager import CallbackManagerForChainRun from langchain.chains.base import Chain from langchain.chains.llm import LLMChain from langchain.chains.sequential import SequentialChain diff --git a/libs/langchain/langchain/chains/mapreduce.py b/libs/langchain/langchain/chains/mapreduce.py index f8e29b58aa7dc..0bcf9907ba653 100644 --- a/libs/langchain/langchain/chains/mapreduce.py +++ b/libs/langchain/langchain/chains/mapreduce.py @@ -7,12 +7,12 @@ from typing import Any, Dict, List, Mapping, Optional +from langchain_core.callbacks import CallbackManagerForChainRun, Callbacks from langchain_core.documents import Document from langchain_core.language_models import BaseLanguageModel from langchain_core.prompts import BasePromptTemplate from langchain_core.pydantic_v1 import Extra -from langchain.callbacks.manager import CallbackManagerForChainRun, Callbacks from langchain.chains import ReduceDocumentsChain from langchain.chains.base import Chain from langchain.chains.combine_documents.base import BaseCombineDocumentsChain diff --git a/libs/langchain/langchain/chains/moderation.py b/libs/langchain/langchain/chains/moderation.py index c2935f3feb317..00d6cbd3f3528 100644 --- a/libs/langchain/langchain/chains/moderation.py +++ b/libs/langchain/langchain/chains/moderation.py @@ -1,11 +1,11 @@ """Pass input through a moderation endpoint.""" from typing import Any, Dict, List, Optional +from langchain_core.callbacks import CallbackManagerForChainRun from langchain_core.pydantic_v1 import root_validator +from langchain_core.utils import get_from_dict_or_env -from langchain.callbacks.manager import CallbackManagerForChainRun from langchain.chains.base import Chain -from langchain.utils import get_from_dict_or_env class OpenAIModerationChain(Chain): diff --git a/libs/langchain/langchain/chains/natbot/base.py b/libs/langchain/langchain/chains/natbot/base.py index 9e9d521e541c2..e74c3477b1419 100644 --- a/libs/langchain/langchain/chains/natbot/base.py +++ b/libs/langchain/langchain/chains/natbot/base.py @@ -5,10 +5,10 @@ from typing import Any, Dict, List, Optional from langchain_community.llms.openai import OpenAI +from langchain_core.callbacks import CallbackManagerForChainRun from langchain_core.language_models import BaseLanguageModel from langchain_core.pydantic_v1 import Extra, root_validator -from langchain.callbacks.manager import CallbackManagerForChainRun from langchain.chains.base import Chain from langchain.chains.llm import LLMChain from langchain.chains.natbot.prompt import PROMPT diff --git a/libs/langchain/langchain/chains/openai_functions/base.py b/libs/langchain/langchain/chains/openai_functions/base.py index 426a41a513150..7f8fdd4431b48 100644 --- a/libs/langchain/langchain/chains/openai_functions/base.py +++ b/libs/langchain/langchain/chains/openai_functions/base.py @@ -9,6 +9,8 @@ Union, ) +from langchain_core._api import deprecated +from langchain_core.language_models import BaseLanguageModel from langchain_core.output_parsers import ( BaseGenerationOutputParser, BaseLLMOutputParser, @@ -22,7 +24,6 @@ convert_to_openai_function, ) -from langchain.base_language import BaseLanguageModel from langchain.chains import LLMChain from langchain.output_parsers.openai_functions import ( JsonOutputFunctionsParser, @@ -106,7 +107,7 @@ def create_openai_fn_runnable( from typing import Optional - from langchain.chains.openai_functions import create_openai_fn_chain + from langchain.chains.openai_functions import create_openai_fn_runnable from langchain_community.chat_models import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate from langchain_core.pydantic_v1 import BaseModel, Field @@ -180,7 +181,7 @@ def create_structured_output_runnable( from typing import Optional - from langchain.chains.openai_functions import create_structured_output_chain + from langchain.chains.openai_functions import create_structured_output_runnable from langchain_community.chat_models import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate from langchain_core.pydantic_v1 import BaseModel, Field @@ -200,7 +201,7 @@ class Dog(BaseModel): ("human", "Tip: Make sure to answer in the correct format"), ] ) - chain = create_structured_output_chain(Dog, llm, prompt) + chain = create_structured_output_runnable(Dog, llm, prompt) chain.invoke({"input": "Harry was a chubby brown beagle who loved chicken"}) # -> Dog(name="Harry", color="brown", fav_food="chicken") """ # noqa: E501 @@ -236,6 +237,7 @@ class _OutputFormatter(BaseModel): """ --- Legacy --- """ +@deprecated(since="0.1.1", removal="0.2.0", alternative="create_openai_fn_runnable") def create_openai_fn_chain( functions: Sequence[Union[Dict[str, Any], Type[BaseModel], Callable]], llm: BaseLanguageModel, @@ -336,6 +338,9 @@ class RecordDog(BaseModel): return llm_chain +@deprecated( + since="0.1.1", removal="0.2.0", alternative="create_structured_output_runnable" +) def create_structured_output_chain( output_schema: Union[Dict[str, Any], Type[BaseModel]], llm: BaseLanguageModel, diff --git a/libs/langchain/langchain/chains/openai_functions/openapi.py b/libs/langchain/langchain/chains/openai_functions/openapi.py index 549872f25b2e8..4b1bc2e7faca7 100644 --- a/libs/langchain/langchain/chains/openai_functions/openapi.py +++ b/libs/langchain/langchain/chains/openai_functions/openapi.py @@ -8,12 +8,12 @@ import requests from langchain_community.chat_models import ChatOpenAI from langchain_community.utilities.openapi import OpenAPISpec +from langchain_core.callbacks import CallbackManagerForChainRun from langchain_core.language_models import BaseLanguageModel from langchain_core.prompts import BasePromptTemplate, ChatPromptTemplate from langchain_core.utils.input import get_colored_text from requests import Response -from langchain.callbacks.manager import CallbackManagerForChainRun from langchain.chains.base import Chain from langchain.chains.llm import LLMChain from langchain.chains.sequential import SequentialChain diff --git a/libs/langchain/langchain/chains/openai_tools/extraction.py b/libs/langchain/langchain/chains/openai_tools/extraction.py index 6e7ebe65a07cc..eda3e4bd59f09 100644 --- a/libs/langchain/langchain/chains/openai_tools/extraction.py +++ b/libs/langchain/langchain/chains/openai_tools/extraction.py @@ -4,9 +4,9 @@ from langchain_core.prompts import ChatPromptTemplate from langchain_core.pydantic_v1 import BaseModel from langchain_core.runnables import Runnable +from langchain_core.utils.function_calling import convert_pydantic_to_openai_function from langchain.output_parsers import PydanticToolsParser -from langchain.utils.openai_functions import convert_pydantic_to_openai_function _EXTRACTION_TEMPLATE = """Extract and save the relevant entities mentioned \ in the following passage together with their properties. diff --git a/libs/langchain/langchain/chains/qa_generation/base.py b/libs/langchain/langchain/chains/qa_generation/base.py index cbb0eb36bac1c..d2ce08c80eca9 100644 --- a/libs/langchain/langchain/chains/qa_generation/base.py +++ b/libs/langchain/langchain/chains/qa_generation/base.py @@ -3,11 +3,11 @@ import json from typing import Any, Dict, List, Optional +from langchain_core.callbacks import CallbackManagerForChainRun from langchain_core.language_models import BaseLanguageModel from langchain_core.prompts import BasePromptTemplate from langchain_core.pydantic_v1 import Field -from langchain.callbacks.manager import CallbackManagerForChainRun from langchain.chains.base import Chain from langchain.chains.llm import LLMChain from langchain.chains.qa_generation.prompt import PROMPT_SELECTOR diff --git a/libs/langchain/langchain/chains/qa_with_sources/base.py b/libs/langchain/langchain/chains/qa_with_sources/base.py index 2144824a0922f..02a1b3aa0a4ee 100644 --- a/libs/langchain/langchain/chains/qa_with_sources/base.py +++ b/libs/langchain/langchain/chains/qa_with_sources/base.py @@ -7,15 +7,15 @@ from abc import ABC, abstractmethod from typing import Any, Dict, List, Optional, Tuple +from langchain_core.callbacks import ( + AsyncCallbackManagerForChainRun, + CallbackManagerForChainRun, +) from langchain_core.documents import Document from langchain_core.language_models import BaseLanguageModel from langchain_core.prompts import BasePromptTemplate from langchain_core.pydantic_v1 import Extra, root_validator -from langchain.callbacks.manager import ( - AsyncCallbackManagerForChainRun, - CallbackManagerForChainRun, -) from langchain.chains import ReduceDocumentsChain from langchain.chains.base import Chain from langchain.chains.combine_documents.base import BaseCombineDocumentsChain diff --git a/libs/langchain/langchain/chains/qa_with_sources/retrieval.py b/libs/langchain/langchain/chains/qa_with_sources/retrieval.py index a2b5c56265268..9642f626ad61a 100644 --- a/libs/langchain/langchain/chains/qa_with_sources/retrieval.py +++ b/libs/langchain/langchain/chains/qa_with_sources/retrieval.py @@ -2,14 +2,14 @@ from typing import Any, Dict, List +from langchain_core.callbacks import ( + AsyncCallbackManagerForChainRun, + CallbackManagerForChainRun, +) from langchain_core.documents import Document from langchain_core.pydantic_v1 import Field from langchain_core.retrievers import BaseRetriever -from langchain.callbacks.manager import ( - AsyncCallbackManagerForChainRun, - CallbackManagerForChainRun, -) from langchain.chains.combine_documents.stuff import StuffDocumentsChain from langchain.chains.qa_with_sources.base import BaseQAWithSourcesChain diff --git a/libs/langchain/langchain/chains/qa_with_sources/vector_db.py b/libs/langchain/langchain/chains/qa_with_sources/vector_db.py index 6feddc5bb778c..31e97bbf22e00 100644 --- a/libs/langchain/langchain/chains/qa_with_sources/vector_db.py +++ b/libs/langchain/langchain/chains/qa_with_sources/vector_db.py @@ -3,14 +3,14 @@ import warnings from typing import Any, Dict, List +from langchain_core.callbacks import ( + AsyncCallbackManagerForChainRun, + CallbackManagerForChainRun, +) from langchain_core.documents import Document from langchain_core.pydantic_v1 import Field, root_validator from langchain_core.vectorstores import VectorStore -from langchain.callbacks.manager import ( - AsyncCallbackManagerForChainRun, - CallbackManagerForChainRun, -) from langchain.chains.combine_documents.stuff import StuffDocumentsChain from langchain.chains.qa_with_sources.base import BaseQAWithSourcesChain diff --git a/libs/langchain/langchain/chains/query_constructor/base.py b/libs/langchain/langchain/chains/query_constructor/base.py index ecbffe18163c8..d99c046abfadf 100644 --- a/libs/langchain/langchain/chains/query_constructor/base.py +++ b/libs/langchain/langchain/chains/query_constructor/base.py @@ -7,6 +7,7 @@ from langchain_core.exceptions import OutputParserException from langchain_core.language_models import BaseLanguageModel from langchain_core.output_parsers import BaseOutputParser +from langchain_core.output_parsers.json import parse_and_check_json_markdown from langchain_core.prompts import BasePromptTemplate from langchain_core.prompts.few_shot import FewShotPromptTemplate from langchain_core.runnables import Runnable @@ -34,7 +35,6 @@ USER_SPECIFIED_EXAMPLE_PROMPT, ) from langchain.chains.query_constructor.schema import AttributeInfo -from langchain.output_parsers.json import parse_and_check_json_markdown class StructuredQueryOutputParser(BaseOutputParser[StructuredQuery]): diff --git a/libs/langchain/langchain/chains/question_answering/__init__.py b/libs/langchain/langchain/chains/question_answering/__init__.py index 3fd5070f1a7bd..34d6c917626de 100644 --- a/libs/langchain/langchain/chains/question_answering/__init__.py +++ b/libs/langchain/langchain/chains/question_answering/__init__.py @@ -1,11 +1,10 @@ """Load question answering chains.""" from typing import Any, Mapping, Optional, Protocol +from langchain_core.callbacks import BaseCallbackManager, Callbacks from langchain_core.language_models import BaseLanguageModel from langchain_core.prompts import BasePromptTemplate -from langchain.callbacks.base import BaseCallbackManager -from langchain.callbacks.manager import Callbacks from langchain.chains import ReduceDocumentsChain from langchain.chains.combine_documents.base import BaseCombineDocumentsChain from langchain.chains.combine_documents.map_reduce import MapReduceDocumentsChain diff --git a/libs/langchain/langchain/chains/retrieval_qa/base.py b/libs/langchain/langchain/chains/retrieval_qa/base.py index 94fd932078e19..392a0b2cc442f 100644 --- a/libs/langchain/langchain/chains/retrieval_qa/base.py +++ b/libs/langchain/langchain/chains/retrieval_qa/base.py @@ -6,6 +6,11 @@ from abc import abstractmethod from typing import Any, Dict, List, Optional +from langchain_core.callbacks import ( + AsyncCallbackManagerForChainRun, + CallbackManagerForChainRun, + Callbacks, +) from langchain_core.documents import Document from langchain_core.language_models import BaseLanguageModel from langchain_core.prompts import PromptTemplate @@ -13,11 +18,6 @@ from langchain_core.retrievers import BaseRetriever from langchain_core.vectorstores import VectorStore -from langchain.callbacks.manager import ( - AsyncCallbackManagerForChainRun, - CallbackManagerForChainRun, - Callbacks, -) from langchain.chains.base import Chain from langchain.chains.combine_documents.base import BaseCombineDocumentsChain from langchain.chains.combine_documents.stuff import StuffDocumentsChain diff --git a/libs/langchain/langchain/chains/router/base.py b/libs/langchain/langchain/chains/router/base.py index c2acb27f91536..e6a9788d0f6dd 100644 --- a/libs/langchain/langchain/chains/router/base.py +++ b/libs/langchain/langchain/chains/router/base.py @@ -4,13 +4,13 @@ from abc import ABC from typing import Any, Dict, List, Mapping, NamedTuple, Optional -from langchain_core.pydantic_v1 import Extra - -from langchain.callbacks.manager import ( +from langchain_core.callbacks import ( AsyncCallbackManagerForChainRun, CallbackManagerForChainRun, Callbacks, ) +from langchain_core.pydantic_v1 import Extra + from langchain.chains.base import Chain diff --git a/libs/langchain/langchain/chains/router/embedding_router.py b/libs/langchain/langchain/chains/router/embedding_router.py index a7efd41a5df10..b8f9d975bd953 100644 --- a/libs/langchain/langchain/chains/router/embedding_router.py +++ b/libs/langchain/langchain/chains/router/embedding_router.py @@ -2,12 +2,12 @@ from typing import Any, Dict, List, Optional, Sequence, Tuple, Type +from langchain_core.callbacks import CallbackManagerForChainRun from langchain_core.documents import Document from langchain_core.embeddings import Embeddings from langchain_core.pydantic_v1 import Extra from langchain_core.vectorstores import VectorStore -from langchain.callbacks.manager import CallbackManagerForChainRun from langchain.chains.router.base import RouterChain diff --git a/libs/langchain/langchain/chains/router/llm_router.py b/libs/langchain/langchain/chains/router/llm_router.py index 03662160c1bd2..9443321827a61 100644 --- a/libs/langchain/langchain/chains/router/llm_router.py +++ b/libs/langchain/langchain/chains/router/llm_router.py @@ -3,16 +3,16 @@ from typing import Any, Dict, List, Optional, Type, cast +from langchain_core.callbacks import ( + AsyncCallbackManagerForChainRun, + CallbackManagerForChainRun, +) from langchain_core.exceptions import OutputParserException from langchain_core.language_models import BaseLanguageModel from langchain_core.output_parsers import BaseOutputParser from langchain_core.prompts import BasePromptTemplate from langchain_core.pydantic_v1 import root_validator -from langchain.callbacks.manager import ( - AsyncCallbackManagerForChainRun, - CallbackManagerForChainRun, -) from langchain.chains import LLMChain from langchain.chains.router.base import RouterChain from langchain.output_parsers.json import parse_and_check_json_markdown diff --git a/libs/langchain/langchain/chains/sequential.py b/libs/langchain/langchain/chains/sequential.py index d9462434fd44d..29cf5dc04d4e8 100644 --- a/libs/langchain/langchain/chains/sequential.py +++ b/libs/langchain/langchain/chains/sequential.py @@ -1,13 +1,13 @@ """Chain pipeline where the outputs of one step feed directly into next.""" from typing import Any, Dict, List, Optional -from langchain_core.pydantic_v1 import Extra, root_validator -from langchain_core.utils.input import get_color_mapping - -from langchain.callbacks.manager import ( +from langchain_core.callbacks import ( AsyncCallbackManagerForChainRun, CallbackManagerForChainRun, ) +from langchain_core.pydantic_v1 import Extra, root_validator +from langchain_core.utils.input import get_color_mapping + from langchain.chains.base import Chain diff --git a/libs/langchain/langchain/chains/sql_database/prompt.py b/libs/langchain/langchain/chains/sql_database/prompt.py index 34cd2307b6b38..d11740e4b4b29 100644 --- a/libs/langchain/langchain/chains/sql_database/prompt.py +++ b/libs/langchain/langchain/chains/sql_database/prompt.py @@ -1,5 +1,5 @@ # flake8: noqa -from langchain.output_parsers.list import CommaSeparatedListOutputParser +from langchain_core.output_parsers.list import CommaSeparatedListOutputParser from langchain_core.prompts.prompt import PromptTemplate diff --git a/libs/langchain/langchain/chains/sql_database/query.py b/libs/langchain/langchain/chains/sql_database/query.py index 63a61fc102277..6bd074c716605 100644 --- a/libs/langchain/langchain/chains/sql_database/query.py +++ b/libs/langchain/langchain/chains/sql_database/query.py @@ -1,10 +1,10 @@ -from typing import List, Optional, TypedDict, Union +from typing import Any, Dict, List, Optional, TypedDict, Union from langchain_community.utilities.sql_database import SQLDatabase from langchain_core.language_models import BaseLanguageModel from langchain_core.output_parsers import StrOutputParser from langchain_core.prompts import BasePromptTemplate -from langchain_core.runnables import Runnable, RunnableParallel +from langchain_core.runnables import Runnable, RunnablePassthrough from langchain.chains.sql_database.prompt import PROMPT, SQL_PROMPTS @@ -31,7 +31,7 @@ def create_sql_query_chain( db: SQLDatabase, prompt: Optional[BasePromptTemplate] = None, k: int = 5, -) -> Runnable[Union[SQLInput, SQLInputWithTables], str]: +) -> Runnable[Union[SQLInput, SQLInputWithTables, Dict[str, Any]], str]: """Create a chain that generates SQL queries. *Security Note*: This chain generates SQL queries for the given database. @@ -50,34 +50,93 @@ def create_sql_query_chain( See https://python.langchain.com/docs/security for more information. Args: - llm: The language model to use - db: The SQLDatabase to generate the query for + llm: The language model to use. + db: The SQLDatabase to generate the query for. prompt: The prompt to use. If none is provided, will choose one - based on dialect. Defaults to None. + based on dialect. Defaults to None. See Prompt section below for more. k: The number of results per select statement to return. Defaults to 5. Returns: A chain that takes in a question and generates a SQL query that answers that question. - """ + + Example: + + .. code-block:: python + + # pip install -U langchain langchain-community langchain-openai + from langchain_openai import ChatOpenAI + from langchain.chains import create_sql_query_chain + from langchain_community.utilities import SQLDatabase + + db = SQLDatabase.from_uri("sqlite:///Chinook.db") + llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0) + chain = create_sql_query_chain(llm, db) + response = chain.invoke({"question": "How many employees are there"}) + + Prompt: + If no prompt is provided, a default prompt is selected based on the SQLDatabase dialect. If one is provided, it must support input variables: + * input: The user question plus suffix "\nSQLQuery: " is passed here. + * top_k: The number of results per select statement (the `k` argument to + this function) is passed in here. + * table_info: Table definitions and sample rows are passed in here. If the + user specifies "table_names_to_use" when invoking chain, only those + will be included. Otherwise, all tables are included. + * dialect (optional): If dialect input variable is in prompt, the db + dialect will be passed in here. + + Here's an example prompt: + + .. code-block:: python + + from langchain_core.prompts import PromptTemplate + + template = '''Given an input question, first create a syntactically correct {dialect} query to run, then look at the results of the query and return the answer. + Use the following format: + + Question: "Question here" + SQLQuery: "SQL Query to run" + SQLResult: "Result of the SQLQuery" + Answer: "Final answer here" + + Only use the following tables: + + {table_info}. + + Question: {input}''' + prompt = PromptTemplate.from_template(template) + """ # noqa: E501 if prompt is not None: prompt_to_use = prompt elif db.dialect in SQL_PROMPTS: prompt_to_use = SQL_PROMPTS[db.dialect] else: prompt_to_use = PROMPT + if {"input", "top_k", "table_info"}.difference(prompt_to_use.input_variables): + raise ValueError( + f"Prompt must have input variables: 'input', 'top_k', " + f"'table_info'. Received prompt with input variables: " + f"{prompt_to_use.input_variables}. Full prompt:\n\n{prompt_to_use}" + ) + if "dialect" in prompt_to_use.input_variables: + prompt_to_use = prompt_to_use.partial(dialect=db.dialect) + inputs = { "input": lambda x: x["question"] + "\nSQLQuery: ", - "top_k": lambda _: k, "table_info": lambda x: db.get_table_info( table_names=x.get("table_names_to_use") ), } - if "dialect" in prompt_to_use.input_variables: - inputs["dialect"] = lambda _: (db.dialect, prompt_to_use) return ( - RunnableParallel(inputs) - | prompt_to_use + RunnablePassthrough.assign(**inputs) # type: ignore + | ( + lambda x: { + k: v + for k, v in x.items() + if k not in ("question", "table_names_to_use") + } + ) + | prompt_to_use.partial(top_k=str(k)) | llm.bind(stop=["\nSQLResult:"]) | StrOutputParser() | _strip diff --git a/libs/langchain/langchain/chains/summarize/__init__.py b/libs/langchain/langchain/chains/summarize/__init__.py index ab17d07952c7f..9bc8b8118bd6b 100644 --- a/libs/langchain/langchain/chains/summarize/__init__.py +++ b/libs/langchain/langchain/chains/summarize/__init__.py @@ -1,10 +1,10 @@ """Load summarizing chains.""" from typing import Any, Mapping, Optional, Protocol +from langchain_core.callbacks import Callbacks from langchain_core.language_models import BaseLanguageModel from langchain_core.prompts import BasePromptTemplate -from langchain.callbacks.manager import Callbacks from langchain.chains.combine_documents.base import BaseCombineDocumentsChain from langchain.chains.combine_documents.map_reduce import MapReduceDocumentsChain from langchain.chains.combine_documents.reduce import ReduceDocumentsChain diff --git a/libs/langchain/langchain/chains/transform.py b/libs/langchain/langchain/chains/transform.py index e251cff2c931a..17a51b205c5ce 100644 --- a/libs/langchain/langchain/chains/transform.py +++ b/libs/langchain/langchain/chains/transform.py @@ -3,12 +3,12 @@ import logging from typing import Any, Awaitable, Callable, Dict, List, Optional -from langchain_core.pydantic_v1 import Field - -from langchain.callbacks.manager import ( +from langchain_core.callbacks import ( AsyncCallbackManagerForChainRun, CallbackManagerForChainRun, ) +from langchain_core.pydantic_v1 import Field + from langchain.chains.base import Chain logger = logging.getLogger(__name__) diff --git a/libs/langchain/langchain/memory/entity.py b/libs/langchain/langchain/memory/entity.py index f2fb62ed9659e..65b31cf2d34dc 100644 --- a/libs/langchain/langchain/memory/entity.py +++ b/libs/langchain/langchain/memory/entity.py @@ -236,6 +236,12 @@ class SQLiteEntityStore(BaseEntityStore): session_id: str = "default" table_name: str = "memory_store" + conn: Any = None + + class Config: + """Configuration for this pydantic object.""" + + arbitrary_types_allowed = True def __init__( self, diff --git a/libs/langchain/langchain/output_parsers/datetime.py b/libs/langchain/langchain/output_parsers/datetime.py index 837f0b0b8be31..100e324c8a876 100644 --- a/libs/langchain/langchain/output_parsers/datetime.py +++ b/libs/langchain/langchain/output_parsers/datetime.py @@ -4,8 +4,7 @@ from langchain_core.exceptions import OutputParserException from langchain_core.output_parsers import BaseOutputParser - -from langchain.utils import comma_list +from langchain_core.utils import comma_list def _generate_random_datetime_strings( diff --git a/libs/langchain/langchain/output_parsers/ernie_functions.py b/libs/langchain/langchain/output_parsers/ernie_functions.py index 0bd481fa8c592..c258f854ef51f 100644 --- a/libs/langchain/langchain/output_parsers/ernie_functions.py +++ b/libs/langchain/langchain/output_parsers/ernie_functions.py @@ -1,180 +1,15 @@ -import copy -import json -from typing import Any, Dict, List, Optional, Type, Union - -import jsonpatch -from langchain_core.output_parsers import ( - BaseCumulativeTransformOutputParser, - BaseGenerationOutputParser, -) - -from langchain.output_parsers.json import parse_partial_json -from langchain.pydantic_v1 import BaseModel, root_validator -from langchain.schema import ( - ChatGeneration, - Generation, - OutputParserException, +from langchain_community.output_parsers.ernie_functions import ( + JsonKeyOutputFunctionsParser, + JsonOutputFunctionsParser, + OutputFunctionsParser, + PydanticAttrOutputFunctionsParser, + PydanticOutputFunctionsParser, ) - -class OutputFunctionsParser(BaseGenerationOutputParser[Any]): - """Parse an output that is one of sets of values.""" - - args_only: bool = True - """Whether to only return the arguments to the function call.""" - - def parse_result(self, result: List[Generation], *, partial: bool = False) -> Any: - generation = result[0] - if not isinstance(generation, ChatGeneration): - raise OutputParserException( - "This output parser can only be used with a chat generation." - ) - message = generation.message - try: - func_call = copy.deepcopy(message.additional_kwargs["function_call"]) - except KeyError as exc: - raise OutputParserException(f"Could not parse function call: {exc}") - - if self.args_only: - return func_call["arguments"] - return func_call - - -class JsonOutputFunctionsParser(BaseCumulativeTransformOutputParser[Any]): - """Parse an output as the Json object.""" - - strict: bool = False - """Whether to allow non-JSON-compliant strings. - - See: https://docs.python.org/3/library/json.html#encoders-and-decoders - - Useful when the parsed output may include unicode characters or new lines. - """ - - args_only: bool = True - """Whether to only return the arguments to the function call.""" - - @property - def _type(self) -> str: - return "json_functions" - - def _diff(self, prev: Optional[Any], next: Any) -> Any: - return jsonpatch.make_patch(prev, next).patch - - def parse_result(self, result: List[Generation], *, partial: bool = False) -> Any: - if len(result) != 1: - raise OutputParserException( - f"Expected exactly one result, but got {len(result)}" - ) - generation = result[0] - if not isinstance(generation, ChatGeneration): - raise OutputParserException( - "This output parser can only be used with a chat generation." - ) - message = generation.message - if "function_call" not in message.additional_kwargs: - return None - try: - function_call = message.additional_kwargs["function_call"] - except KeyError as exc: - if partial: - return None - else: - raise OutputParserException(f"Could not parse function call: {exc}") - try: - if partial: - if self.args_only: - return parse_partial_json( - function_call["arguments"], strict=self.strict - ) - else: - return { - **function_call, - "arguments": parse_partial_json( - function_call["arguments"], strict=self.strict - ), - } - else: - if self.args_only: - try: - return json.loads( - function_call["arguments"], strict=self.strict - ) - except (json.JSONDecodeError, TypeError) as exc: - raise OutputParserException( - f"Could not parse function call data: {exc}" - ) - else: - try: - return { - **function_call, - "arguments": json.loads( - function_call["arguments"], strict=self.strict - ), - } - except (json.JSONDecodeError, TypeError) as exc: - raise OutputParserException( - f"Could not parse function call data: {exc}" - ) - except KeyError: - return None - - # This method would be called by the default implementation of `parse_result` - # but we're overriding that method so it's not needed. - def parse(self, text: str) -> Any: - raise NotImplementedError() - - -class JsonKeyOutputFunctionsParser(JsonOutputFunctionsParser): - """Parse an output as the element of the Json object.""" - - key_name: str - """The name of the key to return.""" - - def parse_result(self, result: List[Generation], *, partial: bool = False) -> Any: - res = super().parse_result(result, partial=partial) - if partial and res is None: - return None - return res.get(self.key_name) if partial else res[self.key_name] - - -class PydanticOutputFunctionsParser(OutputFunctionsParser): - """Parse an output as a pydantic object.""" - - pydantic_schema: Union[Type[BaseModel], Dict[str, Type[BaseModel]]] - """The pydantic schema to parse the output with.""" - - @root_validator(pre=True) - def validate_schema(cls, values: Dict) -> Dict: - schema = values["pydantic_schema"] - if "args_only" not in values: - values["args_only"] = isinstance(schema, type) and issubclass( - schema, BaseModel - ) - elif values["args_only"] and isinstance(schema, Dict): - raise ValueError( - "If multiple pydantic schemas are provided then args_only should be" - " False." - ) - return values - - def parse_result(self, result: List[Generation], *, partial: bool = False) -> Any: - _result = super().parse_result(result) - if self.args_only: - pydantic_args = self.pydantic_schema.parse_raw(_result) # type: ignore - else: - fn_name = _result["name"] - _args = _result["arguments"] - pydantic_args = self.pydantic_schema[fn_name].parse_raw(_args) # type: ignore # noqa: E501 - return pydantic_args - - -class PydanticAttrOutputFunctionsParser(PydanticOutputFunctionsParser): - """Parse an output as an attribute of a pydantic object.""" - - attr_name: str - """The name of the attribute to return.""" - - def parse_result(self, result: List[Generation], *, partial: bool = False) -> Any: - result = super().parse_result(result) - return getattr(result, self.attr_name) +__all__ = [ + "JsonKeyOutputFunctionsParser", + "JsonOutputFunctionsParser", + "OutputFunctionsParser", + "PydanticAttrOutputFunctionsParser", + "PydanticOutputFunctionsParser", +] diff --git a/libs/langchain/langchain/output_parsers/openai_functions.py b/libs/langchain/langchain/output_parsers/openai_functions.py index 5f52d4e6b6f71..062df8ba967aa 100644 --- a/libs/langchain/langchain/output_parsers/openai_functions.py +++ b/libs/langchain/langchain/output_parsers/openai_functions.py @@ -8,11 +8,10 @@ BaseCumulativeTransformOutputParser, BaseGenerationOutputParser, ) +from langchain_core.output_parsers.json import parse_partial_json from langchain_core.outputs import ChatGeneration, Generation from langchain_core.pydantic_v1 import BaseModel, root_validator -from langchain.output_parsers.json import parse_partial_json - class OutputFunctionsParser(BaseGenerationOutputParser[Any]): """Parse an output that is one of sets of values.""" @@ -137,10 +136,52 @@ def parse_result(self, result: List[Generation], *, partial: bool = False) -> An class PydanticOutputFunctionsParser(OutputFunctionsParser): - """Parse an output as a pydantic object.""" + """Parse an output as a pydantic object. + + This parser is used to parse the output of a ChatModel that uses + OpenAI function format to invoke functions. + + The parser extracts the function call invocation and matches + them to the pydantic schema provided. + + An exception will be raised if the function call does not match + the provided schema. + + Example: + + ... code-block:: python + + message = AIMessage( + content="This is a test message", + additional_kwargs={ + "function_call": { + "name": "cookie", + "arguments": json.dumps({"name": "value", "age": 10}), + } + }, + ) + chat_generation = ChatGeneration(message=message) + + class Cookie(BaseModel): + name: str + age: int + + class Dog(BaseModel): + species: str + + # Full output + parser = PydanticOutputFunctionsParser( + pydantic_schema={"cookie": Cookie, "dog": Dog} + ) + result = parser.parse_result([chat_generation]) + """ pydantic_schema: Union[Type[BaseModel], Dict[str, Type[BaseModel]]] - """The pydantic schema to parse the output with.""" + """The pydantic schema to parse the output with. + + If multiple schemas are provided, then the function name will be used to + determine which schema to use. + """ @root_validator(pre=True) def validate_schema(cls, values: Dict) -> Dict: diff --git a/libs/langchain/langchain/output_parsers/openai_tools.py b/libs/langchain/langchain/output_parsers/openai_tools.py index 3c8127c8fb123..bea4b12a3c2bf 100644 --- a/libs/langchain/langchain/output_parsers/openai_tools.py +++ b/libs/langchain/langchain/output_parsers/openai_tools.py @@ -1,5 +1,6 @@ import copy import json +from json import JSONDecodeError from typing import Any, List, Type from langchain_core.exceptions import OutputParserException @@ -13,6 +14,16 @@ class JsonOutputToolsParser(BaseGenerationOutputParser[Any]): """Parse tools from OpenAI response.""" + strict: bool = False + """Whether to allow non-JSON-compliant strings. + + See: https://docs.python.org/3/library/json.html#encoders-and-decoders + + Useful when the parsed output may include unicode characters or new lines. + """ + return_id: bool = False + """Whether to return the tool call id.""" + def parse_result(self, result: List[Generation], *, partial: bool = False) -> Any: generation = result[0] if not isinstance(generation, ChatGeneration): @@ -26,16 +37,30 @@ def parse_result(self, result: List[Generation], *, partial: bool = False) -> An return [] final_tools = [] + exceptions = [] for tool_call in tool_calls: if "function" not in tool_call: - pass - function_args = tool_call["function"]["arguments"] - final_tools.append( - { - "type": tool_call["function"]["name"], - "args": json.loads(function_args), - } - ) + continue + try: + function_args = json.loads( + tool_call["function"]["arguments"], strict=self.strict + ) + except JSONDecodeError as e: + exceptions.append( + f"Function {tool_call['function']['name']} arguments:\n\n" + f"{tool_call['function']['arguments']}\n\nare not valid JSON. " + f"Received JSONDecodeError {e}" + ) + continue + parsed = { + "type": tool_call["function"]["name"], + "args": function_args, + } + if self.return_id: + parsed["id"] = tool_call["id"] + final_tools.append(parsed) + if exceptions: + raise OutputParserException("\n\n".join(exceptions)) return final_tools @@ -44,10 +69,17 @@ class JsonOutputKeyToolsParser(JsonOutputToolsParser): key_name: str """The type of tools to return.""" + return_single: bool = False + """Whether to return only the first tool call.""" def parse_result(self, result: List[Generation], *, partial: bool = False) -> Any: results = super().parse_result(result) - return [res["args"] for res in results if results["type"] == self.key_name] + results = [res for res in results if res["type"] == self.key_name] + if not self.return_id: + results = [res["args"] for res in results] + if self.return_single: + return results[0] if results else None + return results class PydanticToolsParser(JsonOutputToolsParser): diff --git a/libs/langchain/langchain/output_parsers/pandas_dataframe.py b/libs/langchain/langchain/output_parsers/pandas_dataframe.py index 85bde591026da..4c0cb177d02a6 100644 --- a/libs/langchain/langchain/output_parsers/pandas_dataframe.py +++ b/libs/langchain/langchain/output_parsers/pandas_dataframe.py @@ -1,11 +1,13 @@ import re from typing import Any, Dict, List, Tuple, Union +from langchain_core.exceptions import OutputParserException +from langchain_core.output_parsers.base import BaseOutputParser +from langchain_core.pydantic_v1 import validator + from langchain.output_parsers.format_instructions import ( PANDAS_DATAFRAME_FORMAT_INSTRUCTIONS, ) -from langchain.pydantic_v1 import validator -from langchain.schema import BaseOutputParser, OutputParserException class PandasDataFrameOutputParser(BaseOutputParser): diff --git a/libs/langchain/langchain/output_parsers/rail_parser.py b/libs/langchain/langchain/output_parsers/rail_parser.py index f0cabc13eb553..3f796938be162 100644 --- a/libs/langchain/langchain/output_parsers/rail_parser.py +++ b/libs/langchain/langchain/output_parsers/rail_parser.py @@ -1,109 +1,5 @@ -from __future__ import annotations +from langchain_community.output_parsers.rail_parser import ( + GuardrailsOutputParser, +) -from typing import Any, Callable, Dict, Optional - -from langchain_core.output_parsers import BaseOutputParser - - -class GuardrailsOutputParser(BaseOutputParser): - """Parse the output of an LLM call using Guardrails.""" - - guard: Any - """The Guardrails object.""" - api: Optional[Callable] - """The LLM API passed to Guardrails during parsing. An example is `openai.completions.create`.""" # noqa: E501 - args: Any - """Positional arguments to pass to the above LLM API callable.""" - kwargs: Any - """Keyword arguments to pass to the above LLM API callable.""" - - @property - def _type(self) -> str: - return "guardrails" - - @classmethod - def from_rail( - cls, - rail_file: str, - num_reasks: int = 1, - api: Optional[Callable] = None, - *args: Any, - **kwargs: Any, - ) -> GuardrailsOutputParser: - """Create a GuardrailsOutputParser from a rail file. - - Args: - rail_file: a rail file. - num_reasks: number of times to re-ask the question. - api: the API to use for the Guardrails object. - *args: The arguments to pass to the API - **kwargs: The keyword arguments to pass to the API. - - Returns: - GuardrailsOutputParser - """ - try: - from guardrails import Guard - except ImportError: - raise ImportError( - "guardrails-ai package not installed. " - "Install it by running `pip install guardrails-ai`." - ) - return cls( - guard=Guard.from_rail(rail_file, num_reasks=num_reasks), - api=api, - args=args, - kwargs=kwargs, - ) - - @classmethod - def from_rail_string( - cls, - rail_str: str, - num_reasks: int = 1, - api: Optional[Callable] = None, - *args: Any, - **kwargs: Any, - ) -> GuardrailsOutputParser: - try: - from guardrails import Guard - except ImportError: - raise ImportError( - "guardrails-ai package not installed. " - "Install it by running `pip install guardrails-ai`." - ) - return cls( - guard=Guard.from_rail_string(rail_str, num_reasks=num_reasks), - api=api, - args=args, - kwargs=kwargs, - ) - - @classmethod - def from_pydantic( - cls, - output_class: Any, - num_reasks: int = 1, - api: Optional[Callable] = None, - *args: Any, - **kwargs: Any, - ) -> GuardrailsOutputParser: - try: - from guardrails import Guard - except ImportError: - raise ImportError( - "guardrails-ai package not installed. " - "Install it by running `pip install guardrails-ai`." - ) - return cls( - guard=Guard.from_pydantic(output_class, "", num_reasks=num_reasks), - api=api, - args=args, - kwargs=kwargs, - ) - - def get_format_instructions(self) -> str: - return self.guard.raw_prompt.format_instructions - - def parse(self, text: str) -> Dict: - return self.guard.parse(text, llm_api=self.api, *self.args, **self.kwargs) +__all__ = ["GuardrailsOutputParser"] diff --git a/libs/langchain/langchain/output_parsers/structured.py b/libs/langchain/langchain/output_parsers/structured.py index 1d1b61128fc32..1d99363bc1706 100644 --- a/libs/langchain/langchain/output_parsers/structured.py +++ b/libs/langchain/langchain/output_parsers/structured.py @@ -3,13 +3,13 @@ from typing import Any, List from langchain_core.output_parsers import BaseOutputParser +from langchain_core.output_parsers.json import parse_and_check_json_markdown from langchain_core.pydantic_v1 import BaseModel from langchain.output_parsers.format_instructions import ( STRUCTURED_FORMAT_INSTRUCTIONS, STRUCTURED_FORMAT_SIMPLE_INSTRUCTIONS, ) -from langchain.output_parsers.json import parse_and_check_json_markdown line_template = '\t"{name}": {type} // {description}' diff --git a/libs/langchain/langchain/retrievers/contextual_compression.py b/libs/langchain/langchain/retrievers/contextual_compression.py index d06a9b690838d..b41a82a2b419b 100644 --- a/libs/langchain/langchain/retrievers/contextual_compression.py +++ b/libs/langchain/langchain/retrievers/contextual_compression.py @@ -1,12 +1,12 @@ from typing import Any, List -from langchain_core.documents import Document -from langchain_core.retrievers import BaseRetriever - -from langchain.callbacks.manager import ( +from langchain_core.callbacks import ( AsyncCallbackManagerForRetrieverRun, CallbackManagerForRetrieverRun, ) +from langchain_core.documents import Document +from langchain_core.retrievers import BaseRetriever + from langchain.retrievers.document_compressors.base import ( BaseDocumentCompressor, ) diff --git a/libs/langchain/langchain/retrievers/ensemble.py b/libs/langchain/langchain/retrievers/ensemble.py index 324ede8ae279b..050cb88fc4c4e 100644 --- a/libs/langchain/langchain/retrievers/ensemble.py +++ b/libs/langchain/langchain/retrievers/ensemble.py @@ -5,6 +5,10 @@ import asyncio from typing import Any, Dict, List, Optional +from langchain_core.callbacks import ( + AsyncCallbackManagerForRetrieverRun, + CallbackManagerForRetrieverRun, +) from langchain_core.documents import Document from langchain_core.load.dump import dumpd from langchain_core.pydantic_v1 import root_validator @@ -16,11 +20,6 @@ get_unique_config_specs, ) -from langchain.callbacks.manager import ( - AsyncCallbackManagerForRetrieverRun, - CallbackManagerForRetrieverRun, -) - class EnsembleRetriever(BaseRetriever): """Retriever that ensembles the multiple retrievers. @@ -57,7 +56,7 @@ def set_weights(cls, values: Dict[str, Any]) -> Dict[str, Any]: def invoke( self, input: str, config: Optional[RunnableConfig] = None, **kwargs: Any ) -> List[Document]: - from langchain_core.callbacks.manager import CallbackManager + from langchain_core.callbacks import CallbackManager config = ensure_config(config) callback_manager = CallbackManager.configure( @@ -90,7 +89,7 @@ def invoke( async def ainvoke( self, input: str, config: Optional[RunnableConfig] = None, **kwargs: Any ) -> List[Document]: - from langchain_core.callbacks.manager import AsyncCallbackManager + from langchain_core.callbacks import AsyncCallbackManager config = ensure_config(config) callback_manager = AsyncCallbackManager.configure( diff --git a/libs/langchain/langchain/retrievers/merger_retriever.py b/libs/langchain/langchain/retrievers/merger_retriever.py index ab3124a6a7b4c..f5326773bcba1 100644 --- a/libs/langchain/langchain/retrievers/merger_retriever.py +++ b/libs/langchain/langchain/retrievers/merger_retriever.py @@ -1,13 +1,12 @@ import asyncio from typing import List -from langchain_core.documents import Document -from langchain_core.retrievers import BaseRetriever - -from langchain.callbacks.manager import ( +from langchain_core.callbacks import ( AsyncCallbackManagerForRetrieverRun, CallbackManagerForRetrieverRun, ) +from langchain_core.documents import Document +from langchain_core.retrievers import BaseRetriever class MergerRetriever(BaseRetriever): diff --git a/libs/langchain/langchain/retrievers/multi_query.py b/libs/langchain/langchain/retrievers/multi_query.py index e3160bb752a0c..7d60b2215140b 100644 --- a/libs/langchain/langchain/retrievers/multi_query.py +++ b/libs/langchain/langchain/retrievers/multi_query.py @@ -2,16 +2,16 @@ import logging from typing import List, Sequence +from langchain_core.callbacks import ( + AsyncCallbackManagerForRetrieverRun, + CallbackManagerForRetrieverRun, +) from langchain_core.documents import Document from langchain_core.language_models import BaseLLM from langchain_core.prompts.prompt import PromptTemplate from langchain_core.pydantic_v1 import BaseModel, Field from langchain_core.retrievers import BaseRetriever -from langchain.callbacks.manager import ( - AsyncCallbackManagerForRetrieverRun, - CallbackManagerForRetrieverRun, -) from langchain.chains.llm import LLMChain from langchain.output_parsers.pydantic import PydanticOutputParser diff --git a/libs/langchain/langchain/retrievers/multi_vector.py b/libs/langchain/langchain/retrievers/multi_vector.py index bfb21af80ce82..167fc5d4cb81c 100644 --- a/libs/langchain/langchain/retrievers/multi_vector.py +++ b/libs/langchain/langchain/retrievers/multi_vector.py @@ -1,13 +1,13 @@ from enum import Enum from typing import Dict, List, Optional +from langchain_core.callbacks import CallbackManagerForRetrieverRun from langchain_core.documents import Document from langchain_core.pydantic_v1 import Field, root_validator from langchain_core.retrievers import BaseRetriever from langchain_core.stores import BaseStore, ByteStore from langchain_core.vectorstores import VectorStore -from langchain.callbacks.manager import CallbackManagerForRetrieverRun from langchain.storage._lc_store import create_kv_docstore diff --git a/libs/langchain/langchain/retrievers/re_phraser.py b/libs/langchain/langchain/retrievers/re_phraser.py index e4611b202f415..2ffdd45e3eea9 100644 --- a/libs/langchain/langchain/retrievers/re_phraser.py +++ b/libs/langchain/langchain/retrievers/re_phraser.py @@ -1,15 +1,15 @@ import logging from typing import List +from langchain_core.callbacks import ( + AsyncCallbackManagerForRetrieverRun, + CallbackManagerForRetrieverRun, +) from langchain_core.documents import Document from langchain_core.language_models import BaseLLM from langchain_core.prompts.prompt import PromptTemplate from langchain_core.retrievers import BaseRetriever -from langchain.callbacks.manager import ( - AsyncCallbackManagerForRetrieverRun, - CallbackManagerForRetrieverRun, -) from langchain.chains.llm import LLMChain logger = logging.getLogger(__name__) diff --git a/libs/langchain/langchain/retrievers/self_query/astradb.py b/libs/langchain/langchain/retrievers/self_query/astradb.py new file mode 100644 index 0000000000000..0b8d3ab800f7c --- /dev/null +++ b/libs/langchain/langchain/retrievers/self_query/astradb.py @@ -0,0 +1,70 @@ +"""Logic for converting internal query language to a valid AstraDB query.""" +from typing import Dict, Tuple, Union + +from langchain.chains.query_constructor.ir import ( + Comparator, + Comparison, + Operation, + Operator, + StructuredQuery, + Visitor, +) + +MULTIPLE_ARITY_COMPARATORS = [Comparator.IN, Comparator.NIN] + + +class AstraDBTranslator(Visitor): + """Translate AstraDB internal query language elements to valid filters.""" + + """Subset of allowed logical comparators.""" + allowed_comparators = [ + Comparator.EQ, + Comparator.NE, + Comparator.GT, + Comparator.GTE, + Comparator.LT, + Comparator.LTE, + Comparator.IN, + Comparator.NIN, + ] + + """Subset of allowed logical operators.""" + allowed_operators = [Operator.AND, Operator.OR] + + def _format_func(self, func: Union[Operator, Comparator]) -> str: + self._validate_func(func) + map_dict = { + Operator.AND: "$and", + Operator.OR: "$or", + Comparator.EQ: "$eq", + Comparator.NE: "$ne", + Comparator.GTE: "$gte", + Comparator.LTE: "$lte", + Comparator.LT: "$lt", + Comparator.GT: "$gt", + Comparator.IN: "$in", + Comparator.NIN: "$nin", + } + return map_dict[func] + + def visit_operation(self, operation: Operation) -> Dict: + args = [arg.accept(self) for arg in operation.arguments] + return {self._format_func(operation.operator): args} + + def visit_comparison(self, comparison: Comparison) -> Dict: + if comparison.comparator in MULTIPLE_ARITY_COMPARATORS and not isinstance( + comparison.value, list + ): + comparison.value = [comparison.value] + + comparator = self._format_func(comparison.comparator) + return {comparison.attribute: {comparator: comparison.value}} + + def visit_structured_query( + self, structured_query: StructuredQuery + ) -> Tuple[str, dict]: + if structured_query.filter is None: + kwargs = {} + else: + kwargs = {"filter": structured_query.filter.accept(self)} + return structured_query.query, kwargs diff --git a/libs/langchain/langchain/retrievers/self_query/base.py b/libs/langchain/langchain/retrievers/self_query/base.py index 4a033580b4fff..9c6a584a6e174 100644 --- a/libs/langchain/langchain/retrievers/self_query/base.py +++ b/libs/langchain/langchain/retrievers/self_query/base.py @@ -3,6 +3,7 @@ from typing import Any, Dict, List, Optional, Sequence, Tuple, Type, Union from langchain_community.vectorstores import ( + AstraDB, Chroma, DashVector, DeepLake, @@ -33,6 +34,7 @@ from langchain.chains.query_constructor.base import load_query_constructor_runnable from langchain.chains.query_constructor.ir import StructuredQuery, Visitor from langchain.chains.query_constructor.schema import AttributeInfo +from langchain.retrievers.self_query.astradb import AstraDBTranslator from langchain.retrievers.self_query.chroma import ChromaTranslator from langchain.retrievers.self_query.dashvector import DashvectorTranslator from langchain.retrievers.self_query.deeplake import DeepLakeTranslator @@ -55,6 +57,7 @@ def _get_builtin_translator(vectorstore: VectorStore) -> Visitor: """Get the translator class corresponding to the vector store class.""" BUILTIN_TRANSLATORS: Dict[Type[VectorStore], Type[Visitor]] = { + AstraDB: AstraDBTranslator, Pinecone: PineconeTranslator, Chroma: ChromaTranslator, DashVector: DashvectorTranslator, diff --git a/libs/langchain/langchain/retrievers/self_query/milvus.py b/libs/langchain/langchain/retrievers/self_query/milvus.py index f855bf9909398..dbc61f6f71203 100644 --- a/libs/langchain/langchain/retrievers/self_query/milvus.py +++ b/libs/langchain/langchain/retrievers/self_query/milvus.py @@ -16,28 +16,36 @@ Comparator.GTE: ">=", Comparator.LT: "<", Comparator.LTE: "<=", + Comparator.IN: "in", + Comparator.LIKE: "like", } UNARY_OPERATORS = [Operator.NOT] -def process_value(value: Union[int, float, str]) -> str: +def process_value(value: Union[int, float, str], comparator: Comparator) -> str: """Convert a value to a string and add double quotes if it is a string. It required for comparators involving strings. Args: value: The value to convert. + comparator: The comparator. Returns: The converted value as a string. """ # if isinstance(value, str): - # If the value is already a string, add double quotes - return f'"{value}"' + if comparator is Comparator.LIKE: + # If the comparator is LIKE, add a percent sign after it for prefix matching + # and add double quotes + return f'"{value}%"' + else: + # If the value is already a string, add double quotes + return f'"{value}"' else: - # If the valueis not a string, convert it to a string without double quotes + # If the value is not a string, convert it to a string without double quotes return str(value) @@ -54,6 +62,8 @@ class MilvusTranslator(Visitor): Comparator.GTE, Comparator.LT, Comparator.LTE, + Comparator.IN, + Comparator.LIKE, ] def _format_func(self, func: Union[Operator, Comparator]) -> str: @@ -78,7 +88,7 @@ def visit_operation(self, operation: Operation) -> str: def visit_comparison(self, comparison: Comparison) -> str: comparator = self._format_func(comparison.comparator) - processed_value = process_value(comparison.value) + processed_value = process_value(comparison.value, comparison.comparator) attribute = comparison.attribute return "( " + attribute + " " + comparator + " " + processed_value + " )" diff --git a/libs/langchain/langchain/retrievers/self_query/opensearch.py b/libs/langchain/langchain/retrievers/self_query/opensearch.py index 502ee8a872b1f..bb27cddd0b481 100644 --- a/libs/langchain/langchain/retrievers/self_query/opensearch.py +++ b/libs/langchain/langchain/retrievers/self_query/opensearch.py @@ -58,11 +58,25 @@ def visit_comparison(self, comparison: Comparison) -> Dict: Comparator.GT, Comparator.GTE, ]: - return { - "range": { - field: {self._format_func(comparison.comparator): comparison.value} + if isinstance(comparison.value, dict): + if "date" in comparison.value: + return { + "range": { + field: { + self._format_func( + comparison.comparator + ): comparison.value["date"] + } + } + } + else: + return { + "range": { + field: { + self._format_func(comparison.comparator): comparison.value + } + } } - } if comparison.comparator == Comparator.LIKE: return { @@ -70,8 +84,13 @@ def visit_comparison(self, comparison: Comparison) -> Dict: field: {"value": comparison.value} } } + field = f"{field}.keyword" if isinstance(comparison.value, str) else field + if isinstance(comparison.value, dict): + if "date" in comparison.value: + comparison.value = comparison.value["date"] + return {self._format_func(comparison.comparator): {field: comparison.value}} def visit_structured_query( @@ -81,4 +100,5 @@ def visit_structured_query( kwargs = {} else: kwargs = {"filter": structured_query.filter.accept(self)} + return structured_query.query, kwargs diff --git a/libs/langchain/langchain/retrievers/time_weighted_retriever.py b/libs/langchain/langchain/retrievers/time_weighted_retriever.py index 273839984d8b0..33d553a3cae70 100644 --- a/libs/langchain/langchain/retrievers/time_weighted_retriever.py +++ b/libs/langchain/langchain/retrievers/time_weighted_retriever.py @@ -2,13 +2,12 @@ from copy import deepcopy from typing import Any, Dict, List, Optional, Tuple +from langchain_core.callbacks import CallbackManagerForRetrieverRun from langchain_core.documents import Document from langchain_core.pydantic_v1 import Field from langchain_core.retrievers import BaseRetriever from langchain_core.vectorstores import VectorStore -from langchain.callbacks.manager import CallbackManagerForRetrieverRun - def _get_hours_passed(time: datetime.datetime, ref_time: datetime.datetime) -> float: """Get the hours passed between two datetimes.""" diff --git a/libs/langchain/langchain/retrievers/web_research.py b/libs/langchain/langchain/retrievers/web_research.py index 92d790cc85861..149ef247af730 100644 --- a/libs/langchain/langchain/retrievers/web_research.py +++ b/libs/langchain/langchain/retrievers/web_research.py @@ -6,6 +6,10 @@ from langchain_community.document_transformers import Html2TextTransformer from langchain_community.llms import LlamaCpp from langchain_community.utilities import GoogleSearchAPIWrapper +from langchain_core.callbacks import ( + AsyncCallbackManagerForRetrieverRun, + CallbackManagerForRetrieverRun, +) from langchain_core.documents import Document from langchain_core.language_models import BaseLLM from langchain_core.prompts import BasePromptTemplate, PromptTemplate @@ -13,10 +17,6 @@ from langchain_core.retrievers import BaseRetriever from langchain_core.vectorstores import VectorStore -from langchain.callbacks.manager import ( - AsyncCallbackManagerForRetrieverRun, - CallbackManagerForRetrieverRun, -) from langchain.chains import LLMChain from langchain.chains.prompt_selector import ConditionalPromptSelector from langchain.output_parsers.pydantic import PydanticOutputParser diff --git a/libs/langchain/langchain/smith/evaluation/runner_utils.py b/libs/langchain/langchain/smith/evaluation/runner_utils.py index 6a98e4a73e14e..ce50c1500af58 100644 --- a/libs/langchain/langchain/smith/evaluation/runner_utils.py +++ b/libs/langchain/langchain/smith/evaluation/runner_utils.py @@ -34,7 +34,7 @@ ) from langchain_core.tracers.langchain import LangChainTracer from langsmith.client import Client -from langsmith.env import get_git_info +from langsmith.env import get_git_info, get_langchain_env_var_metadata from langsmith.evaluation import EvaluationResult, RunEvaluator from langsmith.run_helpers import as_runnable, is_traceable_function from langsmith.schemas import Dataset, DataType, Example, TracerSession @@ -97,6 +97,7 @@ def get_aggregate_feedback( for col in df.columns if col.startswith("inputs.") or col.startswith("outputs.") + or col in {"input", "output"} or col.startswith("reference") ] return df.describe(include="all").drop(to_drop, axis=1) @@ -656,6 +657,7 @@ async def _arun_llm( tags: Optional[List[str]] = None, callbacks: Callbacks = None, input_mapper: Optional[Callable[[Dict], Any]] = None, + metadata: Optional[Dict[str, Any]] = None, ) -> Union[str, BaseMessage]: """Asynchronously run the language model. @@ -681,7 +683,9 @@ async def _arun_llm( ): return await llm.ainvoke( prompt_or_messages, - config=RunnableConfig(callbacks=callbacks, tags=tags or []), + config=RunnableConfig( + callbacks=callbacks, tags=tags or [], metadata=metadata or {} + ), ) else: raise InputFormatError( @@ -694,12 +698,18 @@ async def _arun_llm( try: prompt = _get_prompt(inputs) llm_output: Union[str, BaseMessage] = await llm.ainvoke( - prompt, config=RunnableConfig(callbacks=callbacks, tags=tags or []) + prompt, + config=RunnableConfig( + callbacks=callbacks, tags=tags or [], metadata=metadata or {} + ), ) except InputFormatError: messages = _get_messages(inputs) llm_output = await llm.ainvoke( - messages, config=RunnableConfig(callbacks=callbacks, tags=tags or []) + messages, + config=RunnableConfig( + callbacks=callbacks, tags=tags or [], metadata=metadata or {} + ), ) return llm_output @@ -711,6 +721,7 @@ async def _arun_chain( *, tags: Optional[List[str]] = None, input_mapper: Optional[Callable[[Dict], Any]] = None, + metadata: Optional[Dict[str, Any]] = None, ) -> Union[dict, str]: """Run a chain asynchronously on inputs.""" inputs_ = inputs if input_mapper is None else input_mapper(inputs) @@ -722,10 +733,15 @@ async def _arun_chain( ): val = next(iter(inputs_.values())) output = await chain.ainvoke( - val, config=RunnableConfig(callbacks=callbacks, tags=tags or []) + val, + config=RunnableConfig( + callbacks=callbacks, tags=tags or [], metadata=metadata or {} + ), ) else: - runnable_config = RunnableConfig(tags=tags or [], callbacks=callbacks) + runnable_config = RunnableConfig( + tags=tags or [], callbacks=callbacks, metadata=metadata or {} + ) output = await chain.ainvoke(inputs_, config=runnable_config) return output @@ -761,6 +777,7 @@ async def _arun_llm_or_chain( tags=config["tags"], callbacks=config["callbacks"], input_mapper=input_mapper, + metadata=config.get("metadata"), ) else: chain = llm_or_chain_factory() @@ -770,6 +787,7 @@ async def _arun_llm_or_chain( tags=config["tags"], callbacks=config["callbacks"], input_mapper=input_mapper, + metadata=config.get("metadata"), ) result = output except Exception as e: @@ -792,6 +810,7 @@ def _run_llm( *, tags: Optional[List[str]] = None, input_mapper: Optional[Callable[[Dict], Any]] = None, + metadata: Optional[Dict[str, Any]] = None, ) -> Union[str, BaseMessage]: """ Run the language model on the example. @@ -818,7 +837,9 @@ def _run_llm( ): llm_output: Union[str, BaseMessage] = llm.invoke( prompt_or_messages, - config=RunnableConfig(callbacks=callbacks, tags=tags or []), + config=RunnableConfig( + callbacks=callbacks, tags=tags or [], metadata=metadata or {} + ), ) else: raise InputFormatError( @@ -830,12 +851,16 @@ def _run_llm( try: llm_prompts = _get_prompt(inputs) llm_output = llm.invoke( - llm_prompts, config=RunnableConfig(callbacks=callbacks, tags=tags or []) + llm_prompts, + config=RunnableConfig( + callbacks=callbacks, tags=tags or [], metadata=metadata or {} + ), ) except InputFormatError: llm_messages = _get_messages(inputs) llm_output = llm.invoke( - llm_messages, config=RunnableConfig(callbacks=callbacks) + llm_messages, + config=RunnableConfig(callbacks=callbacks, metadata=metadata or {}), ) return llm_output @@ -847,6 +872,7 @@ def _run_chain( *, tags: Optional[List[str]] = None, input_mapper: Optional[Callable[[Dict], Any]] = None, + metadata: Optional[Dict[str, Any]] = None, ) -> Union[Dict, str]: """Run a chain on inputs.""" inputs_ = inputs if input_mapper is None else input_mapper(inputs) @@ -858,10 +884,15 @@ def _run_chain( ): val = next(iter(inputs_.values())) output = chain.invoke( - val, config=RunnableConfig(callbacks=callbacks, tags=tags or []) + val, + config=RunnableConfig( + callbacks=callbacks, tags=tags or [], metadata=metadata or {} + ), ) else: - runnable_config = RunnableConfig(tags=tags or [], callbacks=callbacks) + runnable_config = RunnableConfig( + tags=tags or [], callbacks=callbacks, metadata=metadata or {} + ) output = chain.invoke(inputs_, config=runnable_config) return output @@ -898,6 +929,7 @@ def _run_llm_or_chain( config["callbacks"], tags=config["tags"], input_mapper=input_mapper, + metadata=config.get("metadata"), ) else: chain = llm_or_chain_factory() @@ -907,6 +939,7 @@ def _run_llm_or_chain( config["callbacks"], tags=config["tags"], input_mapper=input_mapper, + metadata=config.get("metadata"), ) result = output except Exception as e: @@ -1082,8 +1115,13 @@ def prepare( input_mapper: Optional[Callable[[Dict], Any]] = None, concurrency_level: int = 5, project_metadata: Optional[Dict[str, Any]] = None, + revision_id: Optional[str] = None, ) -> _DatasetRunContainer: project_name = project_name or name_generation.random_name() + if revision_id: + if not project_metadata: + project_metadata = {} + project_metadata.update({"revision_id": revision_id}) wrapped_model, project, dataset, examples = _prepare_eval_run( client, dataset_name, @@ -1120,6 +1158,7 @@ def prepare( ], tags=tags, max_concurrency=concurrency_level, + metadata={"revision_id": revision_id} if revision_id else {}, ) for example in examples ] @@ -1182,11 +1221,14 @@ async def arun_on_dataset( project_metadata: Optional[Dict[str, Any]] = None, verbose: bool = False, tags: Optional[List[str]] = None, + revision_id: Optional[str] = None, **kwargs: Any, ) -> Dict[str, Any]: input_mapper = kwargs.pop("input_mapper", None) if input_mapper: warn_deprecated("0.0.305", message=_INPUT_MAPPER_DEP_WARNING, pending=True) + if revision_id is None: + revision_id = get_langchain_env_var_metadata().get("revision_id") if kwargs: warn_deprecated( @@ -1207,6 +1249,7 @@ async def arun_on_dataset( input_mapper, concurrency_level, project_metadata=project_metadata, + revision_id=revision_id, ) batch_results = await runnable_utils.gather_with_concurrency( container.configs[0].get("max_concurrency"), @@ -1234,11 +1277,14 @@ def run_on_dataset( project_metadata: Optional[Dict[str, Any]] = None, verbose: bool = False, tags: Optional[List[str]] = None, + revision_id: Optional[str] = None, **kwargs: Any, ) -> Dict[str, Any]: input_mapper = kwargs.pop("input_mapper", None) if input_mapper: warn_deprecated("0.0.305", message=_INPUT_MAPPER_DEP_WARNING, pending=True) + if revision_id is None: + revision_id = get_langchain_env_var_metadata().get("revision_id") if kwargs: warn_deprecated( @@ -1259,6 +1305,7 @@ def run_on_dataset( input_mapper, concurrency_level, project_metadata=project_metadata, + revision_id=revision_id, ) if concurrency_level == 0: batch_results = [ @@ -1308,6 +1355,8 @@ def run_on_dataset( log feedback and run traces. verbose: Whether to print progress. tags: Tags to add to each run in the project. + revision_id: Optional revision identifier to assign this test run to + track the performance of different versions of your system. Returns: A dictionary containing the run's project name and the resulting model outputs. diff --git a/libs/langchain/langchain/text_splitter.py b/libs/langchain/langchain/text_splitter.py index da65a80dc9fd7..c4ece25320455 100644 --- a/libs/langchain/langchain/text_splitter.py +++ b/libs/langchain/langchain/text_splitter.py @@ -598,7 +598,9 @@ def split_text_from_file(self, file: Any) -> List[Document]: "Unable to import lxml, please install with `pip install lxml`." ) from e # use lxml library to parse html document and return xml ElementTree - parser = etree.HTMLParser() + # Explicitly encoding in utf-8 allows non-English + # html files to be processed without garbled characters + parser = etree.HTMLParser(encoding="utf-8") tree = etree.parse(file, parser) # document transformation for "structure-aware" chunking is handled with xsl. @@ -1427,6 +1429,37 @@ def split_text(self, text: str) -> List[str]: return self._merge_splits(splits, self._separator) +class KonlpyTextSplitter(TextSplitter): + """Splitting text using Konlpy package. + + It is good for splitting Korean text. + """ + + def __init__( + self, + separator: str = "\n\n", + **kwargs: Any, + ) -> None: + """Initialize the Konlpy text splitter.""" + super().__init__(**kwargs) + self._separator = separator + try: + from konlpy.tag import Kkma + except ImportError: + raise ImportError( + """ + Konlpy is not installed, please install it with + `pip install konlpy` + """ + ) + self.kkma = Kkma() + + def split_text(self, text: str) -> List[str]: + """Split incoming text and return chunks.""" + splits = self.kkma.sentences(text) + return self._merge_splits(splits, self._separator) + + # For backwards compatibility class PythonCodeTextSplitter(RecursiveCharacterTextSplitter): """Attempts to split the text along Python syntax.""" diff --git a/libs/langchain/langchain/tools/retriever.py b/libs/langchain/langchain/tools/retriever.py index a4ed55e96b9b4..1d2cc7bc27248 100644 --- a/libs/langchain/langchain/tools/retriever.py +++ b/libs/langchain/langchain/tools/retriever.py @@ -1,3 +1,7 @@ +from functools import partial +from typing import Optional + +from langchain_core.prompts import BasePromptTemplate, PromptTemplate, format_document from langchain_core.pydantic_v1 import BaseModel, Field from langchain_core.retrievers import BaseRetriever @@ -10,8 +14,37 @@ class RetrieverInput(BaseModel): query: str = Field(description="query to look up in retriever") +def _get_relevant_documents( + query: str, + retriever: BaseRetriever, + document_prompt: BasePromptTemplate, + document_separator: str, +) -> str: + docs = retriever.get_relevant_documents(query) + return document_separator.join( + format_document(doc, document_prompt) for doc in docs + ) + + +async def _aget_relevant_documents( + query: str, + retriever: BaseRetriever, + document_prompt: BasePromptTemplate, + document_separator: str, +) -> str: + docs = await retriever.aget_relevant_documents(query) + return document_separator.join( + format_document(doc, document_prompt) for doc in docs + ) + + def create_retriever_tool( - retriever: BaseRetriever, name: str, description: str + retriever: BaseRetriever, + name: str, + description: str, + *, + document_prompt: Optional[BasePromptTemplate] = None, + document_separator: str = "\n\n", ) -> Tool: """Create a tool to do retrieval of documents. @@ -25,10 +58,23 @@ def create_retriever_tool( Returns: Tool class to pass to an agent """ + document_prompt = document_prompt or PromptTemplate.from_template("{page_content}") + func = partial( + _get_relevant_documents, + retriever=retriever, + document_prompt=document_prompt, + document_separator=document_separator, + ) + afunc = partial( + _aget_relevant_documents, + retriever=retriever, + document_prompt=document_prompt, + document_separator=document_separator, + ) return Tool( name=name, description=description, - func=retriever.get_relevant_documents, - coroutine=retriever.aget_relevant_documents, + func=func, + coroutine=afunc, args_schema=RetrieverInput, ) diff --git a/libs/langchain/poetry.lock b/libs/langchain/poetry.lock index 404601a5c164c..9d7210910ef35 100644 --- a/libs/langchain/poetry.lock +++ b/libs/langchain/poetry.lock @@ -3448,7 +3448,7 @@ files = [ [[package]] name = "langchain-community" -version = "0.0.9" +version = "0.0.14" description = "Community contributed LangChain integrations." optional = false python-versions = ">=3.8.1,<4.0" @@ -3458,8 +3458,8 @@ develop = true [package.dependencies] aiohttp = "^3.8.3" dataclasses-json = ">= 0.5.7, < 0.7" -langchain-core = ">=0.1.7,<0.2" -langsmith = "~0.0.63" +langchain-core = ">=0.1.14,<0.2" +langsmith = ">=0.0.83,<0.1" numpy = "^1" PyYAML = ">=5.3" requests = "^2" @@ -3468,7 +3468,7 @@ tenacity = "^8.1.0" [package.extras] cli = ["typer (>=0.9.0,<0.10.0)"] -extended-testing = ["aiosqlite (>=0.19.0,<0.20.0)", "aleph-alpha-client (>=2.15.0,<3.0.0)", "anthropic (>=0.3.11,<0.4.0)", "arxiv (>=1.4,<2.0)", "assemblyai (>=0.17.0,<0.18.0)", "atlassian-python-api (>=3.36.0,<4.0.0)", "azure-ai-documentintelligence (>=1.0.0b1,<2.0.0)", "beautifulsoup4 (>=4,<5)", "bibtexparser (>=1.4.0,<2.0.0)", "cassio (>=0.1.0,<0.2.0)", "chardet (>=5.1.0,<6.0.0)", "cohere (>=4,<5)", "dashvector (>=1.0.1,<2.0.0)", "databricks-vectorsearch (>=0.21,<0.22)", "datasets (>=2.15.0,<3.0.0)", "dgml-utils (>=0.3.0,<0.4.0)", "esprima (>=4.0.1,<5.0.0)", "faiss-cpu (>=1,<2)", "feedparser (>=6.0.10,<7.0.0)", "fireworks-ai (>=0.9.0,<0.10.0)", "geopandas (>=0.13.1,<0.14.0)", "gitpython (>=3.1.32,<4.0.0)", "google-cloud-documentai (>=2.20.1,<3.0.0)", "gql (>=3.4.1,<4.0.0)", "gradientai (>=1.4.0,<2.0.0)", "hologres-vector (>=0.0.6,<0.0.7)", "html2text (>=2020.1.16,<2021.0.0)", "javelin-sdk (>=0.1.8,<0.2.0)", "jinja2 (>=3,<4)", "jq (>=1.4.1,<2.0.0)", "jsonschema (>1)", "lxml (>=4.9.2,<5.0.0)", "markdownify (>=0.11.6,<0.12.0)", "motor (>=3.3.1,<4.0.0)", "msal (>=1.25.0,<2.0.0)", "mwparserfromhell (>=0.6.4,<0.7.0)", "mwxml (>=0.3.3,<0.4.0)", "newspaper3k (>=0.2.8,<0.3.0)", "numexpr (>=2.8.6,<3.0.0)", "openai (<2)", "openapi-pydantic (>=0.3.2,<0.4.0)", "oracle-ads (>=2.9.1,<3.0.0)", "pandas (>=2.0.1,<3.0.0)", "pdfminer-six (>=20221105,<20221106)", "pgvector (>=0.1.6,<0.2.0)", "praw (>=7.7.1,<8.0.0)", "psychicapi (>=0.8.0,<0.9.0)", "py-trello (>=0.19.0,<0.20.0)", "pymupdf (>=1.22.3,<2.0.0)", "pypdf (>=3.4.0,<4.0.0)", "pypdfium2 (>=4.10.0,<5.0.0)", "pyspark (>=3.4.0,<4.0.0)", "rank-bm25 (>=0.2.2,<0.3.0)", "rapidfuzz (>=3.1.1,<4.0.0)", "rapidocr-onnxruntime (>=1.3.2,<2.0.0)", "requests-toolbelt (>=1.0.0,<2.0.0)", "rspace_client (>=2.5.0,<3.0.0)", "scikit-learn (>=1.2.2,<2.0.0)", "sqlite-vss (>=0.1.2,<0.2.0)", "streamlit (>=1.18.0,<2.0.0)", "sympy (>=1.12,<2.0)", "telethon (>=1.28.5,<2.0.0)", "timescale-vector (>=0.0.1,<0.0.2)", "tqdm (>=4.48.0)", "upstash-redis (>=0.15.0,<0.16.0)", "xata (>=1.0.0a7,<2.0.0)", "xmltodict (>=0.13.0,<0.14.0)", "zhipuai (>=1.0.7,<2.0.0)"] +extended-testing = ["aiosqlite (>=0.19.0,<0.20.0)", "aleph-alpha-client (>=2.15.0,<3.0.0)", "anthropic (>=0.3.11,<0.4.0)", "arxiv (>=1.4,<2.0)", "assemblyai (>=0.17.0,<0.18.0)", "atlassian-python-api (>=3.36.0,<4.0.0)", "azure-ai-documentintelligence (>=1.0.0b1,<2.0.0)", "beautifulsoup4 (>=4,<5)", "bibtexparser (>=1.4.0,<2.0.0)", "cassio (>=0.1.0,<0.2.0)", "chardet (>=5.1.0,<6.0.0)", "cohere (>=4,<5)", "dashvector (>=1.0.1,<2.0.0)", "databricks-vectorsearch (>=0.21,<0.22)", "datasets (>=2.15.0,<3.0.0)", "dgml-utils (>=0.3.0,<0.4.0)", "elasticsearch (>=8.12.0,<9.0.0)", "esprima (>=4.0.1,<5.0.0)", "faiss-cpu (>=1,<2)", "feedparser (>=6.0.10,<7.0.0)", "fireworks-ai (>=0.9.0,<0.10.0)", "geopandas (>=0.13.1,<0.14.0)", "gitpython (>=3.1.32,<4.0.0)", "google-cloud-documentai (>=2.20.1,<3.0.0)", "gql (>=3.4.1,<4.0.0)", "gradientai (>=1.4.0,<2.0.0)", "hologres-vector (>=0.0.6,<0.0.7)", "html2text (>=2020.1.16,<2021.0.0)", "javelin-sdk (>=0.1.8,<0.2.0)", "jinja2 (>=3,<4)", "jq (>=1.4.1,<2.0.0)", "jsonschema (>1)", "lxml (>=4.9.2,<5.0.0)", "markdownify (>=0.11.6,<0.12.0)", "motor (>=3.3.1,<4.0.0)", "msal (>=1.25.0,<2.0.0)", "mwparserfromhell (>=0.6.4,<0.7.0)", "mwxml (>=0.3.3,<0.4.0)", "newspaper3k (>=0.2.8,<0.3.0)", "numexpr (>=2.8.6,<3.0.0)", "openai (<2)", "openapi-pydantic (>=0.3.2,<0.4.0)", "oracle-ads (>=2.9.1,<3.0.0)", "pandas (>=2.0.1,<3.0.0)", "pdfminer-six (>=20221105,<20221106)", "pgvector (>=0.1.6,<0.2.0)", "praw (>=7.7.1,<8.0.0)", "psychicapi (>=0.8.0,<0.9.0)", "py-trello (>=0.19.0,<0.20.0)", "pymupdf (>=1.22.3,<2.0.0)", "pypdf (>=3.4.0,<4.0.0)", "pypdfium2 (>=4.10.0,<5.0.0)", "pyspark (>=3.4.0,<4.0.0)", "rank-bm25 (>=0.2.2,<0.3.0)", "rapidfuzz (>=3.1.1,<4.0.0)", "rapidocr-onnxruntime (>=1.3.2,<2.0.0)", "requests-toolbelt (>=1.0.0,<2.0.0)", "rspace_client (>=2.5.0,<3.0.0)", "scikit-learn (>=1.2.2,<2.0.0)", "sqlite-vss (>=0.1.2,<0.2.0)", "streamlit (>=1.18.0,<2.0.0)", "sympy (>=1.12,<2.0)", "telethon (>=1.28.5,<2.0.0)", "timescale-vector (>=0.0.1,<0.0.2)", "tqdm (>=4.48.0)", "upstash-redis (>=0.15.0,<0.16.0)", "xata (>=1.0.0a7,<2.0.0)", "xmltodict (>=0.13.0,<0.14.0)", "zhipuai (>=1.0.7,<2.0.0)"] [package.source] type = "directory" @@ -3476,7 +3476,7 @@ url = "../community" [[package]] name = "langchain-core" -version = "0.1.7" +version = "0.1.14" description = "Building applications with LLMs through composability" optional = false python-versions = ">=3.8.1,<4.0" @@ -3486,7 +3486,7 @@ develop = true [package.dependencies] anyio = ">=3,<5" jsonpatch = "^1.33" -langsmith = "~0.0.63" +langsmith = ">=0.0.83,<0.1" packaging = "^23.2" pydantic = ">=1,<3" PyYAML = ">=5.3" @@ -3519,13 +3519,13 @@ tiktoken = ">=0.5.2,<0.6.0" [[package]] name = "langsmith" -version = "0.0.77" +version = "0.0.83" description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform." optional = false python-versions = ">=3.8.1,<4.0" files = [ - {file = "langsmith-0.0.77-py3-none-any.whl", hash = "sha256:750c0aa9177240c64e131d831e009ed08dd59038f7cabbd0bbcf62ccb7c8dcac"}, - {file = "langsmith-0.0.77.tar.gz", hash = "sha256:c4c8d3a96ad8671a41064f3ccc673e2e22a4153e823b19f915c9c9b8a4f33a2c"}, + {file = "langsmith-0.0.83-py3-none-any.whl", hash = "sha256:a5bb7ac58c19a415a9d5f51db56dd32ee2cd7343a00825bbc2018312eb3d122a"}, + {file = "langsmith-0.0.83.tar.gz", hash = "sha256:94427846b334ad9bdbec3266fee12903fe9f5448f628667689d0412012aaf392"}, ] [package.dependencies] @@ -9061,4 +9061,4 @@ text-helpers = ["chardet"] [metadata] lock-version = "2.0" python-versions = ">=3.8.1,<4.0" -content-hash = "00113cc914ec1dd07b5cb99d13fe9cb99ce79743743f80b345f745398faa3515" +content-hash = "2f5a1d207f8102e6531a0947886be63cd6d109dee4da9e37a0cb399bba259b4a" diff --git a/libs/langchain/pyproject.toml b/libs/langchain/pyproject.toml index 87a0080384b4c..ebd84e5f36501 100644 --- a/libs/langchain/pyproject.toml +++ b/libs/langchain/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "langchain" -version = "0.1.0" +version = "0.1.3" description = "Building applications with LLMs through composability" authors = [] license = "MIT" @@ -12,8 +12,9 @@ langchain-server = "langchain.server:main" [tool.poetry.dependencies] python = ">=3.8.1,<4.0" -langchain-core = ">=0.1.7,<0.2" -langchain-community = ">=0.0.9,<0.1" +langchain-core = ">=0.1.14,<0.2" +langchain-community = ">=0.0.14,<0.1" +langsmith = ">=0.0.83,<0.1" pydantic = ">=1,<3" SQLAlchemy = ">=1.4,<3" requests = "^2" @@ -80,7 +81,6 @@ cassio = {version = "^0.1.0", optional = true} sympy = {version = "^1.12", optional = true} rapidfuzz = {version = "^3.1.1", optional = true} jsonschema = {version = ">1", optional = true} -langsmith = "~0.0.77" rank-bm25 = {version = "^0.2.2", optional = true} geopandas = {version = "^0.13.1", optional = true} gitpython = {version = "^3.1.32", optional = true} diff --git a/libs/langchain/tests/integration_tests/.env.example b/libs/langchain/tests/integration_tests/.env.example index 40c520736d729..9e8ebfbdc559c 100644 --- a/libs/langchain/tests/integration_tests/.env.example +++ b/libs/langchain/tests/integration_tests/.env.example @@ -8,23 +8,6 @@ OPENAI_API_KEY=your_openai_api_key_here SEARCHAPI_API_KEY=your_searchapi_api_key_here -# pinecone -# your api key from left menu "API Keys" in https://app.pinecone.io -PINECONE_API_KEY=your_pinecone_api_key_here -# your pinecone environment from left menu "API Keys" in https://app.pinecone.io -PINECONE_ENVIRONMENT=us-west4-gcp - - -# jira -# your api token from https://id.atlassian.com/manage-profile/security/api-tokens -# more details here: https://confluence.atlassian.com/enterprise/using-personal-access-tokens-1026032365.html -# JIRA_API_TOKEN=your_jira_api_token_here -# JIRA_USERNAME=your_jira_username_here -# JIRA_INSTANCE_URL=your_jira_instance_url_here - -# clickup -CLICKUP_ACCESS_TOKEN=your_clickup_access_token - # power bi # sign in to azure in order to authenticate with DefaultAzureCredentials # details here https://learn.microsoft.com/en-us/dotnet/api/azure.identity.defaultazurecredential?view=azure-dotnet @@ -33,5 +16,7 @@ POWERBI_TABLE_NAME=_test_table_name_here POWERBI_NUMROWS=_num_rows_in_your_test_table -# MongoDB Atlas Vector Search -MONGODB_ATLAS_URI=your_mongodb_atlas_connection_string \ No newline at end of file +# astra db +ASTRA_DB_API_ENDPOINT=https://your_astra_db_id-your_region.apps.astra.datastax.com +ASTRA_DB_APPLICATION_TOKEN=AstraCS:your_astra_db_application_token +# ASTRA_DB_KEYSPACE=your_astra_db_namespace diff --git a/libs/langchain/tests/unit_tests/agents/test_openai_assistant.py b/libs/langchain/tests/unit_tests/agents/test_openai_assistant.py new file mode 100644 index 0000000000000..aaa4ba48d1df0 --- /dev/null +++ b/libs/langchain/tests/unit_tests/agents/test_openai_assistant.py @@ -0,0 +1,21 @@ +import pytest + +from langchain.agents.openai_assistant import OpenAIAssistantRunnable + + +@pytest.mark.requires("openai") +def test_user_supplied_client() -> None: + import openai + + client = openai.AzureOpenAI( + azure_endpoint="azure_endpoint", + api_key="api_key", + api_version="api_version", + ) + + assistant = OpenAIAssistantRunnable( + assistant_id="assistant_id", + client=client, + ) + + assert assistant.client == client diff --git a/libs/langchain/tests/unit_tests/load/__snapshots__/test_dump.ambr b/libs/langchain/tests/unit_tests/load/__snapshots__/test_dump.ambr index 2cf2fdf8cd258..0540760d4ce9d 100644 --- a/libs/langchain/tests/unit_tests/load/__snapshots__/test_dump.ambr +++ b/libs/langchain/tests/unit_tests/load/__snapshots__/test_dump.ambr @@ -30,10 +30,9 @@ "lc": 1, "type": "constructor", "id": [ - "tests", - "unit_tests", - "load", - "test_dump", + "my", + "special", + "namespace", "SpecialPerson" ], "kwargs": { diff --git a/libs/langchain/tests/unit_tests/load/test_dump.py b/libs/langchain/tests/unit_tests/load/test_dump.py index d428b04c71e06..3e59fbda81186 100644 --- a/libs/langchain/tests/unit_tests/load/test_dump.py +++ b/libs/langchain/tests/unit_tests/load/test_dump.py @@ -1,6 +1,8 @@ """Test for Serializable base class""" -from typing import Any, Dict +import os +from typing import Any, Dict, List +from unittest.mock import patch import pytest from langchain_community.chat_models.openai import ChatOpenAI @@ -37,6 +39,10 @@ class SpecialPerson(Person): another_visible: str = "bye" + @classmethod + def get_lc_namespace(cls) -> List[str]: + return ["my", "special", "namespace"] + # Gets merged with parent class's secrets @property def lc_secrets(self) -> Dict[str, str]: @@ -58,6 +64,7 @@ def test_person(snapshot: Any) -> None: sp = SpecialPerson(another_secret="Wooo", secret="Hmm") assert dumps(sp, pretty=True) == snapshot assert Person.lc_id() == ["tests", "unit_tests", "load", "test_dump", "Person"] + assert SpecialPerson.lc_id() == ["my", "special", "namespace", "SpecialPerson"] def test_typeerror() -> None: @@ -69,15 +76,16 @@ def test_typeerror() -> None: @pytest.mark.requires("openai") def test_serialize_openai_llm(snapshot: Any) -> None: - llm = OpenAI( - model="davinci", - temperature=0.5, - openai_api_key="hello", - # This is excluded from serialization - callbacks=[LangChainTracer()], - ) - llm.temperature = 0.7 # this is reflected in serialization - assert dumps(llm, pretty=True) == snapshot + with patch.dict(os.environ, {"LANGCHAIN_API_KEY": "test-api-key"}): + llm = OpenAI( + model="davinci", + temperature=0.5, + openai_api_key="hello", + # This is excluded from serialization + callbacks=[LangChainTracer()], + ) + llm.temperature = 0.7 # this is reflected in serialization + assert dumps(llm, pretty=True) == snapshot @pytest.mark.requires("openai") diff --git a/libs/langchain/tests/unit_tests/output_parsers/test_openai_functions.py b/libs/langchain/tests/unit_tests/output_parsers/test_openai_functions.py index 6af2be93c319a..ed1bebf6df5ce 100644 --- a/libs/langchain/tests/unit_tests/output_parsers/test_openai_functions.py +++ b/libs/langchain/tests/unit_tests/output_parsers/test_openai_functions.py @@ -1,3 +1,4 @@ +import json from typing import Any, Dict import pytest @@ -7,7 +8,9 @@ from langchain.output_parsers.openai_functions import ( JsonOutputFunctionsParser, + PydanticOutputFunctionsParser, ) +from langchain.pydantic_v1 import BaseModel def test_json_output_function_parser() -> None: @@ -134,3 +137,61 @@ def test_exceptions_raised_while_parsing(bad_message: BaseMessage) -> None: with pytest.raises(OutputParserException): JsonOutputFunctionsParser().parse_result([chat_generation]) + + +def test_pydantic_output_functions_parser() -> None: + """Test pydantic output functions parser.""" + message = AIMessage( + content="This is a test message", + additional_kwargs={ + "function_call": { + "name": "function_name", + "arguments": json.dumps({"name": "value", "age": 10}), + } + }, + ) + chat_generation = ChatGeneration(message=message) + + class Model(BaseModel): + """Test model.""" + + name: str + age: int + + # Full output + parser = PydanticOutputFunctionsParser(pydantic_schema=Model) + result = parser.parse_result([chat_generation]) + assert result == Model(name="value", age=10) + + +def test_pydantic_output_functions_parser_multiple_schemas() -> None: + """Test that the parser works if providing multiple pydantic schemas.""" + + message = AIMessage( + content="This is a test message", + additional_kwargs={ + "function_call": { + "name": "cookie", + "arguments": json.dumps({"name": "value", "age": 10}), + } + }, + ) + chat_generation = ChatGeneration(message=message) + + class Cookie(BaseModel): + """Test model.""" + + name: str + age: int + + class Dog(BaseModel): + """Test model.""" + + species: str + + # Full output + parser = PydanticOutputFunctionsParser( + pydantic_schema={"cookie": Cookie, "dog": Dog} + ) + result = parser.parse_result([chat_generation]) + assert result == Cookie(name="value", age=10) diff --git a/libs/langchain/tests/unit_tests/retrievers/self_query/test_astradb.py b/libs/langchain/tests/unit_tests/retrievers/self_query/test_astradb.py new file mode 100644 index 0000000000000..78fc47603a434 --- /dev/null +++ b/libs/langchain/tests/unit_tests/retrievers/self_query/test_astradb.py @@ -0,0 +1,131 @@ +from typing import Dict, Tuple + +from langchain.chains.query_constructor.ir import ( + Comparator, + Comparison, + Operation, + Operator, + StructuredQuery, +) +from langchain.retrievers.self_query.astradb import AstraDBTranslator + +DEFAULT_TRANSLATOR = AstraDBTranslator() + + +def test_visit_comparison_lt() -> None: + comp = Comparison(comparator=Comparator.LT, attribute="qty", value=20) + expected = {"qty": {"$lt": 20}} + actual = DEFAULT_TRANSLATOR.visit_comparison(comp) + assert expected == actual + + +def test_visit_comparison_eq() -> None: + comp = Comparison(comparator=Comparator.EQ, attribute="qty", value=10) + expected = {"qty": {"$eq": 10}} + actual = DEFAULT_TRANSLATOR.visit_comparison(comp) + assert expected == actual + + +def test_visit_comparison_ne() -> None: + comp = Comparison(comparator=Comparator.NE, attribute="name", value="foo") + expected = {"name": {"$ne": "foo"}} + actual = DEFAULT_TRANSLATOR.visit_comparison(comp) + assert expected == actual + + +def test_visit_comparison_in() -> None: + comp = Comparison(comparator=Comparator.IN, attribute="name", value="foo") + expected = {"name": {"$in": ["foo"]}} + actual = DEFAULT_TRANSLATOR.visit_comparison(comp) + assert expected == actual + + +def test_visit_comparison_nin() -> None: + comp = Comparison(comparator=Comparator.NIN, attribute="name", value="foo") + expected = {"name": {"$nin": ["foo"]}} + actual = DEFAULT_TRANSLATOR.visit_comparison(comp) + assert expected == actual + + +def test_visit_operation() -> None: + op = Operation( + operator=Operator.AND, + arguments=[ + Comparison(comparator=Comparator.GTE, attribute="qty", value=10), + Comparison(comparator=Comparator.LTE, attribute="qty", value=20), + Comparison(comparator=Comparator.EQ, attribute="name", value="foo"), + ], + ) + expected = { + "$and": [ + {"qty": {"$gte": 10}}, + {"qty": {"$lte": 20}}, + {"name": {"$eq": "foo"}}, + ] + } + actual = DEFAULT_TRANSLATOR.visit_operation(op) + assert expected == actual + + +def test_visit_structured_query_no_filter() -> None: + query = "What is the capital of France?" + structured_query = StructuredQuery( + query=query, + filter=None, + ) + expected: Tuple[str, Dict] = (query, {}) + actual = DEFAULT_TRANSLATOR.visit_structured_query(structured_query) + assert expected == actual + + +def test_visit_structured_query_one_attr() -> None: + query = "What is the capital of France?" + comp = Comparison(comparator=Comparator.IN, attribute="qty", value=[5, 15, 20]) + structured_query = StructuredQuery( + query=query, + filter=comp, + ) + expected = ( + query, + {"filter": {"qty": {"$in": [5, 15, 20]}}}, + ) + actual = DEFAULT_TRANSLATOR.visit_structured_query(structured_query) + assert expected == actual + + +def test_visit_structured_query_deep_nesting() -> None: + query = "What is the capital of France?" + op = Operation( + operator=Operator.AND, + arguments=[ + Comparison(comparator=Comparator.EQ, attribute="name", value="foo"), + Operation( + operator=Operator.OR, + arguments=[ + Comparison(comparator=Comparator.GT, attribute="qty", value=6), + Comparison( + comparator=Comparator.NIN, + attribute="tags", + value=["bar", "foo"], + ), + ], + ), + ], + ) + structured_query = StructuredQuery( + query=query, + filter=op, + ) + expected = ( + query, + { + "filter": { + "$and": [ + {"name": {"$eq": "foo"}}, + {"$or": [{"qty": {"$gt": 6}}, {"tags": {"$nin": ["bar", "foo"]}}]}, + ] + } + }, + ) + actual = DEFAULT_TRANSLATOR.visit_structured_query(structured_query) + assert expected == actual diff --git a/libs/langchain/tests/unit_tests/retrievers/self_query/test_milvus.py b/libs/langchain/tests/unit_tests/retrievers/self_query/test_milvus.py index d444974404413..4a96c18e274a6 100644 --- a/libs/langchain/tests/unit_tests/retrievers/self_query/test_milvus.py +++ b/libs/langchain/tests/unit_tests/retrievers/self_query/test_milvus.py @@ -1,4 +1,6 @@ -from typing import Dict, Tuple +from typing import Any, Dict, Tuple + +import pytest from langchain.chains.query_constructor.ir import ( Comparator, @@ -12,11 +14,22 @@ DEFAULT_TRANSLATOR = MilvusTranslator() -def test_visit_comparison() -> None: - comp = Comparison(comparator=Comparator.LT, attribute="foo", value=4) - expected = "( foo < 4 )" +@pytest.mark.parametrize( + "triplet", + [ + (Comparator.EQ, 2, "( foo == 2 )"), + (Comparator.GT, 2, "( foo > 2 )"), + (Comparator.GTE, 2, "( foo >= 2 )"), + (Comparator.LT, 2, "( foo < 2 )"), + (Comparator.LTE, 2, "( foo <= 2 )"), + (Comparator.IN, ["bar", "abc"], "( foo in ['bar', 'abc'] )"), + (Comparator.LIKE, "bar", '( foo like "bar%" )'), + ], +) +def test_visit_comparison(triplet: Tuple[Comparator, Any, str]) -> None: + comparator, value, expected = triplet + comp = Comparison(comparator=comparator, attribute="foo", value=value) actual = DEFAULT_TRANSLATOR.visit_comparison(comp) - assert expected == actual diff --git a/libs/langchain/tests/unit_tests/retrievers/self_query/test_opensearch.py b/libs/langchain/tests/unit_tests/retrievers/self_query/test_opensearch.py index 629d195402e31..93b6629ecb01e 100644 --- a/libs/langchain/tests/unit_tests/retrievers/self_query/test_opensearch.py +++ b/libs/langchain/tests/unit_tests/retrievers/self_query/test_opensearch.py @@ -85,3 +85,90 @@ def test_visit_structured_query() -> None: ) actual = DEFAULT_TRANSLATOR.visit_structured_query(structured_query) assert expected == actual + + +def test_visit_structured_query_with_date_range() -> None: + query = "Who was the president of France in 1995?" + operation = Operation( + operator=Operator.AND, + arguments=[ + Comparison(comparator=Comparator.EQ, attribute="foo", value="20"), + Operation( + operator=Operator.AND, + arguments=[ + Comparison( + comparator=Comparator.GTE, + attribute="timestamp", + value={"date": "1995-01-01", "type": "date"}, + ), + Comparison( + comparator=Comparator.LT, + attribute="timestamp", + value={"date": "1996-01-01", "type": "date"}, + ), + ], + ), + ], + ) + structured_query = StructuredQuery(query=query, filter=operation, limit=None) + expected = ( + query, + { + "filter": { + "bool": { + "must": [ + {"term": {"metadata.foo.keyword": "20"}}, + { + "bool": { + "must": [ + { + "range": { + "metadata.timestamp": {"gte": "1995-01-01"} + } + }, + { + "range": { + "metadata.timestamp": {"lt": "1996-01-01"} + } + }, + ] + } + }, + ] + } + } + }, + ) + actual = DEFAULT_TRANSLATOR.visit_structured_query(structured_query) + assert expected == actual + + +def test_visit_structured_query_with_date() -> None: + query = "Who was the president of France on 1st of January 1995?" + operation = Operation( + operator=Operator.AND, + arguments=[ + Comparison(comparator=Comparator.EQ, attribute="foo", value="20"), + Comparison( + comparator=Comparator.EQ, + attribute="timestamp", + value={"date": "1995-01-01", "type": "date"}, + ), + ], + ) + structured_query = StructuredQuery(query=query, filter=operation, limit=None) + expected = ( + query, + { + "filter": { + "bool": { + "must": [ + {"term": {"metadata.foo.keyword": "20"}}, + {"term": {"metadata.timestamp": "1995-01-01"}}, + ] + } + } + }, + ) + actual = DEFAULT_TRANSLATOR.visit_structured_query(structured_query) + assert expected == actual diff --git a/libs/partners/anthropic/pyproject.toml b/libs/partners/anthropic/pyproject.toml index 53ab7dd79fd70..459d07d3b4ff3 100644 --- a/libs/partners/anthropic/pyproject.toml +++ b/libs/partners/anthropic/pyproject.toml @@ -1,10 +1,11 @@ [tool.poetry] name = "langchain-anthropic" -version = "0.0.1.post1" +version = "0.0.1.post2" description = "An integration package connecting AnthropicMessages and LangChain" authors = [] readme = "README.md" repository = "https://github.com/langchain-ai/langchain" +license = "MIT" [tool.poetry.urls] "Source Code" = "https://github.com/langchain-ai/langchain/tree/master/libs/partners/anthropic" diff --git a/libs/partners/google-genai/langchain_google_genai/chat_models.py b/libs/partners/google-genai/langchain_google_genai/chat_models.py index d226aeefc4f64..62cb707cdca8c 100644 --- a/libs/partners/google-genai/langchain_google_genai/chat_models.py +++ b/libs/partners/google-genai/langchain_google_genai/chat_models.py @@ -443,7 +443,7 @@ class ChatGoogleGenerativeAI(BaseChatModel): top_k: Optional[int] = None """Decode using top-k sampling: consider the set of top_k most probable tokens. Must be positive.""" - top_p: Optional[int] = None + top_p: Optional[float] = None """The maximum cumulative probability of tokens to consider when sampling. The model uses combined Top-k and nucleus sampling. diff --git a/libs/partners/google-genai/pyproject.toml b/libs/partners/google-genai/pyproject.toml index f98ed0a765edb..28884a3cbf13f 100644 --- a/libs/partners/google-genai/pyproject.toml +++ b/libs/partners/google-genai/pyproject.toml @@ -1,10 +1,11 @@ [tool.poetry] name = "langchain-google-genai" -version = "0.0.6" +version = "0.0.6.post1" description = "An integration package connecting Google's genai package and LangChain" authors = [] readme = "README.md" repository = "https://github.com/langchain-ai/langchain" +license = "MIT" [tool.poetry.urls] "Source Code" = "https://github.com/langchain-ai/langchain/tree/master/libs/partners/google-genai" diff --git a/libs/partners/google-vertexai/Makefile b/libs/partners/google-vertexai/Makefile index ceb9823f9d1be..29214d4bbc70d 100644 --- a/libs/partners/google-vertexai/Makefile +++ b/libs/partners/google-vertexai/Makefile @@ -6,7 +6,9 @@ all: help # Define a variable for the test file path. TEST_FILE ?= tests/unit_tests/ -test: +integration_tests: TEST_FILE = tests/integration_tests/ + +test integration_tests: poetry run pytest $(TEST_FILE) tests: diff --git a/libs/partners/google-vertexai/README.md b/libs/partners/google-vertexai/README.md index 6a4839254fc2d..0637bd7cc9119 100644 --- a/libs/partners/google-vertexai/README.md +++ b/libs/partners/google-vertexai/README.md @@ -10,7 +10,7 @@ pip install -U langchain-google-vertexai ## Chat Models -`ChatVertexAI` class exposes models . +`ChatVertexAI` class exposes models such as `gemini-pro` and `chat-bison`. To use, you should have Google Cloud project with APIs enabled, and configured credentials. Initialize the model as: @@ -63,7 +63,7 @@ The value of `image_url` can be any of the following: You can use Google Cloud's embeddings models as: -``` +```python from langchain_google_vertexai import VertexAIEmbeddings embeddings = VertexAIEmbeddings() diff --git a/libs/partners/google-vertexai/langchain_google_vertexai/__init__.py b/libs/partners/google-vertexai/langchain_google_vertexai/__init__.py index 391a7c7b1d1e0..be365bde4c33a 100644 --- a/libs/partners/google-vertexai/langchain_google_vertexai/__init__.py +++ b/libs/partners/google-vertexai/langchain_google_vertexai/__init__.py @@ -1,5 +1,17 @@ +from langchain_google_vertexai._enums import HarmBlockThreshold, HarmCategory +from langchain_google_vertexai.chains import create_structured_runnable from langchain_google_vertexai.chat_models import ChatVertexAI from langchain_google_vertexai.embeddings import VertexAIEmbeddings +from langchain_google_vertexai.functions_utils import PydanticFunctionsOutputParser from langchain_google_vertexai.llms import VertexAI, VertexAIModelGarden -__all__ = ["ChatVertexAI", "VertexAIEmbeddings", "VertexAI", "VertexAIModelGarden"] +__all__ = [ + "ChatVertexAI", + "VertexAIEmbeddings", + "VertexAI", + "VertexAIModelGarden", + "HarmBlockThreshold", + "HarmCategory", + "PydanticFunctionsOutputParser", + "create_structured_runnable", +] diff --git a/libs/partners/google-vertexai/langchain_google_vertexai/_enums.py b/libs/partners/google-vertexai/langchain_google_vertexai/_enums.py new file mode 100644 index 0000000000000..00a2abaa32106 --- /dev/null +++ b/libs/partners/google-vertexai/langchain_google_vertexai/_enums.py @@ -0,0 +1,6 @@ +from vertexai.preview.generative_models import ( # type: ignore + HarmBlockThreshold, + HarmCategory, +) + +__all__ = ["HarmBlockThreshold", "HarmCategory"] diff --git a/libs/partners/google-vertexai/langchain_google_vertexai/_utils.py b/libs/partners/google-vertexai/langchain_google_vertexai/_utils.py index 6dcc7a2d73cd8..340acc05d8ff6 100644 --- a/libs/partners/google-vertexai/langchain_google_vertexai/_utils.py +++ b/libs/partners/google-vertexai/langchain_google_vertexai/_utils.py @@ -1,6 +1,6 @@ """Utilities to init Vertex AI.""" from importlib import metadata -from typing import Any, Callable, Optional, Union +from typing import Any, Callable, Dict, Optional, Union import google.api_core from google.api_core.gapic_v1.client_info import ClientInfo @@ -86,3 +86,29 @@ def is_codey_model(model_name: str) -> bool: def is_gemini_model(model_name: str) -> bool: """Returns True if the model name is a Gemini model.""" return model_name is not None and "gemini" in model_name + + +def get_generation_info(candidate: Any, is_gemini: bool) -> Optional[Dict[str, Any]]: + try: + if is_gemini: + # https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/gemini#response_body + return { + "is_blocked": any( + [rating.blocked for rating in candidate.safety_ratings] + ), + "safety_ratings": [ + { + "category": rating.category.name, + "probability_label": rating.probability.name, + } + for rating in candidate.safety_ratings + ], + } + else: + # https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/text-chat#response_body + return { + "is_blocked": candidate.is_blocked, + "safety_attributes": candidate.safety_attributes, + } + except Exception: + return None diff --git a/libs/partners/google-vertexai/langchain_google_vertexai/chains.py b/libs/partners/google-vertexai/langchain_google_vertexai/chains.py new file mode 100644 index 0000000000000..9b11794b30936 --- /dev/null +++ b/libs/partners/google-vertexai/langchain_google_vertexai/chains.py @@ -0,0 +1,111 @@ +from typing import ( + Dict, + Optional, + Sequence, + Type, + Union, +) + +from langchain_core.output_parsers import ( + BaseGenerationOutputParser, + BaseOutputParser, +) +from langchain_core.prompts import BasePromptTemplate +from langchain_core.pydantic_v1 import BaseModel +from langchain_core.runnables import Runnable + +from langchain_google_vertexai.functions_utils import PydanticFunctionsOutputParser + + +def get_output_parser( + functions: Sequence[Type[BaseModel]], +) -> Union[BaseOutputParser, BaseGenerationOutputParser]: + """Get the appropriate function output parser given the user functions. + + Args: + functions: Sequence where element is a dictionary, a pydantic.BaseModel class, + or a Python function. If a dictionary is passed in, it is assumed to + already be a valid OpenAI function. + + Returns: + A PydanticFunctionsOutputParser + """ + function_names = [f.__name__ for f in functions] + if len(functions) > 1: + pydantic_schema: Union[Dict, Type[BaseModel]] = { + name: fn for name, fn in zip(function_names, functions) + } + else: + pydantic_schema = functions[0] + output_parser: Union[ + BaseOutputParser, BaseGenerationOutputParser + ] = PydanticFunctionsOutputParser(pydantic_schema=pydantic_schema) + return output_parser + + +def create_structured_runnable( + function: Union[Type[BaseModel], Sequence[Type[BaseModel]]], + llm: Runnable, + *, + prompt: Optional[BasePromptTemplate] = None, +) -> Runnable: + """Create a runnable sequence that uses OpenAI functions. + + Args: + function: Either a single pydantic.BaseModel class or a sequence + of pydantic.BaseModels classes. + For best results, pydantic.BaseModels + should have descriptions of the parameters. + llm: Language model to use, + assumed to support the Google Vertex function-calling API. + prompt: BasePromptTemplate to pass to the model. + + Returns: + A runnable sequence that will pass in the given functions to the model when run. + + Example: + .. code-block:: python + + from typing import Optional + + from langchain_google_vertexai import ChatVertexAI, create_structured_runnable + from langchain_core.prompts import ChatPromptTemplate + from langchain_core.pydantic_v1 import BaseModel, Field + + + class RecordPerson(BaseModel): + \"\"\"Record some identifying information about a person.\"\"\" + + name: str = Field(..., description="The person's name") + age: int = Field(..., description="The person's age") + fav_food: Optional[str] = Field(None, description="The person's favorite food") + + + class RecordDog(BaseModel): + \"\"\"Record some identifying information about a dog.\"\"\" + + name: str = Field(..., description="The dog's name") + color: str = Field(..., description="The dog's color") + fav_food: Optional[str] = Field(None, description="The dog's favorite food") + + + llm = ChatVertexAI(model_name="gemini-pro") + prompt = ChatPromptTemplate.from_template(\"\"\" + You are a world class algorithm for recording entities. + Make calls to the relevant function to record the entities in the following input: {input} + Tip: Make sure to answer in the correct format\"\"\" + ) + chain = create_structured_runnable([RecordPerson, RecordDog], llm, prompt=prompt) + chain.invoke({"input": "Harry was a chubby brown beagle who loved chicken"}) + # -> RecordDog(name="Harry", color="brown", fav_food="chicken") + """ # noqa: E501 + if not function: + raise ValueError("Need to pass in at least one function. Received zero.") + functions = function if isinstance(function, Sequence) else [function] + output_parser = get_output_parser(functions) + llm_with_functions = llm.bind(functions=functions) + if prompt is None: + initial_chain = llm_with_functions + else: + initial_chain = prompt | llm_with_functions + return initial_chain | output_parser diff --git a/libs/partners/google-vertexai/langchain_google_vertexai/chat_models.py b/libs/partners/google-vertexai/langchain_google_vertexai/chat_models.py index 49f28d0bf8ca1..1c62d76c75c0d 100644 --- a/libs/partners/google-vertexai/langchain_google_vertexai/chat_models.py +++ b/libs/partners/google-vertexai/langchain_google_vertexai/chat_models.py @@ -2,6 +2,7 @@ from __future__ import annotations import base64 +import json import logging import re from dataclasses import dataclass, field @@ -9,6 +10,8 @@ from urllib.parse import urlparse import requests +from google.cloud.aiplatform_v1beta1.types.content import Part as GapicPart +from google.cloud.aiplatform_v1beta1.types.tool import FunctionCall from langchain_core.callbacks import ( AsyncCallbackManagerForLLMRun, CallbackManagerForLLMRun, @@ -21,6 +24,7 @@ AIMessage, AIMessageChunk, BaseMessage, + FunctionMessage, HumanMessage, SystemMessage, ) @@ -35,6 +39,7 @@ InputOutputTextPair, ) from vertexai.preview.generative_models import ( # type: ignore + Candidate, Content, GenerativeModel, Image, @@ -42,10 +47,14 @@ ) from langchain_google_vertexai._utils import ( + get_generation_info, is_codey_model, is_gemini_model, load_image_from_gcs, ) +from langchain_google_vertexai.functions_utils import ( + _format_tools_to_vertex_tool, +) from langchain_google_vertexai.llms import ( _VertexAICommon, ) @@ -102,7 +111,9 @@ def _is_url(s: str) -> bool: def _parse_chat_history_gemini( - history: List[BaseMessage], project: Optional[str] + history: List[BaseMessage], + project: Optional[str] = None, + convert_system_message_to_human: Optional[bool] = False, ) -> List[Content]: def _convert_to_prompt(part: Union[str, Dict]) -> Part: if isinstance(part, str): @@ -139,23 +150,73 @@ def _convert_to_prompt(part: Union[str, Dict]) -> Part: raise ValueError("Only text and image_url types are supported!") return Part.from_image(image) + def _convert_to_parts(message: BaseMessage) -> List[Part]: + raw_content = message.content + if isinstance(raw_content, str): + raw_content = [raw_content] + return [_convert_to_prompt(part) for part in raw_content] + vertex_messages = [] + raw_system_message = None for i, message in enumerate(history): - if i == 0 and isinstance(message, SystemMessage): - raise ValueError("SystemMessages are not yet supported!") + if ( + i == 0 + and isinstance(message, SystemMessage) + and not convert_system_message_to_human + ): + raise ValueError( + """SystemMessages are not yet supported! + +To automatically convert the leading SystemMessage to a HumanMessage, +set `convert_system_message_to_human` to True. Example: + +llm = ChatVertexAI(model_name="gemini-pro", convert_system_message_to_human=True) +""" + ) + elif i == 0 and isinstance(message, SystemMessage): + raw_system_message = message + continue elif isinstance(message, AIMessage): + raw_function_call = message.additional_kwargs.get("function_call") role = "model" + if raw_function_call: + function_call = FunctionCall( + { + "name": raw_function_call["name"], + "args": json.loads(raw_function_call["arguments"]), + } + ) + gapic_part = GapicPart(function_call=function_call) + parts = [Part._from_gapic(gapic_part)] + else: + parts = _convert_to_parts(message) elif isinstance(message, HumanMessage): role = "user" + parts = _convert_to_parts(message) + elif isinstance(message, FunctionMessage): + role = "user" + parts = [ + Part.from_function_response( + name=message.name, + response={ + "content": message.content, + }, + ) + ] else: raise ValueError( f"Unexpected message with type {type(message)} at the position {i}." ) - raw_content = message.content - if isinstance(raw_content, str): - raw_content = [raw_content] - parts = [_convert_to_prompt(part) for part in raw_content] + if raw_system_message: + if role == "model": + raise ValueError( + "SystemMessage should be followed by a HumanMessage and " + "not by AIMessage." + ) + parts = _convert_to_parts(raw_system_message) + parts + raw_system_message = None + vertex_message = Content(role=role, parts=parts) vertex_messages.append(vertex_message) return vertex_messages @@ -201,20 +262,60 @@ def _get_question(messages: List[BaseMessage]) -> HumanMessage: return question +def _parse_response_candidate(response_candidate: "Candidate") -> AIMessage: + try: + content = response_candidate.text + except ValueError: + content = "" + + additional_kwargs = {} + first_part = response_candidate.content.parts[0] + if first_part.function_call: + function_call = {"name": first_part.function_call.name} + + # dump to match other function calling llm for now + function_call["arguments"] = json.dumps( + {k: first_part.function_call.args[k] for k in first_part.function_call.args} + ) + additional_kwargs["function_call"] = function_call + return AIMessage(content=content, additional_kwargs=additional_kwargs) + + class ChatVertexAI(_VertexAICommon, BaseChatModel): """`Vertex AI` Chat large language models API.""" model_name: str = "chat-bison" "Underlying model name." examples: Optional[List[BaseMessage]] = None + convert_system_message_to_human: bool = False + """Whether to merge any leading SystemMessage into the following HumanMessage. + + Gemini does not support system messages; any unsupported messages will + raise an error.""" + + @classmethod + def is_lc_serializable(self) -> bool: + return True + + @classmethod + def get_lc_namespace(cls) -> List[str]: + """Get the namespace of the langchain object.""" + return ["langchain", "chat_models", "vertexai"] @root_validator() def validate_environment(cls, values: Dict) -> Dict: """Validate that the python package exists in environment.""" is_gemini = is_gemini_model(values["model_name"]) + safety_settings = values["safety_settings"] + + if safety_settings and not is_gemini: + raise ValueError("Safety settings are only supported for Gemini models") + cls._init_vertexai(values) if is_gemini: - values["client"] = GenerativeModel(model_name=values["model_name"]) + values["client"] = GenerativeModel( + model_name=values["model_name"], safety_settings=safety_settings + ) else: if is_codey_model(values["model_name"]): model_cls = CodeChatModel @@ -247,34 +348,58 @@ def _generate( ValueError: if the last message in the list is not from human. """ should_stream = stream if stream is not None else self.streaming + safety_settings = kwargs.pop("safety_settings", None) if should_stream: stream_iter = self._stream( messages, stop=stop, run_manager=run_manager, **kwargs ) return generate_from_stream(stream_iter) - question = _get_question(messages) params = self._prepare_params(stop=stop, stream=False, **kwargs) msg_params = {} if "candidate_count" in params: msg_params["candidate_count"] = params.pop("candidate_count") if self._is_gemini_model: - history_gemini = _parse_chat_history_gemini(messages, project=self.project) + history_gemini = _parse_chat_history_gemini( + messages, + project=self.project, + convert_system_message_to_human=self.convert_system_message_to_human, + ) message = history_gemini.pop() chat = self.client.start_chat(history=history_gemini) - response = chat.send_message(message, generation_config=params) + + # set param to `functions` until core tool/function calling implemented + raw_tools = params.pop("functions") if "functions" in params else None + tools = _format_tools_to_vertex_tool(raw_tools) if raw_tools else None + response = chat.send_message( + message, + generation_config=params, + tools=tools, + safety_settings=safety_settings, + ) + generations = [ + ChatGeneration( + message=_parse_response_candidate(c), + generation_info=get_generation_info(c, self._is_gemini_model), + ) + for c in response.candidates + ] else: + question = _get_question(messages) history = _parse_chat_history(messages[:-1]) examples = kwargs.get("examples") or self.examples if examples: params["examples"] = _parse_examples(examples) chat = self._start_chat(history, **params) response = chat.send_message(question.content, **msg_params) - generations = [ - ChatGeneration(message=AIMessage(content=r.text)) - for r in response.candidates - ] + generations = [ + ChatGeneration( + message=AIMessage(content=r.text), + generation_info=get_generation_info(r, self._is_gemini_model), + ) + for r in response.candidates + ] return ChatResult(generations=generations) async def _agenerate( @@ -303,28 +428,50 @@ async def _agenerate( logger.warning("ChatVertexAI does not currently support async streaming.") params = self._prepare_params(stop=stop, **kwargs) + safety_settings = kwargs.pop("safety_settings", None) msg_params = {} if "candidate_count" in params: msg_params["candidate_count"] = params.pop("candidate_count") if self._is_gemini_model: - history_gemini = _parse_chat_history_gemini(messages, project=self.project) + history_gemini = _parse_chat_history_gemini( + messages, + project=self.project, + convert_system_message_to_human=self.convert_system_message_to_human, + ) message = history_gemini.pop() chat = self.client.start_chat(history=history_gemini) - response = await chat.send_message_async(message, generation_config=params) + # set param to `functions` until core tool/function calling implemented + raw_tools = params.pop("functions") if "functions" in params else None + tools = _format_tools_to_vertex_tool(raw_tools) if raw_tools else None + response = await chat.send_message_async( + message, + generation_config=params, + tools=tools, + safety_settings=safety_settings, + ) + generations = [ + ChatGeneration( + message=_parse_response_candidate(c), + generation_info=get_generation_info(c, self._is_gemini_model), + ) + for c in response.candidates + ] else: question = _get_question(messages) history = _parse_chat_history(messages[:-1]) - examples = kwargs.get("examples", None) + examples = kwargs.get("examples", None) or self.examples if examples: params["examples"] = _parse_examples(examples) chat = self._start_chat(history, **params) response = await chat.send_message_async(question.content, **msg_params) - - generations = [ - ChatGeneration(message=AIMessage(content=r.text)) - for r in response.candidates - ] + generations = [ + ChatGeneration( + message=AIMessage(content=r.text), + generation_info=get_generation_info(r, self._is_gemini_model), + ) + for r in response.candidates + ] return ChatResult(generations=generations) def _stream( @@ -336,12 +483,29 @@ def _stream( ) -> Iterator[ChatGenerationChunk]: params = self._prepare_params(stop=stop, stream=True, **kwargs) if self._is_gemini_model: - history_gemini = _parse_chat_history_gemini(messages, project=self.project) + history_gemini = _parse_chat_history_gemini( + messages, + project=self.project, + convert_system_message_to_human=self.convert_system_message_to_human, + ) message = history_gemini.pop() chat = self.client.start_chat(history=history_gemini) + # set param to `functions` until core tool/function calling implemented + raw_tools = params.pop("functions") if "functions" in params else None + tools = _format_tools_to_vertex_tool(raw_tools) if raw_tools else None responses = chat.send_message( - message, stream=True, generation_config=params + message, stream=True, generation_config=params, tools=tools ) + for response in responses: + message = _parse_response_candidate(response.candidates[0]) + if run_manager: + run_manager.on_llm_new_token(message.content) + yield ChatGenerationChunk( + message=AIMessageChunk( + content=message.content, + additional_kwargs=message.additional_kwargs, + ) + ) else: question = _get_question(messages) history = _parse_chat_history(messages[:-1]) @@ -353,7 +517,10 @@ def _stream( for response in responses: if run_manager: run_manager.on_llm_new_token(response.text) - yield ChatGenerationChunk(message=AIMessageChunk(content=response.text)) + yield ChatGenerationChunk( + message=AIMessageChunk(content=response.text), + generation_info=get_generation_info(response, self._is_gemini_model), + ) def _start_chat( self, history: _ChatHistory, **kwargs: Any diff --git a/libs/partners/google-vertexai/langchain_google_vertexai/functions_utils.py b/libs/partners/google-vertexai/langchain_google_vertexai/functions_utils.py new file mode 100644 index 0000000000000..b44b679afcbcf --- /dev/null +++ b/libs/partners/google-vertexai/langchain_google_vertexai/functions_utils.py @@ -0,0 +1,154 @@ +import json +from typing import Dict, List, Type, Union + +from langchain_core.exceptions import OutputParserException +from langchain_core.output_parsers import BaseOutputParser +from langchain_core.outputs import ChatGeneration, Generation +from langchain_core.pydantic_v1 import BaseModel +from langchain_core.tools import BaseTool +from langchain_core.utils.function_calling import FunctionDescription +from langchain_core.utils.json_schema import dereference_refs +from vertexai.preview.generative_models import ( # type: ignore + FunctionDeclaration, +) +from vertexai.preview.generative_models import ( + Tool as VertexTool, # type: ignore +) + + +def _format_pydantic_to_vertex_function( + pydantic_model: Type[BaseModel], +) -> FunctionDescription: + schema = dereference_refs(pydantic_model.schema()) + schema.pop("definitions", None) + + return { + "name": schema["title"], + "description": schema.get("description", ""), + "parameters": { + "properties": { + k: { + "type": v["type"], + "description": v.get("description"), + } + for k, v in schema["properties"].items() + }, + "required": schema["required"], + "type": schema["type"], + }, + } + + +def _format_tool_to_vertex_function(tool: BaseTool) -> FunctionDescription: + "Format tool into the Vertex function API." + if tool.args_schema: + schema = dereference_refs(tool.args_schema.schema()) + schema.pop("definitions", None) + + return { + "name": tool.name or schema["title"], + "description": tool.description or schema["description"], + "parameters": { + "properties": { + k: { + "type": v["type"], + "description": v.get("description"), + } + for k, v in schema["properties"].items() + }, + "required": schema["required"], + "type": schema["type"], + }, + } + else: + return { + "name": tool.name, + "description": tool.description, + "parameters": { + "properties": { + "__arg1": {"type": "string"}, + }, + "required": ["__arg1"], + "type": "object", + }, + } + + +def _format_tools_to_vertex_tool( + tools: List[Union[BaseTool, Type[BaseModel]]], +) -> List[VertexTool]: + "Format tool into the Vertex Tool instance." + function_declarations = [] + for tool in tools: + if isinstance(tool, BaseTool): + func = _format_tool_to_vertex_function(tool) + else: + func = _format_pydantic_to_vertex_function(tool) + function_declarations.append(FunctionDeclaration(**func)) + + return [VertexTool(function_declarations=function_declarations)] + + +class PydanticFunctionsOutputParser(BaseOutputParser): + """Parse an output as a pydantic object. + + This parser is used to parse the output of a ChatModel that uses + Google Vertex function format to invoke functions. + + The parser extracts the function call invocation and matches + them to the pydantic schema provided. + + An exception will be raised if the function call does not match + the provided schema. + + Example: + + ... code-block:: python + + message = AIMessage( + content="This is a test message", + additional_kwargs={ + "function_call": { + "name": "cookie", + "arguments": json.dumps({"name": "value", "age": 10}), + } + }, + ) + chat_generation = ChatGeneration(message=message) + + class Cookie(BaseModel): + name: str + age: int + + class Dog(BaseModel): + species: str + + # Full output + parser = PydanticOutputFunctionsParser( + pydantic_schema={"cookie": Cookie, "dog": Dog} + ) + result = parser.parse_result([chat_generation]) + """ + + pydantic_schema: Union[Type[BaseModel], Dict[str, Type[BaseModel]]] + + def parse_result( + self, result: List[Generation], *, partial: bool = False + ) -> BaseModel: + if not isinstance(result[0], ChatGeneration): + raise ValueError("This output parser only works on ChatGeneration output") + message = result[0].message + function_call = message.additional_kwargs.get("function_call", {}) + if function_call: + function_name = function_call["name"] + tool_input = function_call.get("arguments", {}) + if isinstance(self.pydantic_schema, dict): + schema = self.pydantic_schema[function_name] + else: + schema = self.pydantic_schema + return schema(**json.loads(tool_input)) + else: + raise OutputParserException(f"Could not parse function call: {message}") + + def parse(self, text: str) -> BaseModel: + raise ValueError("Can only parse messages") diff --git a/libs/partners/google-vertexai/langchain_google_vertexai/llms.py b/libs/partners/google-vertexai/langchain_google_vertexai/llms.py index c8905c99a6a8e..cfb5f17426c5e 100644 --- a/libs/partners/google-vertexai/langchain_google_vertexai/llms.py +++ b/libs/partners/google-vertexai/langchain_google_vertexai/llms.py @@ -26,7 +26,10 @@ from vertexai.language_models._language_models import ( # type: ignore TextGenerationResponse, ) -from vertexai.preview.generative_models import GenerativeModel, Image # type: ignore +from vertexai.preview.generative_models import ( # type: ignore + GenerativeModel, + Image, +) from vertexai.preview.language_models import ( # type: ignore CodeGenerationModel as PreviewCodeGenerationModel, ) @@ -34,13 +37,20 @@ TextGenerationModel as PreviewTextGenerationModel, ) +from langchain_google_vertexai._enums import HarmBlockThreshold, HarmCategory from langchain_google_vertexai._utils import ( create_retry_decorator, get_client_info, + get_generation_info, is_codey_model, is_gemini_model, ) +_PALM_DEFAULT_MAX_OUTPUT_TOKENS = TextGenerationModel._DEFAULT_MAX_OUTPUT_TOKENS +_PALM_DEFAULT_TEMPERATURE = 0.0 +_PALM_DEFAULT_TOP_P = 0.95 +_PALM_DEFAULT_TOP_K = 40 + def _completion_with_retry( llm: VertexAI, @@ -61,7 +71,10 @@ def _completion_with_retry_inner( ) -> Any: if is_gemini: return llm.client.generate_content( - prompt, stream=stream, generation_config=kwargs + prompt, + stream=stream, + safety_settings=kwargs.pop("safety_settings", None), + generation_config=kwargs, ) else: if stream: @@ -89,7 +102,9 @@ async def _acompletion_with_retry_inner( ) -> Any: if is_gemini: return await llm.client.generate_content_async( - prompt, generation_config=kwargs + prompt, + generation_config=kwargs, + safety_settings=kwargs.pop("safety_settings", None), ) return await llm.client.predict_async(prompt, **kwargs) @@ -118,14 +133,14 @@ class _VertexAICommon(_VertexAIBase): client_preview: Any = None #: :meta private: model_name: str "Underlying model name." - temperature: float = 0.0 + temperature: Optional[float] = None "Sampling temperature, it controls the degree of randomness in token selection." - max_output_tokens: int = 128 + max_output_tokens: Optional[int] = None "Token limit determines the maximum amount of text output from one prompt." - top_p: float = 0.95 + top_p: Optional[float] = None "Tokens are selected from most probable to least until the sum of their " "probabilities equals the top-p value. Top-p is ignored for Codey models." - top_k: int = 40 + top_k: Optional[int] = None "How the model selects tokens for output, the next token is selected from " "among the top-k most probable tokens. Top-k is ignored for Codey models." credentials: Any = Field(default=None, exclude=True) @@ -136,6 +151,21 @@ class _VertexAICommon(_VertexAIBase): """How many completions to generate for each prompt.""" streaming: bool = False """Whether to stream the results or not.""" + safety_settings: Optional[Dict[HarmCategory, HarmBlockThreshold]] = None + """The default safety settings to use for all generations. + + For example: + + from langchain_google_vertexai import HarmBlockThreshold, HarmCategory + + safety_settings = { + HarmCategory.HARM_CATEGORY_UNSPECIFIED: HarmBlockThreshold.BLOCK_NONE, + HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE, + HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_ONLY_HIGH, + HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE, + HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: HarmBlockThreshold.BLOCK_NONE, + } + """ # noqa: E501 @property def _llm_type(self) -> str: @@ -156,6 +186,15 @@ def _identifying_params(self) -> Dict[str, Any]: @property def _default_params(self) -> Dict[str, Any]: + if self._is_gemini_model: + default_params = {} + else: + default_params = { + "temperature": _PALM_DEFAULT_TEMPERATURE, + "max_output_tokens": _PALM_DEFAULT_MAX_OUTPUT_TOKENS, + "top_p": _PALM_DEFAULT_TOP_P, + "top_k": _PALM_DEFAULT_TOP_K, + } params = { "temperature": self.temperature, "max_output_tokens": self.max_output_tokens, @@ -168,7 +207,14 @@ def _default_params(self) -> Dict[str, Any]: "top_p": self.top_p, } ) - return params + updated_params = {} + for param_name, param_value in params.items(): + default_value = default_params.get(param_name) + if param_value or default_value: + updated_params[param_name] = ( + param_value if param_value else default_value + ) + return updated_params @classmethod def _init_vertexai(cls, values: Dict) -> None: @@ -202,14 +248,27 @@ class VertexAI(_VertexAICommon, BaseLLM): tuned_model_name: Optional[str] = None "The name of a tuned model. If provided, model_name is ignored." + @classmethod + def is_lc_serializable(self) -> bool: + return True + + @classmethod + def get_lc_namespace(cls) -> List[str]: + """Get the namespace of the langchain object.""" + return ["langchain", "llms", "vertexai"] + @root_validator() def validate_environment(cls, values: Dict) -> Dict: """Validate that the python package exists in environment.""" tuned_model_name = values.get("tuned_model_name") model_name = values["model_name"] + safety_settings = values["safety_settings"] is_gemini = is_gemini_model(values["model_name"]) cls._init_vertexai(values) + if safety_settings and (not is_gemini or tuned_model_name): + raise ValueError("Safety settings are only supported for Gemini models") + if is_codey_model(model_name): model_cls = CodeGenerationModel preview_model_cls = PreviewCodeGenerationModel @@ -227,8 +286,12 @@ def validate_environment(cls, values: Dict) -> Dict: ) else: if is_gemini: - values["client"] = model_cls(model_name=model_name) - values["client_preview"] = preview_model_cls(model_name=model_name) + values["client"] = model_cls( + model_name=model_name, safety_settings=safety_settings + ) + values["client_preview"] = preview_model_cls( + model_name=model_name, safety_settings=safety_settings + ) else: values["client"] = model_cls.from_pretrained(model_name) values["client_preview"] = preview_model_cls.from_pretrained(model_name) @@ -255,14 +318,17 @@ def _response_to_generation( self, response: TextGenerationResponse ) -> GenerationChunk: """Converts a stream response to a generation chunk.""" + generation_info = get_generation_info(response, self._is_gemini_model) try: - generation_info = { - "is_blocked": response.is_blocked, - "safety_attributes": response.safety_attributes, - } - except Exception: - generation_info = None - return GenerationChunk(text=response.text, generation_info=generation_info) + text = response.text + except AttributeError: + text = "" + except ValueError: + text = "" + return GenerationChunk( + text=text, + generation_info=generation_info, + ) def _generate( self, @@ -305,7 +371,7 @@ async def _agenerate( **kwargs: Any, ) -> LLMResult: params = self._prepare_params(stop=stop, **kwargs) - generations = [] + generations: List[List[Generation]] = [] for prompt in prompts: res = await _acompletion_with_retry( self, diff --git a/libs/partners/google-vertexai/poetry.lock b/libs/partners/google-vertexai/poetry.lock index 3fd6f04c24924..929bb2ea8b881 100644 --- a/libs/partners/google-vertexai/poetry.lock +++ b/libs/partners/google-vertexai/poetry.lock @@ -1,5 +1,115 @@ # This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +[[package]] +name = "aiohttp" +version = "3.9.1" +description = "Async http client/server framework (asyncio)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "aiohttp-3.9.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e1f80197f8b0b846a8d5cf7b7ec6084493950d0882cc5537fb7b96a69e3c8590"}, + {file = "aiohttp-3.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c72444d17777865734aa1a4d167794c34b63e5883abb90356a0364a28904e6c0"}, + {file = "aiohttp-3.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9b05d5cbe9dafcdc733262c3a99ccf63d2f7ce02543620d2bd8db4d4f7a22f83"}, + {file = "aiohttp-3.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c4fa235d534b3547184831c624c0b7c1e262cd1de847d95085ec94c16fddcd5"}, + {file = "aiohttp-3.9.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:289ba9ae8e88d0ba16062ecf02dd730b34186ea3b1e7489046fc338bdc3361c4"}, + {file = "aiohttp-3.9.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bff7e2811814fa2271be95ab6e84c9436d027a0e59665de60edf44e529a42c1f"}, + {file = "aiohttp-3.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81b77f868814346662c96ab36b875d7814ebf82340d3284a31681085c051320f"}, + {file = "aiohttp-3.9.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b9c7426923bb7bd66d409da46c41e3fb40f5caf679da624439b9eba92043fa6"}, + {file = "aiohttp-3.9.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8d44e7bf06b0c0a70a20f9100af9fcfd7f6d9d3913e37754c12d424179b4e48f"}, + {file = "aiohttp-3.9.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:22698f01ff5653fe66d16ffb7658f582a0ac084d7da1323e39fd9eab326a1f26"}, + {file = "aiohttp-3.9.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ca7ca5abfbfe8d39e653870fbe8d7710be7a857f8a8386fc9de1aae2e02ce7e4"}, + {file = "aiohttp-3.9.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:8d7f98fde213f74561be1d6d3fa353656197f75d4edfbb3d94c9eb9b0fc47f5d"}, + {file = "aiohttp-3.9.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5216b6082c624b55cfe79af5d538e499cd5f5b976820eac31951fb4325974501"}, + {file = "aiohttp-3.9.1-cp310-cp310-win32.whl", hash = "sha256:0e7ba7ff228c0d9a2cd66194e90f2bca6e0abca810b786901a569c0de082f489"}, + {file = "aiohttp-3.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:c7e939f1ae428a86e4abbb9a7c4732bf4706048818dfd979e5e2839ce0159f23"}, + {file = "aiohttp-3.9.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:df9cf74b9bc03d586fc53ba470828d7b77ce51b0582d1d0b5b2fb673c0baa32d"}, + {file = "aiohttp-3.9.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ecca113f19d5e74048c001934045a2b9368d77b0b17691d905af18bd1c21275e"}, + {file = "aiohttp-3.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8cef8710fb849d97c533f259103f09bac167a008d7131d7b2b0e3a33269185c0"}, + {file = "aiohttp-3.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bea94403a21eb94c93386d559bce297381609153e418a3ffc7d6bf772f59cc35"}, + {file = "aiohttp-3.9.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91c742ca59045dce7ba76cab6e223e41d2c70d79e82c284a96411f8645e2afff"}, + {file = "aiohttp-3.9.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6c93b7c2e52061f0925c3382d5cb8980e40f91c989563d3d32ca280069fd6a87"}, + {file = "aiohttp-3.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee2527134f95e106cc1653e9ac78846f3a2ec1004cf20ef4e02038035a74544d"}, + {file = "aiohttp-3.9.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11ff168d752cb41e8492817e10fb4f85828f6a0142b9726a30c27c35a1835f01"}, + {file = "aiohttp-3.9.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b8c3a67eb87394386847d188996920f33b01b32155f0a94f36ca0e0c635bf3e3"}, + {file = "aiohttp-3.9.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c7b5d5d64e2a14e35a9240b33b89389e0035e6de8dbb7ffa50d10d8b65c57449"}, + {file = "aiohttp-3.9.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:69985d50a2b6f709412d944ffb2e97d0be154ea90600b7a921f95a87d6f108a2"}, + {file = "aiohttp-3.9.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:c9110c06eaaac7e1f5562caf481f18ccf8f6fdf4c3323feab28a93d34cc646bd"}, + {file = "aiohttp-3.9.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d737e69d193dac7296365a6dcb73bbbf53bb760ab25a3727716bbd42022e8d7a"}, + {file = "aiohttp-3.9.1-cp311-cp311-win32.whl", hash = "sha256:4ee8caa925aebc1e64e98432d78ea8de67b2272252b0a931d2ac3bd876ad5544"}, + {file = "aiohttp-3.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:a34086c5cc285be878622e0a6ab897a986a6e8bf5b67ecb377015f06ed316587"}, + {file = "aiohttp-3.9.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f800164276eec54e0af5c99feb9494c295118fc10a11b997bbb1348ba1a52065"}, + {file = "aiohttp-3.9.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:500f1c59906cd142d452074f3811614be04819a38ae2b3239a48b82649c08821"}, + {file = "aiohttp-3.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0b0a6a36ed7e164c6df1e18ee47afbd1990ce47cb428739d6c99aaabfaf1b3af"}, + {file = "aiohttp-3.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69da0f3ed3496808e8cbc5123a866c41c12c15baaaead96d256477edf168eb57"}, + {file = "aiohttp-3.9.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:176df045597e674fa950bf5ae536be85699e04cea68fa3a616cf75e413737eb5"}, + {file = "aiohttp-3.9.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b796b44111f0cab6bbf66214186e44734b5baab949cb5fb56154142a92989aeb"}, + {file = "aiohttp-3.9.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f27fdaadce22f2ef950fc10dcdf8048407c3b42b73779e48a4e76b3c35bca26c"}, + {file = "aiohttp-3.9.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bcb6532b9814ea7c5a6a3299747c49de30e84472fa72821b07f5a9818bce0f66"}, + {file = "aiohttp-3.9.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:54631fb69a6e44b2ba522f7c22a6fb2667a02fd97d636048478db2fd8c4e98fe"}, + {file = "aiohttp-3.9.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4b4c452d0190c5a820d3f5c0f3cd8a28ace48c54053e24da9d6041bf81113183"}, + {file = "aiohttp-3.9.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:cae4c0c2ca800c793cae07ef3d40794625471040a87e1ba392039639ad61ab5b"}, + {file = "aiohttp-3.9.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:565760d6812b8d78d416c3c7cfdf5362fbe0d0d25b82fed75d0d29e18d7fc30f"}, + {file = "aiohttp-3.9.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:54311eb54f3a0c45efb9ed0d0a8f43d1bc6060d773f6973efd90037a51cd0a3f"}, + {file = "aiohttp-3.9.1-cp312-cp312-win32.whl", hash = "sha256:85c3e3c9cb1d480e0b9a64c658cd66b3cfb8e721636ab8b0e746e2d79a7a9eed"}, + {file = "aiohttp-3.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:11cb254e397a82efb1805d12561e80124928e04e9c4483587ce7390b3866d213"}, + {file = "aiohttp-3.9.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8a22a34bc594d9d24621091d1b91511001a7eea91d6652ea495ce06e27381f70"}, + {file = "aiohttp-3.9.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:598db66eaf2e04aa0c8900a63b0101fdc5e6b8a7ddd805c56d86efb54eb66672"}, + {file = "aiohttp-3.9.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2c9376e2b09895c8ca8b95362283365eb5c03bdc8428ade80a864160605715f1"}, + {file = "aiohttp-3.9.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41473de252e1797c2d2293804e389a6d6986ef37cbb4a25208de537ae32141dd"}, + {file = "aiohttp-3.9.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c5857612c9813796960c00767645cb5da815af16dafb32d70c72a8390bbf690"}, + {file = "aiohttp-3.9.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ffcd828e37dc219a72c9012ec44ad2e7e3066bec6ff3aaa19e7d435dbf4032ca"}, + {file = "aiohttp-3.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:219a16763dc0294842188ac8a12262b5671817042b35d45e44fd0a697d8c8361"}, + {file = "aiohttp-3.9.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f694dc8a6a3112059258a725a4ebe9acac5fe62f11c77ac4dcf896edfa78ca28"}, + {file = "aiohttp-3.9.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bcc0ea8d5b74a41b621ad4a13d96c36079c81628ccc0b30cfb1603e3dfa3a014"}, + {file = "aiohttp-3.9.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:90ec72d231169b4b8d6085be13023ece8fa9b1bb495e4398d847e25218e0f431"}, + {file = "aiohttp-3.9.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:cf2a0ac0615842b849f40c4d7f304986a242f1e68286dbf3bd7a835e4f83acfd"}, + {file = "aiohttp-3.9.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:0e49b08eafa4f5707ecfb321ab9592717a319e37938e301d462f79b4e860c32a"}, + {file = "aiohttp-3.9.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2c59e0076ea31c08553e868cec02d22191c086f00b44610f8ab7363a11a5d9d8"}, + {file = "aiohttp-3.9.1-cp38-cp38-win32.whl", hash = "sha256:4831df72b053b1eed31eb00a2e1aff6896fb4485301d4ccb208cac264b648db4"}, + {file = "aiohttp-3.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:3135713c5562731ee18f58d3ad1bf41e1d8883eb68b363f2ffde5b2ea4b84cc7"}, + {file = "aiohttp-3.9.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:cfeadf42840c1e870dc2042a232a8748e75a36b52d78968cda6736de55582766"}, + {file = "aiohttp-3.9.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:70907533db712f7aa791effb38efa96f044ce3d4e850e2d7691abd759f4f0ae0"}, + {file = "aiohttp-3.9.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cdefe289681507187e375a5064c7599f52c40343a8701761c802c1853a504558"}, + {file = "aiohttp-3.9.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7481f581251bb5558ba9f635db70908819caa221fc79ee52a7f58392778c636"}, + {file = "aiohttp-3.9.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:49f0c1b3c2842556e5de35f122fc0f0b721334ceb6e78c3719693364d4af8499"}, + {file = "aiohttp-3.9.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0d406b01a9f5a7e232d1b0d161b40c05275ffbcbd772dc18c1d5a570961a1ca4"}, + {file = "aiohttp-3.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d8e4450e7fe24d86e86b23cc209e0023177b6d59502e33807b732d2deb6975f"}, + {file = "aiohttp-3.9.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c0266cd6f005e99f3f51e583012de2778e65af6b73860038b968a0a8888487a"}, + {file = "aiohttp-3.9.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab221850108a4a063c5b8a70f00dd7a1975e5a1713f87f4ab26a46e5feac5a0e"}, + {file = "aiohttp-3.9.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c88a15f272a0ad3d7773cf3a37cc7b7d077cbfc8e331675cf1346e849d97a4e5"}, + {file = "aiohttp-3.9.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:237533179d9747080bcaad4d02083ce295c0d2eab3e9e8ce103411a4312991a0"}, + {file = "aiohttp-3.9.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:02ab6006ec3c3463b528374c4cdce86434e7b89ad355e7bf29e2f16b46c7dd6f"}, + {file = "aiohttp-3.9.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04fa38875e53eb7e354ece1607b1d2fdee2d175ea4e4d745f6ec9f751fe20c7c"}, + {file = "aiohttp-3.9.1-cp39-cp39-win32.whl", hash = "sha256:82eefaf1a996060602f3cc1112d93ba8b201dbf5d8fd9611227de2003dddb3b7"}, + {file = "aiohttp-3.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:9b05d33ff8e6b269e30a7957bd3244ffbce2a7a35a81b81c382629b80af1a8bf"}, + {file = "aiohttp-3.9.1.tar.gz", hash = "sha256:8fc49a87ac269d4529da45871e2ffb6874e87779c3d0e2ccd813c0899221239d"}, +] + +[package.dependencies] +aiosignal = ">=1.1.2" +async-timeout = {version = ">=4.0,<5.0", markers = "python_version < \"3.11\""} +attrs = ">=17.3.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +yarl = ">=1.0,<2.0" + +[package.extras] +speedups = ["Brotli", "aiodns", "brotlicffi"] + +[[package]] +name = "aiosignal" +version = "1.3.1" +description = "aiosignal: a list of registered asynchronous callbacks" +optional = false +python-versions = ">=3.7" +files = [ + {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, + {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, +] + +[package.dependencies] +frozenlist = ">=1.1.0" + [[package]] name = "annotated-types" version = "0.6.0" @@ -36,6 +146,36 @@ doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphin test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] trio = ["trio (>=0.23)"] +[[package]] +name = "async-timeout" +version = "4.0.3" +description = "Timeout context manager for asyncio programs" +optional = false +python-versions = ">=3.7" +files = [ + {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, + {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, +] + +[[package]] +name = "attrs" +version = "23.2.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.7" +files = [ + {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, + {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, +] + +[package.extras] +cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] +dev = ["attrs[tests]", "pre-commit"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] +tests = ["attrs[tests-no-zope]", "zope-interface"] +tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] +tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] + [[package]] name = "cachetools" version = "5.3.2" @@ -185,6 +325,21 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "dataclasses-json" +version = "0.6.3" +description = "Easily serialize dataclasses to and from JSON." +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "dataclasses_json-0.6.3-py3-none-any.whl", hash = "sha256:4aeb343357997396f6bca1acae64e486c3a723d8f5c76301888abeccf0c45176"}, + {file = "dataclasses_json-0.6.3.tar.gz", hash = "sha256:35cb40aae824736fdf959801356641836365219cfe14caeb115c39136f775d2a"}, +] + +[package.dependencies] +marshmallow = ">=3.18.0,<4.0.0" +typing-inspect = ">=0.4.0,<1" + [[package]] name = "exceptiongroup" version = "1.2.0" @@ -213,6 +368,92 @@ files = [ [package.dependencies] python-dateutil = ">=2.7" +[[package]] +name = "frozenlist" +version = "1.4.1" +description = "A list-like structure which implements collections.abc.MutableSequence" +optional = false +python-versions = ">=3.8" +files = [ + {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac"}, + {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868"}, + {file = "frozenlist-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc"}, + {file = "frozenlist-1.4.1-cp310-cp310-win32.whl", hash = "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1"}, + {file = "frozenlist-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439"}, + {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0"}, + {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49"}, + {file = "frozenlist-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2"}, + {file = "frozenlist-1.4.1-cp311-cp311-win32.whl", hash = "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17"}, + {file = "frozenlist-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825"}, + {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae"}, + {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb"}, + {file = "frozenlist-1.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8"}, + {file = "frozenlist-1.4.1-cp312-cp312-win32.whl", hash = "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89"}, + {file = "frozenlist-1.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5"}, + {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d"}, + {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826"}, + {file = "frozenlist-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7"}, + {file = "frozenlist-1.4.1-cp38-cp38-win32.whl", hash = "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497"}, + {file = "frozenlist-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09"}, + {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e"}, + {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d"}, + {file = "frozenlist-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6"}, + {file = "frozenlist-1.4.1-cp39-cp39-win32.whl", hash = "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932"}, + {file = "frozenlist-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0"}, + {file = "frozenlist-1.4.1-py3-none-any.whl", hash = "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7"}, + {file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"}, +] + [[package]] name = "google-api-core" version = "2.15.0" @@ -243,15 +484,33 @@ grpc = ["grpcio (>=1.33.2,<2.0dev)", "grpcio (>=1.49.1,<2.0dev)", "grpcio-status grpcgcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] +[[package]] +name = "google-api-python-client" +version = "2.114.0" +description = "Google API Client Library for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "google-api-python-client-2.114.0.tar.gz", hash = "sha256:e041bbbf60e682261281e9d64b4660035f04db1cccba19d1d68eebc24d1465ed"}, + {file = "google_api_python_client-2.114.0-py2.py3-none-any.whl", hash = "sha256:690e0bb67d70ff6dea4e8a5d3738639c105a478ac35da153d3b2a384064e9e1a"}, +] + +[package.dependencies] +google-api-core = ">=1.31.5,<2.0.dev0 || >2.3.0,<3.0.0.dev0" +google-auth = ">=1.19.0,<3.0.0.dev0" +google-auth-httplib2 = ">=0.1.0" +httplib2 = ">=0.15.0,<1.dev0" +uritemplate = ">=3.0.1,<5" + [[package]] name = "google-auth" -version = "2.26.1" +version = "2.26.2" description = "Google Authentication Library" optional = false python-versions = ">=3.7" files = [ - {file = "google-auth-2.26.1.tar.gz", hash = "sha256:54385acca5c0fbdda510cd8585ba6f3fcb06eeecf8a6ecca39d3ee148b092590"}, - {file = "google_auth-2.26.1-py2.py3-none-any.whl", hash = "sha256:2c8b55e3e564f298122a02ab7b97458ccfcc5617840beb5d0ac757ada92c9780"}, + {file = "google-auth-2.26.2.tar.gz", hash = "sha256:97327dbbf58cccb58fc5a1712bba403ae76668e64814eb30f7316f7e27126b81"}, + {file = "google_auth-2.26.2-py2.py3-none-any.whl", hash = "sha256:3f445c8ce9b61ed6459aad86d8ccdba4a9afed841b2d1451a11ef4db08957424"}, ] [package.dependencies] @@ -266,15 +525,30 @@ pyopenssl = ["cryptography (>=38.0.3)", "pyopenssl (>=20.0.0)"] reauth = ["pyu2f (>=0.1.5)"] requests = ["requests (>=2.20.0,<3.0.0.dev0)"] +[[package]] +name = "google-auth-httplib2" +version = "0.2.0" +description = "Google Authentication Library: httplib2 transport" +optional = false +python-versions = "*" +files = [ + {file = "google-auth-httplib2-0.2.0.tar.gz", hash = "sha256:38aa7badf48f974f1eb9861794e9c0cb2a0511a4ec0679b1f886d108f5640e05"}, + {file = "google_auth_httplib2-0.2.0-py2.py3-none-any.whl", hash = "sha256:b65a0a2123300dd71281a7bf6e64d65a0759287df52729bdd1ae2e47dc311a3d"}, +] + +[package.dependencies] +google-auth = "*" +httplib2 = ">=0.19.0" + [[package]] name = "google-cloud-aiplatform" -version = "1.38.1" +version = "1.39.0" description = "Vertex AI API client library" optional = false python-versions = ">=3.8" files = [ - {file = "google-cloud-aiplatform-1.38.1.tar.gz", hash = "sha256:30439d914bb028443c0506cc0e6dd825cff5401aeb8233e13d8cfd77c3c87da1"}, - {file = "google_cloud_aiplatform-1.38.1-py2.py3-none-any.whl", hash = "sha256:5e1fcd1068dd2c4f0fc89aa616e34a8b9434eaa72ea6216f5036ef26f08bd448"}, + {file = "google-cloud-aiplatform-1.39.0.tar.gz", hash = "sha256:62d6accbf9035895736910bc980f0b2a819d5841ae8bc0c981457cc16c49ecd1"}, + {file = "google_cloud_aiplatform-1.39.0-py2.py3-none-any.whl", hash = "sha256:d7b5c44fbb10d34c7941c5f7aadf7ff480c1469e37eac5b305bc9821fa49f7ee"}, ] [package.dependencies] @@ -293,7 +567,7 @@ autologging = ["mlflow (>=1.27.0,<=2.1.1)"] cloud-profiler = ["tensorboard-plugin-profile (>=2.4.0,<3.0.0dev)", "tensorflow (>=2.4.0,<3.0.0dev)", "werkzeug (>=2.0.0,<2.1.0dev)"] datasets = ["pyarrow (>=10.0.1)", "pyarrow (>=3.0.0,<8.0dev)"] endpoint = ["requests (>=2.28.1)"] -full = ["cloudpickle (<3.0)", "docker (>=5.0.3)", "explainable-ai-sdk (>=1.0.0)", "fastapi (>=0.71.0,<0.103.1)", "google-cloud-bigquery", "google-cloud-bigquery-storage", "google-cloud-logging (<4.0)", "google-vizier (==0.0.11)", "google-vizier (==0.0.4)", "google-vizier (>=0.0.14)", "google-vizier (>=0.1.6)", "httpx (>=0.23.0,<0.25.0)", "lit-nlp (==0.4.0)", "mlflow (>=1.27.0,<=2.1.1)", "numpy (>=1.15.0)", "pandas (>=1.0.0)", "pyarrow (>=10.0.1)", "pyarrow (>=3.0.0,<8.0dev)", "pyarrow (>=6.0.1)", "pydantic (<2)", "pyyaml (==5.3.1)", "ray[default] (>=2.4,<2.5)", "ray[default] (>=2.5,<2.5.1)", "requests (>=2.28.1)", "starlette (>=0.17.1)", "tensorflow (>=2.3.0,<3.0.0dev)", "urllib3 (>=1.21.1,<1.27)", "uvicorn[standard] (>=0.16.0)"] +full = ["cloudpickle (<3.0)", "docker (>=5.0.3)", "explainable-ai-sdk (>=1.0.0)", "fastapi (>=0.71.0,<0.103.1)", "google-cloud-bigquery", "google-cloud-bigquery-storage", "google-cloud-logging (<4.0)", "google-vizier (==0.0.11)", "google-vizier (==0.0.4)", "google-vizier (>=0.0.14)", "google-vizier (>=0.1.6)", "httpx (>=0.23.0,<0.25.0)", "lit-nlp (==0.4.0)", "mlflow (>=1.27.0,<=2.1.1)", "numpy (>=1.15.0)", "pandas (>=1.0.0)", "pyarrow (>=10.0.1)", "pyarrow (>=3.0.0,<8.0dev)", "pyarrow (>=6.0.1)", "pydantic (<2)", "pyyaml (==5.3.1)", "ray[default] (>=2.4,<2.5)", "ray[default] (>=2.5,<2.5.1)", "requests (>=2.28.1)", "starlette (>=0.17.1)", "tensorflow (>=2.3.0,<2.15.0)", "tensorflow (>=2.3.0,<3.0.0dev)", "urllib3 (>=1.21.1,<1.27)", "uvicorn[standard] (>=0.16.0)"] lit = ["explainable-ai-sdk (>=1.0.0)", "lit-nlp (==0.4.0)", "pandas (>=1.0.0)", "tensorflow (>=2.3.0,<3.0.0dev)"] metadata = ["numpy (>=1.15.0)", "pandas (>=1.0.0)"] pipelines = ["pyyaml (==5.3.1)"] @@ -301,20 +575,20 @@ prediction = ["docker (>=5.0.3)", "fastapi (>=0.71.0,<0.103.1)", "httpx (>=0.23. preview = ["cloudpickle (<3.0)", "google-cloud-logging (<4.0)"] private-endpoints = ["requests (>=2.28.1)", "urllib3 (>=1.21.1,<1.27)"] ray = ["google-cloud-bigquery", "google-cloud-bigquery-storage", "pandas (>=1.0.0)", "pyarrow (>=6.0.1)", "pydantic (<2)", "ray[default] (>=2.4,<2.5)", "ray[default] (>=2.5,<2.5.1)"] -tensorboard = ["tensorflow (>=2.3.0,<3.0.0dev)"] -testing = ["bigframes", "cloudpickle (<3.0)", "docker (>=5.0.3)", "explainable-ai-sdk (>=1.0.0)", "fastapi (>=0.71.0,<0.103.1)", "google-cloud-bigquery", "google-cloud-bigquery-storage", "google-cloud-logging (<4.0)", "google-vizier (==0.0.11)", "google-vizier (==0.0.4)", "google-vizier (>=0.0.14)", "google-vizier (>=0.1.6)", "grpcio-testing", "httpx (>=0.23.0,<0.25.0)", "ipython", "kfp", "lit-nlp (==0.4.0)", "mlflow (>=1.27.0,<=2.1.1)", "numpy (>=1.15.0)", "pandas (>=1.0.0)", "pyarrow (>=10.0.1)", "pyarrow (>=3.0.0,<8.0dev)", "pyarrow (>=6.0.1)", "pydantic (<2)", "pyfakefs", "pytest-asyncio", "pytest-xdist", "pyyaml (==5.3.1)", "ray[default] (>=2.4,<2.5)", "ray[default] (>=2.5,<2.5.1)", "requests (>=2.28.1)", "requests-toolbelt (<1.0.0)", "scikit-learn", "starlette (>=0.17.1)", "tensorboard-plugin-profile (>=2.4.0,<3.0.0dev)", "tensorflow (>=2.3.0,<3.0.0dev)", "tensorflow (>=2.3.0,<=2.12.0)", "tensorflow (>=2.4.0,<3.0.0dev)", "torch (>=2.0.0,<2.1.0)", "urllib3 (>=1.21.1,<1.27)", "uvicorn[standard] (>=0.16.0)", "werkzeug (>=2.0.0,<2.1.0dev)", "xgboost", "xgboost-ray"] +tensorboard = ["tensorflow (>=2.3.0,<2.15.0)"] +testing = ["bigframes", "cloudpickle (<3.0)", "docker (>=5.0.3)", "explainable-ai-sdk (>=1.0.0)", "fastapi (>=0.71.0,<0.103.1)", "google-cloud-bigquery", "google-cloud-bigquery-storage", "google-cloud-logging (<4.0)", "google-vizier (==0.0.11)", "google-vizier (==0.0.4)", "google-vizier (>=0.0.14)", "google-vizier (>=0.1.6)", "grpcio-testing", "httpx (>=0.23.0,<0.25.0)", "ipython", "kfp", "lit-nlp (==0.4.0)", "mlflow (>=1.27.0,<=2.1.1)", "numpy (>=1.15.0)", "pandas (>=1.0.0)", "pyarrow (>=10.0.1)", "pyarrow (>=3.0.0,<8.0dev)", "pyarrow (>=6.0.1)", "pydantic (<2)", "pyfakefs", "pytest-asyncio", "pytest-xdist", "pyyaml (==5.3.1)", "ray[default] (>=2.4,<2.5)", "ray[default] (>=2.5,<2.5.1)", "requests (>=2.28.1)", "requests-toolbelt (<1.0.0)", "scikit-learn", "starlette (>=0.17.1)", "tensorboard-plugin-profile (>=2.4.0,<3.0.0dev)", "tensorflow (>=2.3.0,<2.15.0)", "tensorflow (>=2.3.0,<3.0.0dev)", "tensorflow (>=2.3.0,<=2.12.0)", "tensorflow (>=2.4.0,<3.0.0dev)", "torch (>=2.0.0,<2.1.0)", "urllib3 (>=1.21.1,<1.27)", "uvicorn[standard] (>=0.16.0)", "werkzeug (>=2.0.0,<2.1.0dev)", "xgboost", "xgboost-ray"] vizier = ["google-vizier (==0.0.11)", "google-vizier (==0.0.4)", "google-vizier (>=0.0.14)", "google-vizier (>=0.1.6)"] xai = ["tensorflow (>=2.3.0,<3.0.0dev)"] [[package]] name = "google-cloud-bigquery" -version = "3.14.1" +version = "3.16.0" description = "Google BigQuery API client library" optional = false python-versions = ">=3.7" files = [ - {file = "google-cloud-bigquery-3.14.1.tar.gz", hash = "sha256:aa15bd86f79ea76824c7d710f5ae532323c4b3ba01ef4abff42d4ee7a2e9b142"}, - {file = "google_cloud_bigquery-3.14.1-py2.py3-none-any.whl", hash = "sha256:a8ded18455da71508db222b7c06197bc12b6dbc6ed5b0b64e7007b76d7016957"}, + {file = "google-cloud-bigquery-3.16.0.tar.gz", hash = "sha256:1d6abf4b1d740df17cb43a078789872af8059a0b1dd999f32ea69ebc6f7ba7ef"}, + {file = "google_cloud_bigquery-3.16.0-py2.py3-none-any.whl", hash = "sha256:8bac7754f92bf87ee81f38deabb7554d82bb9591fbe06a5c82f33e46e5a482f9"}, ] [package.dependencies] @@ -509,6 +783,77 @@ protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.1 || >4.21.1,<4 [package.extras] grpc = ["grpcio (>=1.44.0,<2.0.0.dev0)"] +[[package]] +name = "greenlet" +version = "3.0.3" +description = "Lightweight in-process concurrent programming" +optional = false +python-versions = ">=3.7" +files = [ + {file = "greenlet-3.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405"}, + {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f"}, + {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb"}, + {file = "greenlet-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9"}, + {file = "greenlet-3.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22"}, + {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3"}, + {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d"}, + {file = "greenlet-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728"}, + {file = "greenlet-3.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf"}, + {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305"}, + {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6"}, + {file = "greenlet-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2"}, + {file = "greenlet-3.0.3-cp37-cp37m-macosx_11_0_universal2.whl", hash = "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41"}, + {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7"}, + {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6"}, + {file = "greenlet-3.0.3-cp37-cp37m-win32.whl", hash = "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d"}, + {file = "greenlet-3.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67"}, + {file = "greenlet-3.0.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4"}, + {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5"}, + {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da"}, + {file = "greenlet-3.0.3-cp38-cp38-win32.whl", hash = "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3"}, + {file = "greenlet-3.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf"}, + {file = "greenlet-3.0.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b"}, + {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6"}, + {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113"}, + {file = "greenlet-3.0.3-cp39-cp39-win32.whl", hash = "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e"}, + {file = "greenlet-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067"}, + {file = "greenlet-3.0.3.tar.gz", hash = "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491"}, +] + +[package.extras] +docs = ["Sphinx", "furo"] +test = ["objgraph", "psutil"] + [[package]] name = "grpc-google-iam-v1" version = "0.13.0" @@ -607,6 +952,20 @@ googleapis-common-protos = ">=1.5.5" grpcio = ">=1.60.0" protobuf = ">=4.21.6" +[[package]] +name = "httplib2" +version = "0.22.0" +description = "A comprehensive HTTP client library." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "httplib2-0.22.0-py3-none-any.whl", hash = "sha256:14ae0a53c1ba8f3d37e9e27cf37eabb0fb9980f435ba405d546948b009dd64dc"}, + {file = "httplib2-0.22.0.tar.gz", hash = "sha256:d7a10bc5ef5ab08322488bde8c726eeee5c8618723fdb399597ec58f3d82df81"}, +] + +[package.dependencies] +pyparsing = {version = ">=2.4.2,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.0.2 || >3.0.2,<3.0.3 || >3.0.3,<4", markers = "python_version > \"3.0\""} + [[package]] name = "idna" version = "3.6" @@ -654,9 +1013,78 @@ files = [ {file = "jsonpointer-2.4.tar.gz", hash = "sha256:585cee82b70211fa9e6043b7bb89db6e1aa49524340dde8ad6b63206ea689d88"}, ] +[[package]] +name = "langchain" +version = "0.1.1" +description = "Building applications with LLMs through composability" +optional = false +python-versions = ">=3.8.1,<4.0" +files = [] +develop = false + +[package.dependencies] +aiohttp = "^3.8.3" +async-timeout = {version = "^4.0.0", markers = "python_version < \"3.11\""} +dataclasses-json = ">= 0.5.7, < 0.7" +jsonpatch = "^1.33" +langchain-community = ">=0.0.13,<0.1" +langchain-core = ">=0.1.9,<0.2" +langsmith = "~0.0.77" +numpy = "^1" +pydantic = ">=1,<3" +PyYAML = ">=5.3" +requests = "^2" +SQLAlchemy = ">=1.4,<3" +tenacity = "^8.1.0" + +[package.extras] +all = [] +azure = ["azure-ai-formrecognizer (>=3.2.1,<4.0.0)", "azure-ai-textanalytics (>=5.3.0,<6.0.0)", "azure-ai-vision (>=0.11.1b1,<0.12.0)", "azure-cognitiveservices-speech (>=1.28.0,<2.0.0)", "azure-core (>=1.26.4,<2.0.0)", "azure-cosmos (>=4.4.0b1,<5.0.0)", "azure-identity (>=1.12.0,<2.0.0)", "azure-search-documents (==11.4.0b8)", "openai (<2)"] +clarifai = ["clarifai (>=9.1.0)"] +cli = ["typer (>=0.9.0,<0.10.0)"] +cohere = ["cohere (>=4,<5)"] +docarray = ["docarray[hnswlib] (>=0.32.0,<0.33.0)"] +embeddings = ["sentence-transformers (>=2,<3)"] +extended-testing = ["aiosqlite (>=0.19.0,<0.20.0)", "aleph-alpha-client (>=2.15.0,<3.0.0)", "anthropic (>=0.3.11,<0.4.0)", "arxiv (>=1.4,<2.0)", "assemblyai (>=0.17.0,<0.18.0)", "atlassian-python-api (>=3.36.0,<4.0.0)", "beautifulsoup4 (>=4,<5)", "bibtexparser (>=1.4.0,<2.0.0)", "cassio (>=0.1.0,<0.2.0)", "chardet (>=5.1.0,<6.0.0)", "cohere (>=4,<5)", "couchbase (>=4.1.9,<5.0.0)", "dashvector (>=1.0.1,<2.0.0)", "databricks-vectorsearch (>=0.21,<0.22)", "datasets (>=2.15.0,<3.0.0)", "dgml-utils (>=0.3.0,<0.4.0)", "esprima (>=4.0.1,<5.0.0)", "faiss-cpu (>=1,<2)", "feedparser (>=6.0.10,<7.0.0)", "fireworks-ai (>=0.9.0,<0.10.0)", "geopandas (>=0.13.1,<0.14.0)", "gitpython (>=3.1.32,<4.0.0)", "google-cloud-documentai (>=2.20.1,<3.0.0)", "gql (>=3.4.1,<4.0.0)", "hologres-vector (>=0.0.6,<0.0.7)", "html2text (>=2020.1.16,<2021.0.0)", "javelin-sdk (>=0.1.8,<0.2.0)", "jinja2 (>=3,<4)", "jq (>=1.4.1,<2.0.0)", "jsonschema (>1)", "langchain-openai (>=0.0.2,<0.1)", "lxml (>=4.9.2,<5.0.0)", "markdownify (>=0.11.6,<0.12.0)", "motor (>=3.3.1,<4.0.0)", "msal (>=1.25.0,<2.0.0)", "mwparserfromhell (>=0.6.4,<0.7.0)", "mwxml (>=0.3.3,<0.4.0)", "newspaper3k (>=0.2.8,<0.3.0)", "numexpr (>=2.8.6,<3.0.0)", "openai (<2)", "openai (<2)", "openapi-pydantic (>=0.3.2,<0.4.0)", "pandas (>=2.0.1,<3.0.0)", "pdfminer-six (>=20221105,<20221106)", "pgvector (>=0.1.6,<0.2.0)", "praw (>=7.7.1,<8.0.0)", "psychicapi (>=0.8.0,<0.9.0)", "py-trello (>=0.19.0,<0.20.0)", "pymupdf (>=1.22.3,<2.0.0)", "pypdf (>=3.4.0,<4.0.0)", "pypdfium2 (>=4.10.0,<5.0.0)", "pyspark (>=3.4.0,<4.0.0)", "rank-bm25 (>=0.2.2,<0.3.0)", "rapidfuzz (>=3.1.1,<4.0.0)", "rapidocr-onnxruntime (>=1.3.2,<2.0.0)", "requests-toolbelt (>=1.0.0,<2.0.0)", "rspace_client (>=2.5.0,<3.0.0)", "scikit-learn (>=1.2.2,<2.0.0)", "sqlite-vss (>=0.1.2,<0.2.0)", "streamlit (>=1.18.0,<2.0.0)", "sympy (>=1.12,<2.0)", "telethon (>=1.28.5,<2.0.0)", "timescale-vector (>=0.0.1,<0.0.2)", "tqdm (>=4.48.0)", "upstash-redis (>=0.15.0,<0.16.0)", "xata (>=1.0.0a7,<2.0.0)", "xmltodict (>=0.13.0,<0.14.0)"] +javascript = ["esprima (>=4.0.1,<5.0.0)"] +llms = ["clarifai (>=9.1.0)", "cohere (>=4,<5)", "huggingface_hub (>=0,<1)", "manifest-ml (>=0.0.1,<0.0.2)", "nlpcloud (>=1,<2)", "openai (<2)", "openlm (>=0.0.5,<0.0.6)", "torch (>=1,<3)", "transformers (>=4,<5)"] +openai = ["openai (<2)", "tiktoken (>=0.3.2,<0.6.0)"] +qdrant = ["qdrant-client (>=1.3.1,<2.0.0)"] +text-helpers = ["chardet (>=5.1.0,<6.0.0)"] + +[package.source] +type = "directory" +url = "../../langchain" + +[[package]] +name = "langchain-community" +version = "0.0.13" +description = "Community contributed LangChain integrations." +optional = false +python-versions = ">=3.8.1,<4.0" +files = [ + {file = "langchain_community-0.0.13-py3-none-any.whl", hash = "sha256:655196e446e7f37f4882221b6f3f791d6add28ea596d521ccf6f4507386b9a13"}, + {file = "langchain_community-0.0.13.tar.gz", hash = "sha256:cf66c6ff7fcbeb582f5e88ee00decea7fdeca5ccddda725046f28efc697c41a7"}, +] + +[package.dependencies] +aiohttp = ">=3.8.3,<4.0.0" +dataclasses-json = ">=0.5.7,<0.7" +langchain-core = ">=0.1.9,<0.2" +langsmith = ">=0.0.63,<0.1.0" +numpy = ">=1,<2" +PyYAML = ">=5.3" +requests = ">=2,<3" +SQLAlchemy = ">=1.4,<3" +tenacity = ">=8.1.0,<9.0.0" + +[package.extras] +cli = ["typer (>=0.9.0,<0.10.0)"] +extended-testing = ["aiosqlite (>=0.19.0,<0.20.0)", "aleph-alpha-client (>=2.15.0,<3.0.0)", "anthropic (>=0.3.11,<0.4.0)", "arxiv (>=1.4,<2.0)", "assemblyai (>=0.17.0,<0.18.0)", "atlassian-python-api (>=3.36.0,<4.0.0)", "azure-ai-documentintelligence (>=1.0.0b1,<2.0.0)", "beautifulsoup4 (>=4,<5)", "bibtexparser (>=1.4.0,<2.0.0)", "cassio (>=0.1.0,<0.2.0)", "chardet (>=5.1.0,<6.0.0)", "cohere (>=4,<5)", "dashvector (>=1.0.1,<2.0.0)", "databricks-vectorsearch (>=0.21,<0.22)", "datasets (>=2.15.0,<3.0.0)", "dgml-utils (>=0.3.0,<0.4.0)", "esprima (>=4.0.1,<5.0.0)", "faiss-cpu (>=1,<2)", "feedparser (>=6.0.10,<7.0.0)", "fireworks-ai (>=0.9.0,<0.10.0)", "geopandas (>=0.13.1,<0.14.0)", "gitpython (>=3.1.32,<4.0.0)", "google-cloud-documentai (>=2.20.1,<3.0.0)", "gql (>=3.4.1,<4.0.0)", "gradientai (>=1.4.0,<2.0.0)", "hologres-vector (>=0.0.6,<0.0.7)", "html2text (>=2020.1.16,<2021.0.0)", "javelin-sdk (>=0.1.8,<0.2.0)", "jinja2 (>=3,<4)", "jq (>=1.4.1,<2.0.0)", "jsonschema (>1)", "lxml (>=4.9.2,<5.0.0)", "markdownify (>=0.11.6,<0.12.0)", "motor (>=3.3.1,<4.0.0)", "msal (>=1.25.0,<2.0.0)", "mwparserfromhell (>=0.6.4,<0.7.0)", "mwxml (>=0.3.3,<0.4.0)", "newspaper3k (>=0.2.8,<0.3.0)", "numexpr (>=2.8.6,<3.0.0)", "openai (<2)", "openapi-pydantic (>=0.3.2,<0.4.0)", "oracle-ads (>=2.9.1,<3.0.0)", "pandas (>=2.0.1,<3.0.0)", "pdfminer-six (>=20221105,<20221106)", "pgvector (>=0.1.6,<0.2.0)", "praw (>=7.7.1,<8.0.0)", "psychicapi (>=0.8.0,<0.9.0)", "py-trello (>=0.19.0,<0.20.0)", "pymupdf (>=1.22.3,<2.0.0)", "pypdf (>=3.4.0,<4.0.0)", "pypdfium2 (>=4.10.0,<5.0.0)", "pyspark (>=3.4.0,<4.0.0)", "rank-bm25 (>=0.2.2,<0.3.0)", "rapidfuzz (>=3.1.1,<4.0.0)", "rapidocr-onnxruntime (>=1.3.2,<2.0.0)", "requests-toolbelt (>=1.0.0,<2.0.0)", "rspace_client (>=2.5.0,<3.0.0)", "scikit-learn (>=1.2.2,<2.0.0)", "sqlite-vss (>=0.1.2,<0.2.0)", "streamlit (>=1.18.0,<2.0.0)", "sympy (>=1.12,<2.0)", "telethon (>=1.28.5,<2.0.0)", "timescale-vector (>=0.0.1,<0.0.2)", "tqdm (>=4.48.0)", "upstash-redis (>=0.15.0,<0.16.0)", "xata (>=1.0.0a7,<2.0.0)", "xmltodict (>=0.13.0,<0.14.0)", "zhipuai (>=1.0.7,<2.0.0)"] + [[package]] name = "langchain-core" -version = "0.1.6" +version = "0.1.12" description = "Building applications with LLMs through composability" optional = false python-versions = ">=3.8.1,<4.0" @@ -682,19 +1110,122 @@ url = "../../core" [[package]] name = "langsmith" -version = "0.0.77" +version = "0.0.81" description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform." optional = false python-versions = ">=3.8.1,<4.0" files = [ - {file = "langsmith-0.0.77-py3-none-any.whl", hash = "sha256:750c0aa9177240c64e131d831e009ed08dd59038f7cabbd0bbcf62ccb7c8dcac"}, - {file = "langsmith-0.0.77.tar.gz", hash = "sha256:c4c8d3a96ad8671a41064f3ccc673e2e22a4153e823b19f915c9c9b8a4f33a2c"}, + {file = "langsmith-0.0.81-py3-none-any.whl", hash = "sha256:eb816ad456776ec4c6005ddce8a4c315a1a582ed4d079979888e9f8a1db209b3"}, + {file = "langsmith-0.0.81.tar.gz", hash = "sha256:5838e5a4bb1939e9794eb3f802f7c390247a847bd603e31442be5be00068e504"}, ] [package.dependencies] pydantic = ">=1,<3" requests = ">=2,<3" +[[package]] +name = "marshmallow" +version = "3.20.2" +description = "A lightweight library for converting complex datatypes to and from native Python datatypes." +optional = false +python-versions = ">=3.8" +files = [ + {file = "marshmallow-3.20.2-py3-none-any.whl", hash = "sha256:c21d4b98fee747c130e6bc8f45c4b3199ea66bc00c12ee1f639f0aeca034d5e9"}, + {file = "marshmallow-3.20.2.tar.gz", hash = "sha256:4c1daff273513dc5eb24b219a8035559dc573c8f322558ef85f5438ddd1236dd"}, +] + +[package.dependencies] +packaging = ">=17.0" + +[package.extras] +dev = ["pre-commit (>=2.4,<4.0)", "pytest", "pytz", "simplejson", "tox"] +docs = ["alabaster (==0.7.15)", "autodocsumm (==0.2.12)", "sphinx (==7.2.6)", "sphinx-issues (==3.0.1)", "sphinx-version-warning (==1.1.2)"] +lint = ["pre-commit (>=2.4,<4.0)"] +tests = ["pytest", "pytz", "simplejson"] + +[[package]] +name = "multidict" +version = "6.0.4" +description = "multidict implementation" +optional = false +python-versions = ">=3.7" +files = [ + {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8"}, + {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171"}, + {file = "multidict-6.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c048099e4c9e9d615545e2001d3d8a4380bd403e1a0578734e0d31703d1b0c0b"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea20853c6dbbb53ed34cb4d080382169b6f4554d394015f1bef35e881bf83547"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16d232d4e5396c2efbbf4f6d4df89bfa905eb0d4dc5b3549d872ab898451f569"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36c63aaa167f6c6b04ef2c85704e93af16c11d20de1d133e39de6a0e84582a93"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64bdf1086b6043bf519869678f5f2757f473dee970d7abf6da91ec00acb9cb98"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:43644e38f42e3af682690876cff722d301ac585c5b9e1eacc013b7a3f7b696a0"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7582a1d1030e15422262de9f58711774e02fa80df0d1578995c76214f6954988"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ddff9c4e225a63a5afab9dd15590432c22e8057e1a9a13d28ed128ecf047bbdc"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ee2a1ece51b9b9e7752e742cfb661d2a29e7bcdba2d27e66e28a99f1890e4fa0"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a2e4369eb3d47d2034032a26c7a80fcb21a2cb22e1173d761a162f11e562caa5"}, + {file = "multidict-6.0.4-cp310-cp310-win32.whl", hash = "sha256:574b7eae1ab267e5f8285f0fe881f17efe4b98c39a40858247720935b893bba8"}, + {file = "multidict-6.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:4dcbb0906e38440fa3e325df2359ac6cb043df8e58c965bb45f4e406ecb162cc"}, + {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0dfad7a5a1e39c53ed00d2dd0c2e36aed4650936dc18fd9a1826a5ae1cad6f03"}, + {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:64da238a09d6039e3bd39bb3aee9c21a5e34f28bfa5aa22518581f910ff94af3"}, + {file = "multidict-6.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff959bee35038c4624250473988b24f846cbeb2c6639de3602c073f10410ceba"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5cb09abb18c1ea940fb99360ea0396f34d46566f157122c92dfa069d3e0e982"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666daae833559deb2d609afa4490b85830ab0dfca811a98b70a205621a6109fe"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11bdf3f5e1518b24530b8241529d2050014c884cf18b6fc69c0c2b30ca248710"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d18748f2d30f94f498e852c67d61261c643b349b9d2a581131725595c45ec6c"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:458f37be2d9e4c95e2d8866a851663cbc76e865b78395090786f6cd9b3bbf4f4"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b1a2eeedcead3a41694130495593a559a668f382eee0727352b9a41e1c45759a"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7d6ae9d593ef8641544d6263c7fa6408cc90370c8cb2bbb65f8d43e5b0351d9c"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5979b5632c3e3534e42ca6ff856bb24b2e3071b37861c2c727ce220d80eee9ed"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dcfe792765fab89c365123c81046ad4103fcabbc4f56d1c1997e6715e8015461"}, + {file = "multidict-6.0.4-cp311-cp311-win32.whl", hash = "sha256:3601a3cece3819534b11d4efc1eb76047488fddd0c85a3948099d5da4d504636"}, + {file = "multidict-6.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:81a4f0b34bd92df3da93315c6a59034df95866014ac08535fc819f043bfd51f0"}, + {file = "multidict-6.0.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:67040058f37a2a51ed8ea8f6b0e6ee5bd78ca67f169ce6122f3e2ec80dfe9b78"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:853888594621e6604c978ce2a0444a1e6e70c8d253ab65ba11657659dcc9100f"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:39ff62e7d0f26c248b15e364517a72932a611a9b75f35b45be078d81bdb86603"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af048912e045a2dc732847d33821a9d84ba553f5c5f028adbd364dd4765092ac"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1e8b901e607795ec06c9e42530788c45ac21ef3aaa11dbd0c69de543bfb79a9"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62501642008a8b9871ddfccbf83e4222cf8ac0d5aeedf73da36153ef2ec222d2"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:99b76c052e9f1bc0721f7541e5e8c05db3941eb9ebe7b8553c625ef88d6eefde"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:509eac6cf09c794aa27bcacfd4d62c885cce62bef7b2c3e8b2e49d365b5003fe"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:21a12c4eb6ddc9952c415f24eef97e3e55ba3af61f67c7bc388dcdec1404a067"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:5cad9430ab3e2e4fa4a2ef4450f548768400a2ac635841bc2a56a2052cdbeb87"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab55edc2e84460694295f401215f4a58597f8f7c9466faec545093045476327d"}, + {file = "multidict-6.0.4-cp37-cp37m-win32.whl", hash = "sha256:5a4dcf02b908c3b8b17a45fb0f15b695bf117a67b76b7ad18b73cf8e92608775"}, + {file = "multidict-6.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:6ed5f161328b7df384d71b07317f4d8656434e34591f20552c7bcef27b0ab88e"}, + {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5fc1b16f586f049820c5c5b17bb4ee7583092fa0d1c4e28b5239181ff9532e0c"}, + {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1502e24330eb681bdaa3eb70d6358e818e8e8f908a22a1851dfd4e15bc2f8161"}, + {file = "multidict-6.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b692f419760c0e65d060959df05f2a531945af31fda0c8a3b3195d4efd06de11"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45e1ecb0379bfaab5eef059f50115b54571acfbe422a14f668fc8c27ba410e7e"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddd3915998d93fbcd2566ddf9cf62cdb35c9e093075f862935573d265cf8f65d"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:59d43b61c59d82f2effb39a93c48b845efe23a3852d201ed2d24ba830d0b4cf2"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc8e1d0c705233c5dd0c5e6460fbad7827d5d36f310a0fadfd45cc3029762258"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6aa0418fcc838522256761b3415822626f866758ee0bc6632c9486b179d0b52"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6748717bb10339c4760c1e63da040f5f29f5ed6e59d76daee30305894069a660"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4d1a3d7ef5e96b1c9e92f973e43aa5e5b96c659c9bc3124acbbd81b0b9c8a951"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4372381634485bec7e46718edc71528024fcdc6f835baefe517b34a33c731d60"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:fc35cb4676846ef752816d5be2193a1e8367b4c1397b74a565a9d0389c433a1d"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b9d9e4e2b37daddb5c23ea33a3417901fa7c7b3dee2d855f63ee67a0b21e5b1"}, + {file = "multidict-6.0.4-cp38-cp38-win32.whl", hash = "sha256:e41b7e2b59679edfa309e8db64fdf22399eec4b0b24694e1b2104fb789207779"}, + {file = "multidict-6.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:d6c254ba6e45d8e72739281ebc46ea5eb5f101234f3ce171f0e9f5cc86991480"}, + {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:16ab77bbeb596e14212e7bab8429f24c1579234a3a462105cda4a66904998664"}, + {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc779e9e6f7fda81b3f9aa58e3a6091d49ad528b11ed19f6621408806204ad35"}, + {file = "multidict-6.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ceef517eca3e03c1cceb22030a3e39cb399ac86bff4e426d4fc6ae49052cc60"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:281af09f488903fde97923c7744bb001a9b23b039a909460d0f14edc7bf59706"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52f2dffc8acaba9a2f27174c41c9e57f60b907bb9f096b36b1a1f3be71c6284d"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b41156839806aecb3641f3208c0dafd3ac7775b9c4c422d82ee2a45c34ba81ca"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3fc56f88cc98ef8139255cf8cd63eb2c586531e43310ff859d6bb3a6b51f1"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8316a77808c501004802f9beebde51c9f857054a0c871bd6da8280e718444449"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f70b98cd94886b49d91170ef23ec5c0e8ebb6f242d734ed7ed677b24d50c82cf"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bf6774e60d67a9efe02b3616fee22441d86fab4c6d335f9d2051d19d90a40063"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e69924bfcdda39b722ef4d9aa762b2dd38e4632b3641b1d9a57ca9cd18f2f83a"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:6b181d8c23da913d4ff585afd1155a0e1194c0b50c54fcfe286f70cdaf2b7176"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:52509b5be062d9eafc8170e53026fbc54cf3b32759a23d07fd935fb04fc22d95"}, + {file = "multidict-6.0.4-cp39-cp39-win32.whl", hash = "sha256:27c523fbfbdfd19c6867af7346332b62b586eed663887392cff78d614f9ec313"}, + {file = "multidict-6.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2"}, + {file = "multidict-6.0.4.tar.gz", hash = "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49"}, +] + [[package]] name = "mypy" version = "0.991" @@ -756,6 +1287,47 @@ files = [ {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, ] +[[package]] +name = "numexpr" +version = "2.8.8" +description = "Fast numerical expression evaluator for NumPy" +optional = false +python-versions = ">=3.9" +files = [ + {file = "numexpr-2.8.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85c9f79e346c26aa0d425ecfc9e5de7184567d5e48d0bdb02d468bb927e92525"}, + {file = "numexpr-2.8.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dbac846f713b4c82333e6af0814ebea0b4e74dfb2649e76c58953fd4862322dd"}, + {file = "numexpr-2.8.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d7bfc8b77d8a7b04cd64ae42b62b3bf824a8c751ca235692bfd5231c6e90127"}, + {file = "numexpr-2.8.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:307b49fd15ef2ca292f381e67759e5b477410341f2f499a377234f1b42f529a6"}, + {file = "numexpr-2.8.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:aab17d65751c039d13ed9d49c9a7517b130ef488c1885c4666af9b5c6ad59520"}, + {file = "numexpr-2.8.8-cp310-cp310-win32.whl", hash = "sha256:6459dc6ed6abcdeab3cd3667c79f29e4a0f0a02c29ad71ee5cff065e880ee9ef"}, + {file = "numexpr-2.8.8-cp310-cp310-win_amd64.whl", hash = "sha256:22ccd67c0fbeae091f2c577f5b9c8046de6631d46b1cbe22aad46a08d2b42c2d"}, + {file = "numexpr-2.8.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:47c05007cd1c553515492c1a78b5477eaaba9cadc5d7b795d49f7aae53ccdf68"}, + {file = "numexpr-2.8.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b4649c1dcf9b0c2ae0a7b767dbbbde4e05ee68480c1ba7f06fc7963f1f73acf4"}, + {file = "numexpr-2.8.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a82d710145b0fbaec919dde9c90ed9df1e6785625cc36d1c71f3a53112b66fc5"}, + {file = "numexpr-2.8.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a92f230dd9d6c42803f855970e93677b44290b6dad15cb6796fd85edee171ce"}, + {file = "numexpr-2.8.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ccef9b09432d59229c2a737882e55de7906006452003323e107576f264cec373"}, + {file = "numexpr-2.8.8-cp311-cp311-win32.whl", hash = "sha256:bf8c517bbbb82c07c23c17f9d52b4c9f86601f57d48e87c0cbda24af5907f4dd"}, + {file = "numexpr-2.8.8-cp311-cp311-win_amd64.whl", hash = "sha256:4f01d71db6fdb97a68def5407e2dbd748eaea9d98929db08816de40aa4ae3084"}, + {file = "numexpr-2.8.8-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:76f0f010f9c6318bae213b21c5c0e381c2fc9c9ecb8b35f99f5030e7ac96c9ce"}, + {file = "numexpr-2.8.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3f168b4b42d4cb120fe1993676dcf74b77a3e8e45b58855566da037cfd938ca3"}, + {file = "numexpr-2.8.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f031ac4e70f9ad867543bfbde8452e9d1a14f0525346b4b8bd4e5c0f1380a11c"}, + {file = "numexpr-2.8.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:121b049b6909787111daf92919c052c4fd87b5691172e8f19f702b96f20aaafa"}, + {file = "numexpr-2.8.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2ae264c35fa67cd510191ab8144f131fddd0f1d13413af710913ea6fc0c6aa61"}, + {file = "numexpr-2.8.8-cp312-cp312-win32.whl", hash = "sha256:399cb914b41c4027ba88a18f6b8ccfc3af5c32bc3b1758403a7c44c72530618a"}, + {file = "numexpr-2.8.8-cp312-cp312-win_amd64.whl", hash = "sha256:925927cd1f610593e7783d8f2e12e3d800d5928601e077e4910e2b50bde624b6"}, + {file = "numexpr-2.8.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cd07793b074cc38e478637cbe738dff7d8eb92b5cf8ffaacff0c4f0bca5270a0"}, + {file = "numexpr-2.8.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:290f91c7ba7772abaf7107f3cc0601d93d6a3f21c13ee3da93f1b8a9ca3e8d39"}, + {file = "numexpr-2.8.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:296dc1f79d386166dec3bdb45f51caba29ffd8dc91db15447108c04d3001d921"}, + {file = "numexpr-2.8.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7badc50efbb2f1c8b78cd68089031e0fd29cbafa6a9e6d730533f22d88168406"}, + {file = "numexpr-2.8.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4d83a542d9deefb050e389aacaddea0f09d68ec617dd37e45b9a7cfbcba6d729"}, + {file = "numexpr-2.8.8-cp39-cp39-win32.whl", hash = "sha256:17104051f0bd83fd350212e268d8b48017d5eff522b09b573fdbcc560c5e7ab3"}, + {file = "numexpr-2.8.8-cp39-cp39-win_amd64.whl", hash = "sha256:12146521b1730073859a20454e75004e38cd0cb61333e763c58ef5171e101eb2"}, + {file = "numexpr-2.8.8.tar.gz", hash = "sha256:e76ce4d25372f46170cf7eb1ff14ed5d9c69a0b162a405063cbe481bafe3af34"}, +] + +[package.dependencies] +numpy = ">=1.13.3" + [[package]] name = "numpy" version = "1.24.4" @@ -838,22 +1410,22 @@ testing = ["google-api-core[grpc] (>=1.31.5)"] [[package]] name = "protobuf" -version = "4.25.1" +version = "4.25.2" description = "" optional = false python-versions = ">=3.8" files = [ - {file = "protobuf-4.25.1-cp310-abi3-win32.whl", hash = "sha256:193f50a6ab78a970c9b4f148e7c750cfde64f59815e86f686c22e26b4fe01ce7"}, - {file = "protobuf-4.25.1-cp310-abi3-win_amd64.whl", hash = "sha256:3497c1af9f2526962f09329fd61a36566305e6c72da2590ae0d7d1322818843b"}, - {file = "protobuf-4.25.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:0bf384e75b92c42830c0a679b0cd4d6e2b36ae0cf3dbb1e1dfdda48a244f4bcd"}, - {file = "protobuf-4.25.1-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:0f881b589ff449bf0b931a711926e9ddaad3b35089cc039ce1af50b21a4ae8cb"}, - {file = "protobuf-4.25.1-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:ca37bf6a6d0046272c152eea90d2e4ef34593aaa32e8873fc14c16440f22d4b7"}, - {file = "protobuf-4.25.1-cp38-cp38-win32.whl", hash = "sha256:abc0525ae2689a8000837729eef7883b9391cd6aa7950249dcf5a4ede230d5dd"}, - {file = "protobuf-4.25.1-cp38-cp38-win_amd64.whl", hash = "sha256:1484f9e692091450e7edf418c939e15bfc8fc68856e36ce399aed6889dae8bb0"}, - {file = "protobuf-4.25.1-cp39-cp39-win32.whl", hash = "sha256:8bdbeaddaac52d15c6dce38c71b03038ef7772b977847eb6d374fc86636fa510"}, - {file = "protobuf-4.25.1-cp39-cp39-win_amd64.whl", hash = "sha256:becc576b7e6b553d22cbdf418686ee4daa443d7217999125c045ad56322dda10"}, - {file = "protobuf-4.25.1-py3-none-any.whl", hash = "sha256:a19731d5e83ae4737bb2a089605e636077ac001d18781b3cf489b9546c7c80d6"}, - {file = "protobuf-4.25.1.tar.gz", hash = "sha256:57d65074b4f5baa4ab5da1605c02be90ac20c8b40fb137d6a8df9f416b0d0ce2"}, + {file = "protobuf-4.25.2-cp310-abi3-win32.whl", hash = "sha256:b50c949608682b12efb0b2717f53256f03636af5f60ac0c1d900df6213910fd6"}, + {file = "protobuf-4.25.2-cp310-abi3-win_amd64.whl", hash = "sha256:8f62574857ee1de9f770baf04dde4165e30b15ad97ba03ceac65f760ff018ac9"}, + {file = "protobuf-4.25.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:2db9f8fa64fbdcdc93767d3cf81e0f2aef176284071507e3ede160811502fd3d"}, + {file = "protobuf-4.25.2-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:10894a2885b7175d3984f2be8d9850712c57d5e7587a2410720af8be56cdaf62"}, + {file = "protobuf-4.25.2-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:fc381d1dd0516343f1440019cedf08a7405f791cd49eef4ae1ea06520bc1c020"}, + {file = "protobuf-4.25.2-cp38-cp38-win32.whl", hash = "sha256:33a1aeef4b1927431d1be780e87b641e322b88d654203a9e9d93f218ee359e61"}, + {file = "protobuf-4.25.2-cp38-cp38-win_amd64.whl", hash = "sha256:47f3de503fe7c1245f6f03bea7e8d3ec11c6c4a2ea9ef910e3221c8a15516d62"}, + {file = "protobuf-4.25.2-cp39-cp39-win32.whl", hash = "sha256:5e5c933b4c30a988b52e0b7c02641760a5ba046edc5e43d3b94a74c9fc57c1b3"}, + {file = "protobuf-4.25.2-cp39-cp39-win_amd64.whl", hash = "sha256:d66a769b8d687df9024f2985d5137a337f957a0916cf5464d1513eee96a63ff0"}, + {file = "protobuf-4.25.2-py3-none-any.whl", hash = "sha256:a8b7a98d4ce823303145bf3c1a8bdb0f2f4642a414b196f04ad9853ed0c8f830"}, + {file = "protobuf-4.25.2.tar.gz", hash = "sha256:fe599e175cb347efc8ee524bcd4b902d11f7262c0e569ececcb89995c15f0a5e"}, ] [[package]] @@ -1017,6 +1589,20 @@ files = [ [package.dependencies] typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" +[[package]] +name = "pyparsing" +version = "3.1.1" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +optional = false +python-versions = ">=3.6.8" +files = [ + {file = "pyparsing-3.1.1-py3-none-any.whl", hash = "sha256:32c7c0b711493c72ff18a981d24f28aaf9c1fb7ed5e9667c9e84e3db623bdbfb"}, + {file = "pyparsing-3.1.1.tar.gz", hash = "sha256:ede28a1a32462f5a9705e07aea48001a08f7cf81a021585011deba701581a0db"}, +] + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + [[package]] name = "pytest" version = "7.4.4" @@ -1115,6 +1701,7 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -1122,8 +1709,15 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -1140,6 +1734,7 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -1147,6 +1742,7 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -1189,28 +1785,28 @@ pyasn1 = ">=0.1.3" [[package]] name = "ruff" -version = "0.1.11" +version = "0.1.13" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.1.11-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:a7f772696b4cdc0a3b2e527fc3c7ccc41cdcb98f5c80fdd4f2b8c50eb1458196"}, - {file = "ruff-0.1.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:934832f6ed9b34a7d5feea58972635c2039c7a3b434fe5ba2ce015064cb6e955"}, - {file = "ruff-0.1.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea0d3e950e394c4b332bcdd112aa566010a9f9c95814844a7468325290aabfd9"}, - {file = "ruff-0.1.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9bd4025b9c5b429a48280785a2b71d479798a69f5c2919e7d274c5f4b32c3607"}, - {file = "ruff-0.1.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1ad00662305dcb1e987f5ec214d31f7d6a062cae3e74c1cbccef15afd96611d"}, - {file = "ruff-0.1.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4b077ce83f47dd6bea1991af08b140e8b8339f0ba8cb9b7a484c30ebab18a23f"}, - {file = "ruff-0.1.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4a88efecec23c37b11076fe676e15c6cdb1271a38f2b415e381e87fe4517f18"}, - {file = "ruff-0.1.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b25093dad3b055667730a9b491129c42d45e11cdb7043b702e97125bcec48a1"}, - {file = "ruff-0.1.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:231d8fb11b2cc7c0366a326a66dafc6ad449d7fcdbc268497ee47e1334f66f77"}, - {file = "ruff-0.1.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:09c415716884950080921dd6237767e52e227e397e2008e2bed410117679975b"}, - {file = "ruff-0.1.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0f58948c6d212a6b8d41cd59e349751018797ce1727f961c2fa755ad6208ba45"}, - {file = "ruff-0.1.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:190a566c8f766c37074d99640cd9ca3da11d8deae2deae7c9505e68a4a30f740"}, - {file = "ruff-0.1.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6464289bd67b2344d2a5d9158d5eb81025258f169e69a46b741b396ffb0cda95"}, - {file = "ruff-0.1.11-py3-none-win32.whl", hash = "sha256:9b8f397902f92bc2e70fb6bebfa2139008dc72ae5177e66c383fa5426cb0bf2c"}, - {file = "ruff-0.1.11-py3-none-win_amd64.whl", hash = "sha256:eb85ee287b11f901037a6683b2374bb0ec82928c5cbc984f575d0437979c521a"}, - {file = "ruff-0.1.11-py3-none-win_arm64.whl", hash = "sha256:97ce4d752f964ba559c7023a86e5f8e97f026d511e48013987623915431c7ea9"}, - {file = "ruff-0.1.11.tar.gz", hash = "sha256:f9d4d88cb6eeb4dfe20f9f0519bd2eaba8119bde87c3d5065c541dbae2b5a2cb"}, + {file = "ruff-0.1.13-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e3fd36e0d48aeac672aa850045e784673449ce619afc12823ea7868fcc41d8ba"}, + {file = "ruff-0.1.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9fb6b3b86450d4ec6a6732f9f60c4406061b6851c4b29f944f8c9d91c3611c7a"}, + {file = "ruff-0.1.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b13ba5d7156daaf3fd08b6b993360a96060500aca7e307d95ecbc5bb47a69296"}, + {file = "ruff-0.1.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9ebb40442f7b531e136d334ef0851412410061e65d61ca8ce90d894a094feb22"}, + {file = "ruff-0.1.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:226b517f42d59a543d6383cfe03cccf0091e3e0ed1b856c6824be03d2a75d3b6"}, + {file = "ruff-0.1.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5f0312ba1061e9b8c724e9a702d3c8621e3c6e6c2c9bd862550ab2951ac75c16"}, + {file = "ruff-0.1.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2f59bcf5217c661254bd6bc42d65a6fd1a8b80c48763cb5c2293295babd945dd"}, + {file = "ruff-0.1.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e6894b00495e00c27b6ba61af1fc666f17de6140345e5ef27dd6e08fb987259d"}, + {file = "ruff-0.1.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a1600942485c6e66119da294c6294856b5c86fd6df591ce293e4a4cc8e72989"}, + {file = "ruff-0.1.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ee3febce7863e231a467f90e681d3d89210b900d49ce88723ce052c8761be8c7"}, + {file = "ruff-0.1.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dcaab50e278ff497ee4d1fe69b29ca0a9a47cd954bb17963628fa417933c6eb1"}, + {file = "ruff-0.1.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f57de973de4edef3ad3044d6a50c02ad9fc2dff0d88587f25f1a48e3f72edf5e"}, + {file = "ruff-0.1.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7a36fa90eb12208272a858475ec43ac811ac37e91ef868759770b71bdabe27b6"}, + {file = "ruff-0.1.13-py3-none-win32.whl", hash = "sha256:a623349a505ff768dad6bd57087e2461be8db58305ebd5577bd0e98631f9ae69"}, + {file = "ruff-0.1.13-py3-none-win_amd64.whl", hash = "sha256:f988746e3c3982bea7f824c8fa318ce7f538c4dfefec99cd09c8770bd33e6539"}, + {file = "ruff-0.1.13-py3-none-win_arm64.whl", hash = "sha256:6bbbc3042075871ec17f28864808540a26f0f79a4478c357d3e3d2284e832998"}, + {file = "ruff-0.1.13.tar.gz", hash = "sha256:e261f1baed6291f434ffb1d5c6bd8051d1c2a26958072d38dfbec39b3dda7352"}, ] [[package]] @@ -1308,6 +1904,93 @@ files = [ {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, ] +[[package]] +name = "sqlalchemy" +version = "2.0.25" +description = "Database Abstraction Library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "SQLAlchemy-2.0.25-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4344d059265cc8b1b1be351bfb88749294b87a8b2bbe21dfbe066c4199541ebd"}, + {file = "SQLAlchemy-2.0.25-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6f9e2e59cbcc6ba1488404aad43de005d05ca56e069477b33ff74e91b6319735"}, + {file = "SQLAlchemy-2.0.25-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84daa0a2055df9ca0f148a64fdde12ac635e30edbca80e87df9b3aaf419e144a"}, + {file = "SQLAlchemy-2.0.25-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc8b7dabe8e67c4832891a5d322cec6d44ef02f432b4588390017f5cec186a84"}, + {file = "SQLAlchemy-2.0.25-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f5693145220517b5f42393e07a6898acdfe820e136c98663b971906120549da5"}, + {file = "SQLAlchemy-2.0.25-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:db854730a25db7c956423bb9fb4bdd1216c839a689bf9cc15fada0a7fb2f4570"}, + {file = "SQLAlchemy-2.0.25-cp310-cp310-win32.whl", hash = "sha256:14a6f68e8fc96e5e8f5647ef6cda6250c780612a573d99e4d881581432ef1669"}, + {file = "SQLAlchemy-2.0.25-cp310-cp310-win_amd64.whl", hash = "sha256:87f6e732bccd7dcf1741c00f1ecf33797383128bd1c90144ac8adc02cbb98643"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:342d365988ba88ada8af320d43df4e0b13a694dbd75951f537b2d5e4cb5cd002"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f37c0caf14b9e9b9e8f6dbc81bc56db06acb4363eba5a633167781a48ef036ed"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa9373708763ef46782d10e950b49d0235bfe58facebd76917d3f5cbf5971aed"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d24f571990c05f6b36a396218f251f3e0dda916e0c687ef6fdca5072743208f5"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:75432b5b14dc2fff43c50435e248b45c7cdadef73388e5610852b95280ffd0e9"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:884272dcd3ad97f47702965a0e902b540541890f468d24bd1d98bcfe41c3f018"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-win32.whl", hash = "sha256:e607cdd99cbf9bb80391f54446b86e16eea6ad309361942bf88318bcd452363c"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-win_amd64.whl", hash = "sha256:7d505815ac340568fd03f719446a589162d55c52f08abd77ba8964fbb7eb5b5f"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:0dacf67aee53b16f365c589ce72e766efaabd2b145f9de7c917777b575e3659d"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b801154027107461ee992ff4b5c09aa7cc6ec91ddfe50d02bca344918c3265c6"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59a21853f5daeb50412d459cfb13cb82c089ad4c04ec208cd14dddd99fc23b39"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29049e2c299b5ace92cbed0c1610a7a236f3baf4c6b66eb9547c01179f638ec5"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b64b183d610b424a160b0d4d880995e935208fc043d0302dd29fee32d1ee3f95"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4f7a7d7fcc675d3d85fbf3b3828ecd5990b8d61bd6de3f1b260080b3beccf215"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-win32.whl", hash = "sha256:cf18ff7fc9941b8fc23437cc3e68ed4ebeff3599eec6ef5eebf305f3d2e9a7c2"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-win_amd64.whl", hash = "sha256:91f7d9d1c4dd1f4f6e092874c128c11165eafcf7c963128f79e28f8445de82d5"}, + {file = "SQLAlchemy-2.0.25-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bb209a73b8307f8fe4fe46f6ad5979649be01607f11af1eb94aa9e8a3aaf77f0"}, + {file = "SQLAlchemy-2.0.25-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:798f717ae7c806d67145f6ae94dc7c342d3222d3b9a311a784f371a4333212c7"}, + {file = "SQLAlchemy-2.0.25-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fdd402169aa00df3142149940b3bf9ce7dde075928c1886d9a1df63d4b8de62"}, + {file = "SQLAlchemy-2.0.25-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0d3cab3076af2e4aa5693f89622bef7fa770c6fec967143e4da7508b3dceb9b9"}, + {file = "SQLAlchemy-2.0.25-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:74b080c897563f81062b74e44f5a72fa44c2b373741a9ade701d5f789a10ba23"}, + {file = "SQLAlchemy-2.0.25-cp37-cp37m-win32.whl", hash = "sha256:87d91043ea0dc65ee583026cb18e1b458d8ec5fc0a93637126b5fc0bc3ea68c4"}, + {file = "SQLAlchemy-2.0.25-cp37-cp37m-win_amd64.whl", hash = "sha256:75f99202324383d613ddd1f7455ac908dca9c2dd729ec8584c9541dd41822a2c"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:420362338681eec03f53467804541a854617faed7272fe71a1bfdb07336a381e"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c88f0c7dcc5f99bdb34b4fd9b69b93c89f893f454f40219fe923a3a2fd11625"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3be4987e3ee9d9a380b66393b77a4cd6d742480c951a1c56a23c335caca4ce3"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a159111a0f58fb034c93eeba211b4141137ec4b0a6e75789ab7a3ef3c7e7e3"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8b8cb63d3ea63b29074dcd29da4dc6a97ad1349151f2d2949495418fd6e48db9"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:736ea78cd06de6c21ecba7416499e7236a22374561493b456a1f7ffbe3f6cdb4"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-win32.whl", hash = "sha256:10331f129982a19df4284ceac6fe87353ca3ca6b4ca77ff7d697209ae0a5915e"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-win_amd64.whl", hash = "sha256:c55731c116806836a5d678a70c84cb13f2cedba920212ba7dcad53260997666d"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:605b6b059f4b57b277f75ace81cc5bc6335efcbcc4ccb9066695e515dbdb3900"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:665f0a3954635b5b777a55111ababf44b4fc12b1f3ba0a435b602b6387ffd7cf"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecf6d4cda1f9f6cb0b45803a01ea7f034e2f1aed9475e883410812d9f9e3cfcf"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c51db269513917394faec5e5c00d6f83829742ba62e2ac4fa5c98d58be91662f"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:790f533fa5c8901a62b6fef5811d48980adeb2f51f1290ade8b5e7ba990ba3de"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1b1180cda6df7af84fe72e4530f192231b1f29a7496951db4ff38dac1687202d"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-win32.whl", hash = "sha256:555651adbb503ac7f4cb35834c5e4ae0819aab2cd24857a123370764dc7d7e24"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-win_amd64.whl", hash = "sha256:dc55990143cbd853a5d038c05e79284baedf3e299661389654551bd02a6a68d7"}, + {file = "SQLAlchemy-2.0.25-py3-none-any.whl", hash = "sha256:a86b4240e67d4753dc3092d9511886795b3c2852abe599cffe108952f7af7ac3"}, + {file = "SQLAlchemy-2.0.25.tar.gz", hash = "sha256:a2c69a7664fb2d54b8682dd774c3b54f67f84fa123cf84dda2a5f40dcaa04e08"}, +] + +[package.dependencies] +greenlet = {version = "!=0.4.17", markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""} +typing-extensions = ">=4.6.0" + +[package.extras] +aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"] +aioodbc = ["aioodbc", "greenlet (!=0.4.17)"] +aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"] +asyncio = ["greenlet (!=0.4.17)"] +asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"] +mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"] +mssql = ["pyodbc"] +mssql-pymssql = ["pymssql"] +mssql-pyodbc = ["pyodbc"] +mypy = ["mypy (>=0.910)"] +mysql = ["mysqlclient (>=1.4.0)"] +mysql-connector = ["mysql-connector-python"] +oracle = ["cx_oracle (>=8)"] +oracle-oracledb = ["oracledb (>=1.0.1)"] +postgresql = ["psycopg2 (>=2.7)"] +postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] +postgresql-pg8000 = ["pg8000 (>=1.29.1)"] +postgresql-psycopg = ["psycopg (>=3.0.7)"] +postgresql-psycopg2binary = ["psycopg2-binary"] +postgresql-psycopg2cffi = ["psycopg2cffi"] +postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] +pymysql = ["pymysql"] +sqlcipher = ["sqlcipher3_binary"] + [[package]] name = "syrupy" version = "4.6.0" @@ -1347,26 +2030,37 @@ files = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] +[[package]] +name = "types-google-cloud-ndb" +version = "2.2.0.20240106" +description = "Typing stubs for google-cloud-ndb" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-google-cloud-ndb-2.2.0.20240106.tar.gz", hash = "sha256:b81d4ea35f212dc845429d08f1981eb011fe78cee3eebba81157d18b7f6e4616"}, + {file = "types_google_cloud_ndb-2.2.0.20240106-py3-none-any.whl", hash = "sha256:c76efa97b17c15865784fb4e54da56cad805acf81f908dfe4f962a957cb84555"}, +] + [[package]] name = "types-protobuf" -version = "4.24.0.4" +version = "4.24.0.20240106" description = "Typing stubs for protobuf" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "types-protobuf-4.24.0.4.tar.gz", hash = "sha256:57ab42cb171dfdba2c74bb5b50c250478538cc3c5ed95b8b368929ad0c9f90a5"}, - {file = "types_protobuf-4.24.0.4-py3-none-any.whl", hash = "sha256:131ab7d0cbc9e444bc89c994141327dcce7bcaeded72b1acb72a94827eb9c7af"}, + {file = "types-protobuf-4.24.0.20240106.tar.gz", hash = "sha256:024f034f3b5e2bb2bbff55ebc4d591ed0d2280d90faceedcb148b9e714a3f3ee"}, + {file = "types_protobuf-4.24.0.20240106-py3-none-any.whl", hash = "sha256:0612ef3156bd80567460a15ac7c109b313f6022f1fee04b4d922ab2789baab79"}, ] [[package]] name = "types-requests" -version = "2.31.0.20231231" +version = "2.31.0.20240106" description = "Typing stubs for requests" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "types-requests-2.31.0.20231231.tar.gz", hash = "sha256:0f8c0c9764773384122813548d9eea92a5c4e1f33ed54556b508968ec5065cee"}, - {file = "types_requests-2.31.0.20231231-py3-none-any.whl", hash = "sha256:2e2230c7bc8dd63fa3153c1c0ae335f8a368447f0582fc332f17d54f88e69027"}, + {file = "types-requests-2.31.0.20240106.tar.gz", hash = "sha256:0e1c731c17f33618ec58e022b614a1a2ecc25f7dc86800b36ef341380402c612"}, + {file = "types_requests-2.31.0.20240106-py3-none-any.whl", hash = "sha256:da997b3b6a72cc08d09f4dba9802fdbabc89104b35fe24ee588e674037689354"}, ] [package.dependencies] @@ -1383,6 +2077,32 @@ files = [ {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, ] +[[package]] +name = "typing-inspect" +version = "0.9.0" +description = "Runtime inspection utilities for typing module." +optional = false +python-versions = "*" +files = [ + {file = "typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f"}, + {file = "typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78"}, +] + +[package.dependencies] +mypy-extensions = ">=0.3.0" +typing-extensions = ">=3.7.4" + +[[package]] +name = "uritemplate" +version = "4.1.1" +description = "Implementation of RFC 6570 URI Templates" +optional = false +python-versions = ">=3.6" +files = [ + {file = "uritemplate-4.1.1-py2.py3-none-any.whl", hash = "sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e"}, + {file = "uritemplate-4.1.1.tar.gz", hash = "sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0"}, +] + [[package]] name = "urllib3" version = "2.1.0" @@ -1438,7 +2158,110 @@ files = [ [package.extras] watchmedo = ["PyYAML (>=3.10)"] +[[package]] +name = "yarl" +version = "1.9.4" +description = "Yet another URL library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e"}, + {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2"}, + {file = "yarl-1.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541"}, + {file = "yarl-1.9.4-cp310-cp310-win32.whl", hash = "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d"}, + {file = "yarl-1.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b"}, + {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099"}, + {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c"}, + {file = "yarl-1.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98"}, + {file = "yarl-1.9.4-cp311-cp311-win32.whl", hash = "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31"}, + {file = "yarl-1.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1"}, + {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81"}, + {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142"}, + {file = "yarl-1.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10"}, + {file = "yarl-1.9.4-cp312-cp312-win32.whl", hash = "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7"}, + {file = "yarl-1.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984"}, + {file = "yarl-1.9.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434"}, + {file = "yarl-1.9.4-cp37-cp37m-win32.whl", hash = "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749"}, + {file = "yarl-1.9.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2"}, + {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be"}, + {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f"}, + {file = "yarl-1.9.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3"}, + {file = "yarl-1.9.4-cp38-cp38-win32.whl", hash = "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece"}, + {file = "yarl-1.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b"}, + {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27"}, + {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1"}, + {file = "yarl-1.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0"}, + {file = "yarl-1.9.4-cp39-cp39-win32.whl", hash = "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575"}, + {file = "yarl-1.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15"}, + {file = "yarl-1.9.4-py3-none-any.whl", hash = "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad"}, + {file = "yarl-1.9.4.tar.gz", hash = "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf"}, +] + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" + [metadata] lock-version = "2.0" python-versions = ">=3.8.1,<4.0" -content-hash = "7fcbc6833c982cb513d5655481487edf16d011a4366b7612bd2f0da98ade21b0" +content-hash = "a031be3cb062d347bc7b9e1ec95f37c5e0a5184f46c935cc77fbeac9b64bad62" diff --git a/libs/partners/google-vertexai/pyproject.toml b/libs/partners/google-vertexai/pyproject.toml index 22d2280243a85..a6e3bbab48f1c 100644 --- a/libs/partners/google-vertexai/pyproject.toml +++ b/libs/partners/google-vertexai/pyproject.toml @@ -1,20 +1,21 @@ [tool.poetry] name = "langchain-google-vertexai" -version = "0.0.1.post1" +version = "0.0.2" description = "An integration package connecting GoogleVertexAI and LangChain" authors = [] readme = "README.md" repository = "https://github.com/langchain-ai/langchain" +license = "MIT" [tool.poetry.urls] "Source Code" = "https://github.com/langchain-ai/langchain/tree/master/libs/partners/google-vertexai" [tool.poetry.dependencies] python = ">=3.8.1,<4.0" -langchain-core = ">=0.1,<0.2" -google-cloud-aiplatform = "1.38.1" +langchain-core = ">=0.1.7,<0.2" +google-cloud-aiplatform = "^1.39.0" google-cloud-storage = "^2.14.0" -types-requests = "^2.31.0.20231231" +types-requests = "^2.31.0" types-protobuf = "^4.24.0.4" [tool.poetry.group.test] @@ -41,6 +42,9 @@ codespell = "^2.2.0" optional = true [tool.poetry.group.test_integration.dependencies] +langchain = {path = "../../langchain"} +numexpr = {version = "^2.8.8", python = ">=3.9,<4.0"} +google-api-python-client = "^2.114.0" [tool.poetry.group.lint] optional = true @@ -51,6 +55,7 @@ ruff = "^0.1.5" [tool.poetry.group.typing.dependencies] mypy = "^0.991" langchain-core = {path = "../../core", develop = true} +types-google-cloud-ndb = "^2.2.0.20240106" [tool.poetry.group.dev] optional = true diff --git a/libs/partners/google-vertexai/tests/integration_tests/test_chat_models.py b/libs/partners/google-vertexai/tests/integration_tests/test_chat_models.py index 5bf65d99b7dc2..a29094bf920d2 100644 --- a/libs/partners/google-vertexai/tests/integration_tests/test_chat_models.py +++ b/libs/partners/google-vertexai/tests/integration_tests/test_chat_models.py @@ -1,4 +1,6 @@ """Test ChatGoogleVertexAI chat model.""" +from typing import cast + import pytest from langchain_core.messages import ( AIMessage, @@ -6,7 +8,7 @@ HumanMessage, SystemMessage, ) -from langchain_core.outputs import LLMResult +from langchain_core.outputs import ChatGeneration, LLMResult from langchain_google_vertexai.chat_models import ChatVertexAI @@ -60,7 +62,17 @@ async def test_vertexai_agenerate(model_name: str) -> None: assert isinstance(response.generations[0][0].message, AIMessage) # type: ignore sync_response = model.generate([[message]]) - assert response.generations[0][0] == sync_response.generations[0][0] + sync_generation = cast(ChatGeneration, sync_response.generations[0][0]) + async_generation = cast(ChatGeneration, response.generations[0][0]) + + # assert some properties to make debugging easier + + # xfail: this is not equivalent with temp=0 right now + # assert sync_generation.message.content == async_generation.message.content + assert sync_generation.generation_info == async_generation.generation_info + + # xfail: content is not same right now + # assert sync_generation == async_generation @pytest.mark.parametrize("model_name", ["chat-bison@001", "gemini-pro"]) @@ -108,6 +120,7 @@ def test_multimodal() -> None: assert isinstance(output.content, str) +@pytest.mark.xfail(reason="problem on vertex side") def test_multimodal_history() -> None: llm = ChatVertexAI(model_name="gemini-pro-vision") gcs_url = ( @@ -174,3 +187,36 @@ def test_vertexai_single_call_fails_no_message() -> None: str(exc_info.value) == "You should provide at least one message to start the chat!" ) + + +@pytest.mark.parametrize("model_name", ["gemini-pro"]) +def test_chat_vertexai_gemini_system_message_error(model_name: str) -> None: + model = ChatVertexAI(model_name=model_name) + text_question1, text_answer1 = "How much is 2+2?", "4" + text_question2 = "How much is 3+3?" + system_message = SystemMessage(content="You're supposed to answer math questions.") + message1 = HumanMessage(content=text_question1) + message2 = AIMessage(content=text_answer1) + message3 = HumanMessage(content=text_question2) + with pytest.raises(ValueError): + model([system_message, message1, message2, message3]) + + +@pytest.mark.parametrize("model_name", model_names_to_test) +def test_chat_vertexai_system_message(model_name: str) -> None: + if model_name: + model = ChatVertexAI( + model_name=model_name, convert_system_message_to_human=True + ) + else: + model = ChatVertexAI() + + text_question1, text_answer1 = "How much is 2+2?", "4" + text_question2 = "How much is 3+3?" + system_message = SystemMessage(content="You're supposed to answer math questions.") + message1 = HumanMessage(content=text_question1) + message2 = AIMessage(content=text_answer1) + message3 = HumanMessage(content=text_question2) + response = model([system_message, message1, message2, message3]) + assert isinstance(response, AIMessage) + assert isinstance(response.content, str) diff --git a/libs/partners/google-vertexai/tests/integration_tests/test_llms.py b/libs/partners/google-vertexai/tests/integration_tests/test_llms.py index 14f84c0616223..823c8671dc9e7 100644 --- a/libs/partners/google-vertexai/tests/integration_tests/test_llms.py +++ b/libs/partners/google-vertexai/tests/integration_tests/test_llms.py @@ -42,6 +42,7 @@ def test_vertex_call(model_name: str) -> None: assert isinstance(output, str) +@pytest.mark.xfail(reason="VertexAI doesn't always respect number of candidates") def test_vertex_generate() -> None: llm = VertexAI(temperature=0.3, n=2, model_name="text-bison@001") output = llm.generate(["Say foo:"]) @@ -50,6 +51,7 @@ def test_vertex_generate() -> None: assert len(output.generations[0]) == 2 +@pytest.mark.xfail(reason="VertexAI doesn't always respect number of candidates") def test_vertex_generate_code() -> None: llm = VertexAI(temperature=0.3, n=2, model_name="code-bison@001") output = llm.generate(["generate a python method that says foo:"]) @@ -87,6 +89,7 @@ async def test_vertex_consistency() -> None: assert output.generations[0][0].text == async_output.generations[0][0].text +@pytest.mark.skip("CI testing not set up") @pytest.mark.parametrize( "endpoint_os_variable_name,result_arg", [("FALCON_ENDPOINT_ID", "generated_text"), ("LLAMA_ENDPOINT_ID", None)], @@ -115,6 +118,7 @@ def test_model_garden( assert llm._llm_type == "vertexai_model_garden" +@pytest.mark.skip("CI testing not set up") @pytest.mark.parametrize( "endpoint_os_variable_name,result_arg", [("FALCON_ENDPOINT_ID", "generated_text"), ("LLAMA_ENDPOINT_ID", None)], @@ -143,6 +147,7 @@ def test_model_garden_generate( assert len(output.generations) == 2 +@pytest.mark.skip("CI testing not set up") @pytest.mark.asyncio @pytest.mark.parametrize( "endpoint_os_variable_name,result_arg", diff --git a/libs/partners/google-vertexai/tests/integration_tests/test_llms_safety.py b/libs/partners/google-vertexai/tests/integration_tests/test_llms_safety.py new file mode 100644 index 0000000000000..7e526cbf27ad5 --- /dev/null +++ b/libs/partners/google-vertexai/tests/integration_tests/test_llms_safety.py @@ -0,0 +1,97 @@ +from langchain_core.outputs import LLMResult + +from langchain_google_vertexai import HarmBlockThreshold, HarmCategory, VertexAI + +SAFETY_SETTINGS = { + HarmCategory.HARM_CATEGORY_UNSPECIFIED: HarmBlockThreshold.BLOCK_NONE, + HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_NONE, + HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_NONE, + HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_NONE, + HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: HarmBlockThreshold.BLOCK_NONE, +} + + +# below context and question are taken from one of opensource QA datasets +BLOCKED_PROMPT = """ +You are agent designed to answer questions. +You are given context in triple backticks. +``` +The religion\'s failure to report abuse allegations to authorities has also been +criticized. The Watch Tower Society\'s policy is that elders inform authorities when + required by law to do so, but otherwise leave that action up to the victim and his + or her family. The Australian Royal Commission into Institutional Responses to Child +Sexual Abuse found that of 1006 alleged perpetrators of child sexual abuse +identified by the Jehovah\'s Witnesses within their organization since 1950, +"not one was reported by the church to secular authorities." William Bowen, a former +Jehovah\'s Witness elder who established the Silentlambs organization to assist sex +abuse victims within the religion, has claimed Witness leaders discourage followers +from reporting incidents of sexual misconduct to authorities, and other critics claim +the organization is reluctant to alert authorities in order to protect its "crime-free" + reputation. In court cases in the United Kingdom and the United States the Watch Tower + Society has been found to have been negligent in its failure to protect children from + known sex offenders within the congregation and the Society has settled other child +abuse lawsuits out of court, reportedly paying as much as $780,000 to one plaintiff +without admitting wrongdoing. +``` +Question: What have courts in both the UK and the US found the Watch Tower Society to + have been for failing to protect children from sexual predators within the + congregation ? +Answer: +""" + + +def test_gemini_safety_settings_generate() -> None: + llm = VertexAI(model_name="gemini-pro", safety_settings=SAFETY_SETTINGS) + output = llm.generate(["What do you think about child abuse:"]) + assert isinstance(output, LLMResult) + assert len(output.generations) == 1 + generation_info = output.generations[0][0].generation_info + assert generation_info is not None + assert len(generation_info) > 0 + assert not generation_info.get("is_blocked") + + blocked_output = llm.generate([BLOCKED_PROMPT]) + assert isinstance(blocked_output, LLMResult) + assert len(blocked_output.generations) == 1 + assert len(blocked_output.generations[0]) == 0 + + # test safety_settings passed directly to generate + llm = VertexAI(model_name="gemini-pro") + output = llm.generate( + ["What do you think about child abuse:"], safety_settings=SAFETY_SETTINGS + ) + assert isinstance(output, LLMResult) + assert len(output.generations) == 1 + generation_info = output.generations[0][0].generation_info + assert generation_info is not None + assert len(generation_info) > 0 + assert not generation_info.get("is_blocked") + + +async def test_gemini_safety_settings_agenerate() -> None: + llm = VertexAI(model_name="gemini-pro", safety_settings=SAFETY_SETTINGS) + output = await llm.agenerate(["What do you think about child abuse:"]) + assert isinstance(output, LLMResult) + assert len(output.generations) == 1 + generation_info = output.generations[0][0].generation_info + assert generation_info is not None + assert len(generation_info) > 0 + assert not generation_info.get("is_blocked") + + blocked_output = await llm.agenerate([BLOCKED_PROMPT]) + assert isinstance(blocked_output, LLMResult) + assert len(blocked_output.generations) == 1 + # assert len(blocked_output.generations[0][0].generation_info) > 0 + # assert blocked_output.generations[0][0].generation_info.get("is_blocked") + + # test safety_settings passed directly to agenerate + llm = VertexAI(model_name="gemini-pro") + output = await llm.agenerate( + ["What do you think about child abuse:"], safety_settings=SAFETY_SETTINGS + ) + assert isinstance(output, LLMResult) + assert len(output.generations) == 1 + generation_info = output.generations[0][0].generation_info + assert generation_info is not None + assert len(generation_info) > 0 + assert not generation_info.get("is_blocked") diff --git a/libs/partners/google-vertexai/tests/integration_tests/test_tools.py b/libs/partners/google-vertexai/tests/integration_tests/test_tools.py new file mode 100644 index 0000000000000..3230d002db7c5 --- /dev/null +++ b/libs/partners/google-vertexai/tests/integration_tests/test_tools.py @@ -0,0 +1,174 @@ +import os +import re +from typing import List, Union + +from langchain_core.agents import AgentAction, AgentActionMessageLog, AgentFinish +from langchain_core.messages import AIMessageChunk +from langchain_core.output_parsers import BaseOutputParser +from langchain_core.outputs import ChatGeneration, Generation +from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder +from langchain_core.tools import Tool + +from langchain_google_vertexai.chat_models import ChatVertexAI + + +class _TestOutputParser(BaseOutputParser): + def parse_result( + self, result: List[Generation], *, partial: bool = False + ) -> Union[AgentAction, AgentFinish]: + if not isinstance(result[0], ChatGeneration): + raise ValueError("This output parser only works on ChatGeneration output") + message = result[0].message + function_call = message.additional_kwargs.get("function_call", {}) + if function_call: + function_name = function_call["name"] + tool_input = function_call.get("arguments", {}) + + content_msg = f"responded: {message.content}\n" if message.content else "\n" + log_msg = ( + f"\nInvoking: `{function_name}` with `{tool_input}`\n{content_msg}\n" + ) + return AgentActionMessageLog( + tool=function_name, + tool_input=tool_input, + log=log_msg, + message_log=[message], + ) + + return AgentFinish( + return_values={"output": message.content}, log=str(message.content) + ) + + def parse(self, text: str) -> Union[AgentAction, AgentFinish]: + raise ValueError("Can only parse messages") + + +def test_tools() -> None: + from langchain.agents import AgentExecutor # type: ignore + from langchain.agents.format_scratchpad import ( # type: ignore + format_to_openai_function_messages, + ) + from langchain.chains import LLMMathChain # type: ignore + + llm = ChatVertexAI(model_name="gemini-pro") + math_chain = LLMMathChain.from_llm(llm=llm) + tools = [ + Tool( + name="Calculator", + func=math_chain.run, + description="useful for when you need to answer questions about math", + ) + ] + prompt = ChatPromptTemplate.from_messages( + [ + ("user", "{input}"), + MessagesPlaceholder(variable_name="agent_scratchpad"), + ] + ) + llm_with_tools = llm.bind(functions=tools) + + agent = ( + { # type: ignore + "input": lambda x: x["input"], + "agent_scratchpad": lambda x: format_to_openai_function_messages( + x["intermediate_steps"] + ), + } + | prompt + | llm_with_tools + | _TestOutputParser() + ) + agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True) + + response = agent_executor.invoke({"input": "What is 6 raised to the 0.43 power?"}) + print(response) + assert isinstance(response, dict) + assert response["input"] == "What is 6 raised to the 0.43 power?" + + # convert string " The result is 2.160752567226312" to just numbers/periods + # use regex to find \d+\.\d+ + just_numbers = re.findall(r"\d+\.\d+", response["output"])[0] + + assert round(float(just_numbers), 3) == 2.161 + + +def test_stream() -> None: + from langchain.chains import LLMMathChain + + llm = ChatVertexAI(model_name="gemini-pro") + math_chain = LLMMathChain.from_llm(llm=llm) + tools = [ + Tool( + name="Calculator", + func=math_chain.run, + description="useful for when you need to answer questions about math", + ) + ] + response = list(llm.stream("What is 6 raised to the 0.43 power?", functions=tools)) + assert len(response) == 1 + # for chunk in response: + assert isinstance(response[0], AIMessageChunk) + assert "function_call" in response[0].additional_kwargs + + +def test_multiple_tools() -> None: + from langchain.agents import AgentExecutor # type: ignore + from langchain.agents.format_scratchpad import ( + format_to_openai_function_messages, # type: ignore + ) + from langchain.chains import LLMMathChain # type: ignore + from langchain.utilities import GoogleSearchAPIWrapper # type: ignore + + llm = ChatVertexAI(model_name="gemini-pro", max_output_tokens=1024) + math_chain = LLMMathChain.from_llm(llm=llm) + google_search_api_key = os.environ["GOOGLE_SEARCH_API_KEY"] + google_cse_id = os.environ["GOOGLE_CSE_ID"] + search = GoogleSearchAPIWrapper( + k=10, google_api_key=google_search_api_key, google_cse_id=google_cse_id + ) + tools = [ + Tool( + name="Calculator", + func=math_chain.run, + description="useful for when you need to answer questions about math", + ), + Tool( + name="Search", + func=search.run, + description=( + "useful for when you need to answer questions about current events. " + "You should ask targeted questions" + ), + ), + ] + prompt = ChatPromptTemplate.from_messages( + [ + ("user", "{input}"), + MessagesPlaceholder(variable_name="agent_scratchpad"), + ] + ) + llm_with_tools = llm.bind(functions=tools) + + agent = ( + { # type: ignore + "input": lambda x: x["input"], + "agent_scratchpad": lambda x: format_to_openai_function_messages( + x["intermediate_steps"] + ), + } + | prompt + | llm_with_tools + | _TestOutputParser() + ) + agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True) + + question = ( + "Who is Leo DiCaprio's girlfriend? What is her " + "current age raised to the 0.43 power?" + ) + response = agent_executor.invoke({"input": question}) + assert isinstance(response, dict) + assert response["input"] == question + + # xfail: not getting age in search result most of time + # assert "3.850" in response["output"] diff --git a/libs/partners/google-vertexai/tests/unit_tests/test_chat_models.py b/libs/partners/google-vertexai/tests/unit_tests/test_chat_models.py index bff39ee431875..caed17118ab06 100644 --- a/libs/partners/google-vertexai/tests/unit_tests/test_chat_models.py +++ b/libs/partners/google-vertexai/tests/unit_tests/test_chat_models.py @@ -13,6 +13,7 @@ from langchain_google_vertexai.chat_models import ( ChatVertexAI, _parse_chat_history, + _parse_chat_history_gemini, _parse_examples, ) @@ -68,7 +69,7 @@ def test_vertexai_args_passed(stop: Optional[str]) -> None: mock_model.start_chat = mock_start_chat mg.return_value = mock_model - model = ChatVertexAI(**prompt_params) + model = ChatVertexAI(**prompt_params) # type: ignore message = HumanMessage(content=user_prompt) if stop: response = model([message], stop=[stop]) @@ -110,3 +111,70 @@ def test_parse_chat_history_correct() -> None: ChatMessage(content=text_question, author="user"), ChatMessage(content=text_answer, author="bot"), ] + + +def test_parse_history_gemini() -> None: + system_input = "You're supposed to answer math questions." + text_question1, text_answer1 = "How much is 2+2?", "4" + text_question2 = "How much is 3+3?" + system_message = SystemMessage(content=system_input) + message1 = HumanMessage(content=text_question1) + message2 = AIMessage(content=text_answer1) + message3 = HumanMessage(content=text_question2) + messages = [system_message, message1, message2, message3] + history = _parse_chat_history_gemini(messages, convert_system_message_to_human=True) + assert len(history) == 3 + assert history[0].role == "user" + assert history[0].parts[0].text == system_input + assert history[0].parts[1].text == text_question1 + assert history[1].role == "model" + assert history[1].parts[0].text == text_answer1 + + +def test_default_params_palm() -> None: + user_prompt = "Hello" + + with patch("vertexai._model_garden._model_garden_models._from_pretrained") as mg: + mock_response = MagicMock() + mock_response.candidates = [Mock(text="Goodbye")] + mock_chat = MagicMock() + mock_send_message = MagicMock(return_value=mock_response) + mock_chat.send_message = mock_send_message + + mock_model = MagicMock() + mock_start_chat = MagicMock(return_value=mock_chat) + mock_model.start_chat = mock_start_chat + mg.return_value = mock_model + + model = ChatVertexAI(model_name="text-bison@001") + message = HumanMessage(content=user_prompt) + _ = model([message]) + mock_start_chat.assert_called_once_with( + context=None, + message_history=[], + max_output_tokens=128, + top_k=40, + top_p=0.95, + stop_sequences=None, + ) + + +def test_default_params_gemini() -> None: + user_prompt = "Hello" + + with patch("langchain_google_vertexai.chat_models.GenerativeModel") as gm: + mock_response = MagicMock() + content = Mock(parts=[Mock(function_call=None)]) + mock_response.candidates = [Mock(text="Goodbye", content=content)] + mock_chat = MagicMock() + mock_send_message = MagicMock(return_value=mock_response) + mock_chat.send_message = mock_send_message + + mock_model = MagicMock() + mock_start_chat = MagicMock(return_value=mock_chat) + mock_model.start_chat = mock_start_chat + gm.return_value = mock_model + model = ChatVertexAI(model_name="gemini-pro") + message = HumanMessage(content=user_prompt) + _ = model([message]) + mock_start_chat.assert_called_once_with(history=[]) diff --git a/libs/partners/google-vertexai/tests/unit_tests/test_imports.py b/libs/partners/google-vertexai/tests/unit_tests/test_imports.py index 016d6e21c73ba..7afa74f1dc7ce 100644 --- a/libs/partners/google-vertexai/tests/unit_tests/test_imports.py +++ b/libs/partners/google-vertexai/tests/unit_tests/test_imports.py @@ -1,6 +1,15 @@ from langchain_google_vertexai import __all__ -EXPECTED_ALL = ["ChatVertexAI", "VertexAIEmbeddings", "VertexAI", "VertexAIModelGarden"] +EXPECTED_ALL = [ + "ChatVertexAI", + "VertexAIEmbeddings", + "VertexAI", + "VertexAIModelGarden", + "HarmBlockThreshold", + "HarmCategory", + "PydanticFunctionsOutputParser", + "create_structured_runnable", +] def test_all_imports() -> None: diff --git a/libs/partners/mistralai/README.md b/libs/partners/mistralai/README.md index 9ec32ed43218e..752544e10ce2c 100644 --- a/libs/partners/mistralai/README.md +++ b/libs/partners/mistralai/README.md @@ -39,3 +39,19 @@ await chat.ainvoke(messages) for chunk in chat.stream(messages): print(chunk.content, end="", flush=True) ``` + +## Embeddings + +With `MistralAIEmbeddings`, you can directly use the default model 'mistral-embed', or set a different one if available. + +### Choose model + +`embedding.model = 'mistral-embed'` + +### Simple query + +`res_query = embedding.embed_query("The test information")` + +### Documents + +`res_document = embedding.embed_documents(["test1", "another test"])` \ No newline at end of file diff --git a/libs/partners/mistralai/docs/embeddings.ipynb b/libs/partners/mistralai/docs/embeddings.ipynb new file mode 100644 index 0000000000000..33ed1137fdc9f --- /dev/null +++ b/libs/partners/mistralai/docs/embeddings.ipynb @@ -0,0 +1,103 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "b14a24db", + "metadata": {}, + "source": [ + "# MistralAIEmbeddings\n", + "\n", + "This notebook explains how to use MistralAIEmbeddings, which is included in the langchain_mistralai package, to embed texts in langchain." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "0ab948fc", + "metadata": {}, + "outputs": [], + "source": [ + "# pip install -U langchain-mistralai" + ] + }, + { + "cell_type": "markdown", + "id": "67c637ca", + "metadata": {}, + "source": [ + "## import the library" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "5709b030", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_mistralai import MistralAIEmbeddings" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "1756b1ba", + "metadata": {}, + "outputs": [], + "source": [ + "embedding = MistralAIEmbeddings(mistral_api_key='your-api-key')" + ] + }, + { + "cell_type": "markdown", + "id": "4a2a098d", + "metadata": {}, + "source": [ + "# Using the Embedding Model\n", + "With `MistralAIEmbeddings`, you can directly use the default model 'mistral-embed', or set a different one if available." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "584b9af5", + "metadata": {}, + "outputs": [], + "source": [ + "embedding.model = 'mistral-embed' # or your preferred model if available" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "be18b873", + "metadata": {}, + "outputs": [], + "source": [ + "res_query = embedding.embed_query(\"The test information\")\n", + "res_document = embedding.embed_documents([\"test1\", \"another test\"])" + ] + } + ], + "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.4" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/libs/partners/mistralai/langchain_mistralai/__init__.py b/libs/partners/mistralai/langchain_mistralai/__init__.py index d592f9da7e4a8..10a26e83fae03 100644 --- a/libs/partners/mistralai/langchain_mistralai/__init__.py +++ b/libs/partners/mistralai/langchain_mistralai/__init__.py @@ -1,3 +1,4 @@ from langchain_mistralai.chat_models import ChatMistralAI +from langchain_mistralai.embeddings import MistralAIEmbeddings -__all__ = ["ChatMistralAI"] +__all__ = ["ChatMistralAI", "MistralAIEmbeddings"] diff --git a/libs/partners/mistralai/langchain_mistralai/chat_models.py b/libs/partners/mistralai/langchain_mistralai/chat_models.py index dda70525d80ec..a13308e5d581c 100644 --- a/libs/partners/mistralai/langchain_mistralai/chat_models.py +++ b/libs/partners/mistralai/langchain_mistralai/chat_models.py @@ -42,27 +42,25 @@ ChatGenerationChunk, ChatResult, ) -from langchain_core.pydantic_v1 import SecretStr, root_validator +from langchain_core.pydantic_v1 import Field, SecretStr, root_validator from langchain_core.utils import convert_to_secret_str, get_from_dict_or_env - -# TODO: Remove 'type: ignore' once mistralai has stubs or py.typed marker. -from mistralai.async_client import MistralAsyncClient # type: ignore[import] -from mistralai.client import MistralClient # type: ignore[import] -from mistralai.constants import ( # type: ignore[import] +from mistralai.async_client import MistralAsyncClient +from mistralai.client import MistralClient +from mistralai.constants import ( ENDPOINT as DEFAULT_MISTRAL_ENDPOINT, ) -from mistralai.exceptions import ( # type: ignore[import] +from mistralai.exceptions import ( MistralAPIException, MistralConnectionException, MistralException, ) -from mistralai.models.chat_completion import ( # type: ignore[import] +from mistralai.models.chat_completion import ( ChatCompletionResponse as MistralChatCompletionResponse, ) -from mistralai.models.chat_completion import ( # type: ignore[import] +from mistralai.models.chat_completion import ( ChatMessage as MistralChatMessage, ) -from mistralai.models.chat_completion import ( # type: ignore[import] +from mistralai.models.chat_completion import ( DeltaMessage as MistralDeltaMessage, ) @@ -156,8 +154,8 @@ def _convert_message_to_mistral_chat_message( class ChatMistralAI(BaseChatModel): """A chat model that uses the MistralAI API.""" - client: MistralClient = None #: :meta private: - async_client: MistralAsyncClient = None #: :meta private: + client: MistralClient = Field(default=None) #: :meta private: + async_client: MistralAsyncClient = Field(default=None) #: :meta private: mistral_api_key: Optional[SecretStr] = None endpoint: str = DEFAULT_MISTRAL_ENDPOINT max_retries: int = 5 diff --git a/libs/partners/mistralai/langchain_mistralai/embeddings.py b/libs/partners/mistralai/langchain_mistralai/embeddings.py new file mode 100644 index 0000000000000..de9c440626216 --- /dev/null +++ b/libs/partners/mistralai/langchain_mistralai/embeddings.py @@ -0,0 +1,141 @@ +import logging +from typing import Dict, List, Optional + +from langchain_core.embeddings import Embeddings +from langchain_core.pydantic_v1 import ( + BaseModel, + Extra, + Field, + SecretStr, + root_validator, +) +from langchain_core.utils import convert_to_secret_str, get_from_dict_or_env +from mistralai.async_client import MistralAsyncClient +from mistralai.client import MistralClient +from mistralai.constants import ( + ENDPOINT as DEFAULT_MISTRAL_ENDPOINT, +) +from mistralai.exceptions import MistralException + +logger = logging.getLogger(__name__) + + +class MistralAIEmbeddings(BaseModel, Embeddings): + """MistralAI embedding models. + + To use, set the environment variable `MISTRAL_API_KEY` is set with your API key or + pass it as a named parameter to the constructor. + + Example: + .. code-block:: python + + from langchain_mistralai import MistralAIEmbeddings + mistral = MistralAIEmbeddings( + model="mistral-embed", + mistral_api_key="my-api-key" + ) + """ + + client: MistralClient = Field(default=None) #: :meta private: + async_client: MistralAsyncClient = Field(default=None) #: :meta private: + mistral_api_key: Optional[SecretStr] = None + endpoint: str = DEFAULT_MISTRAL_ENDPOINT + max_retries: int = 5 + timeout: int = 120 + max_concurrent_requests: int = 64 + + model: str = "mistral-embed" + + class Config: + extra = Extra.forbid + arbitrary_types_allowed = True + + @root_validator() + def validate_environment(cls, values: Dict) -> Dict: + """Validate configuration.""" + + values["mistral_api_key"] = convert_to_secret_str( + get_from_dict_or_env( + values, "mistral_api_key", "MISTRAL_API_KEY", default="" + ) + ) + values["client"] = MistralClient( + api_key=values["mistral_api_key"].get_secret_value(), + endpoint=values["endpoint"], + max_retries=values["max_retries"], + timeout=values["timeout"], + ) + values["async_client"] = MistralAsyncClient( + api_key=values["mistral_api_key"].get_secret_value(), + endpoint=values["endpoint"], + max_retries=values["max_retries"], + timeout=values["timeout"], + max_concurrent_requests=values["max_concurrent_requests"], + ) + return values + + def embed_documents(self, texts: List[str]) -> List[List[float]]: + """Embed a list of document texts. + + Args: + texts: The list of texts to embed. + + Returns: + List of embeddings, one for each text. + """ + try: + embeddings_batch_response = self.client.embeddings( + model=self.model, + input=texts, + ) + return [ + list(map(float, embedding_obj.embedding)) + for embedding_obj in embeddings_batch_response.data + ] + except MistralException as e: + logger.error(f"An error occurred with MistralAI: {e}") + raise + + async def aembed_documents(self, texts: List[str]) -> List[List[float]]: + """Embed a list of document texts. + + Args: + texts: The list of texts to embed. + + Returns: + List of embeddings, one for each text. + """ + try: + embeddings_batch_response = await self.async_client.embeddings( + model=self.model, + input=texts, + ) + return [ + list(map(float, embedding_obj.embedding)) + for embedding_obj in embeddings_batch_response.data + ] + except MistralException as e: + logger.error(f"An error occurred with MistralAI: {e}") + raise + + def embed_query(self, text: str) -> List[float]: + """Embed a single query text. + + Args: + text: The text to embed. + + Returns: + Embedding for the text. + """ + return self.embed_documents([text])[0] + + async def aembed_query(self, text: str) -> List[float]: + """Embed a single query text. + + Args: + text: The text to embed. + + Returns: + Embedding for the text. + """ + return (await self.aembed_documents([text]))[0] diff --git a/libs/partners/mistralai/poetry.lock b/libs/partners/mistralai/poetry.lock index d7aefe11e703b..8d406846bf2bf 100644 --- a/libs/partners/mistralai/poetry.lock +++ b/libs/partners/mistralai/poetry.lock @@ -1,115 +1,5 @@ # This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. -[[package]] -name = "aiohttp" -version = "3.9.1" -description = "Async http client/server framework (asyncio)" -optional = false -python-versions = ">=3.8" -files = [ - {file = "aiohttp-3.9.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e1f80197f8b0b846a8d5cf7b7ec6084493950d0882cc5537fb7b96a69e3c8590"}, - {file = "aiohttp-3.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c72444d17777865734aa1a4d167794c34b63e5883abb90356a0364a28904e6c0"}, - {file = "aiohttp-3.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9b05d5cbe9dafcdc733262c3a99ccf63d2f7ce02543620d2bd8db4d4f7a22f83"}, - {file = "aiohttp-3.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c4fa235d534b3547184831c624c0b7c1e262cd1de847d95085ec94c16fddcd5"}, - {file = "aiohttp-3.9.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:289ba9ae8e88d0ba16062ecf02dd730b34186ea3b1e7489046fc338bdc3361c4"}, - {file = "aiohttp-3.9.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bff7e2811814fa2271be95ab6e84c9436d027a0e59665de60edf44e529a42c1f"}, - {file = "aiohttp-3.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81b77f868814346662c96ab36b875d7814ebf82340d3284a31681085c051320f"}, - {file = "aiohttp-3.9.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b9c7426923bb7bd66d409da46c41e3fb40f5caf679da624439b9eba92043fa6"}, - {file = "aiohttp-3.9.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8d44e7bf06b0c0a70a20f9100af9fcfd7f6d9d3913e37754c12d424179b4e48f"}, - {file = "aiohttp-3.9.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:22698f01ff5653fe66d16ffb7658f582a0ac084d7da1323e39fd9eab326a1f26"}, - {file = "aiohttp-3.9.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ca7ca5abfbfe8d39e653870fbe8d7710be7a857f8a8386fc9de1aae2e02ce7e4"}, - {file = "aiohttp-3.9.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:8d7f98fde213f74561be1d6d3fa353656197f75d4edfbb3d94c9eb9b0fc47f5d"}, - {file = "aiohttp-3.9.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5216b6082c624b55cfe79af5d538e499cd5f5b976820eac31951fb4325974501"}, - {file = "aiohttp-3.9.1-cp310-cp310-win32.whl", hash = "sha256:0e7ba7ff228c0d9a2cd66194e90f2bca6e0abca810b786901a569c0de082f489"}, - {file = "aiohttp-3.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:c7e939f1ae428a86e4abbb9a7c4732bf4706048818dfd979e5e2839ce0159f23"}, - {file = "aiohttp-3.9.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:df9cf74b9bc03d586fc53ba470828d7b77ce51b0582d1d0b5b2fb673c0baa32d"}, - {file = "aiohttp-3.9.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ecca113f19d5e74048c001934045a2b9368d77b0b17691d905af18bd1c21275e"}, - {file = "aiohttp-3.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8cef8710fb849d97c533f259103f09bac167a008d7131d7b2b0e3a33269185c0"}, - {file = "aiohttp-3.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bea94403a21eb94c93386d559bce297381609153e418a3ffc7d6bf772f59cc35"}, - {file = "aiohttp-3.9.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91c742ca59045dce7ba76cab6e223e41d2c70d79e82c284a96411f8645e2afff"}, - {file = "aiohttp-3.9.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6c93b7c2e52061f0925c3382d5cb8980e40f91c989563d3d32ca280069fd6a87"}, - {file = "aiohttp-3.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee2527134f95e106cc1653e9ac78846f3a2ec1004cf20ef4e02038035a74544d"}, - {file = "aiohttp-3.9.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11ff168d752cb41e8492817e10fb4f85828f6a0142b9726a30c27c35a1835f01"}, - {file = "aiohttp-3.9.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b8c3a67eb87394386847d188996920f33b01b32155f0a94f36ca0e0c635bf3e3"}, - {file = "aiohttp-3.9.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c7b5d5d64e2a14e35a9240b33b89389e0035e6de8dbb7ffa50d10d8b65c57449"}, - {file = "aiohttp-3.9.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:69985d50a2b6f709412d944ffb2e97d0be154ea90600b7a921f95a87d6f108a2"}, - {file = "aiohttp-3.9.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:c9110c06eaaac7e1f5562caf481f18ccf8f6fdf4c3323feab28a93d34cc646bd"}, - {file = "aiohttp-3.9.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d737e69d193dac7296365a6dcb73bbbf53bb760ab25a3727716bbd42022e8d7a"}, - {file = "aiohttp-3.9.1-cp311-cp311-win32.whl", hash = "sha256:4ee8caa925aebc1e64e98432d78ea8de67b2272252b0a931d2ac3bd876ad5544"}, - {file = "aiohttp-3.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:a34086c5cc285be878622e0a6ab897a986a6e8bf5b67ecb377015f06ed316587"}, - {file = "aiohttp-3.9.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f800164276eec54e0af5c99feb9494c295118fc10a11b997bbb1348ba1a52065"}, - {file = "aiohttp-3.9.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:500f1c59906cd142d452074f3811614be04819a38ae2b3239a48b82649c08821"}, - {file = "aiohttp-3.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0b0a6a36ed7e164c6df1e18ee47afbd1990ce47cb428739d6c99aaabfaf1b3af"}, - {file = "aiohttp-3.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69da0f3ed3496808e8cbc5123a866c41c12c15baaaead96d256477edf168eb57"}, - {file = "aiohttp-3.9.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:176df045597e674fa950bf5ae536be85699e04cea68fa3a616cf75e413737eb5"}, - {file = "aiohttp-3.9.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b796b44111f0cab6bbf66214186e44734b5baab949cb5fb56154142a92989aeb"}, - {file = "aiohttp-3.9.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f27fdaadce22f2ef950fc10dcdf8048407c3b42b73779e48a4e76b3c35bca26c"}, - {file = "aiohttp-3.9.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bcb6532b9814ea7c5a6a3299747c49de30e84472fa72821b07f5a9818bce0f66"}, - {file = "aiohttp-3.9.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:54631fb69a6e44b2ba522f7c22a6fb2667a02fd97d636048478db2fd8c4e98fe"}, - {file = "aiohttp-3.9.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4b4c452d0190c5a820d3f5c0f3cd8a28ace48c54053e24da9d6041bf81113183"}, - {file = "aiohttp-3.9.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:cae4c0c2ca800c793cae07ef3d40794625471040a87e1ba392039639ad61ab5b"}, - {file = "aiohttp-3.9.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:565760d6812b8d78d416c3c7cfdf5362fbe0d0d25b82fed75d0d29e18d7fc30f"}, - {file = "aiohttp-3.9.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:54311eb54f3a0c45efb9ed0d0a8f43d1bc6060d773f6973efd90037a51cd0a3f"}, - {file = "aiohttp-3.9.1-cp312-cp312-win32.whl", hash = "sha256:85c3e3c9cb1d480e0b9a64c658cd66b3cfb8e721636ab8b0e746e2d79a7a9eed"}, - {file = "aiohttp-3.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:11cb254e397a82efb1805d12561e80124928e04e9c4483587ce7390b3866d213"}, - {file = "aiohttp-3.9.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8a22a34bc594d9d24621091d1b91511001a7eea91d6652ea495ce06e27381f70"}, - {file = "aiohttp-3.9.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:598db66eaf2e04aa0c8900a63b0101fdc5e6b8a7ddd805c56d86efb54eb66672"}, - {file = "aiohttp-3.9.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2c9376e2b09895c8ca8b95362283365eb5c03bdc8428ade80a864160605715f1"}, - {file = "aiohttp-3.9.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41473de252e1797c2d2293804e389a6d6986ef37cbb4a25208de537ae32141dd"}, - {file = "aiohttp-3.9.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c5857612c9813796960c00767645cb5da815af16dafb32d70c72a8390bbf690"}, - {file = "aiohttp-3.9.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ffcd828e37dc219a72c9012ec44ad2e7e3066bec6ff3aaa19e7d435dbf4032ca"}, - {file = "aiohttp-3.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:219a16763dc0294842188ac8a12262b5671817042b35d45e44fd0a697d8c8361"}, - {file = "aiohttp-3.9.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f694dc8a6a3112059258a725a4ebe9acac5fe62f11c77ac4dcf896edfa78ca28"}, - {file = "aiohttp-3.9.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bcc0ea8d5b74a41b621ad4a13d96c36079c81628ccc0b30cfb1603e3dfa3a014"}, - {file = "aiohttp-3.9.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:90ec72d231169b4b8d6085be13023ece8fa9b1bb495e4398d847e25218e0f431"}, - {file = "aiohttp-3.9.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:cf2a0ac0615842b849f40c4d7f304986a242f1e68286dbf3bd7a835e4f83acfd"}, - {file = "aiohttp-3.9.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:0e49b08eafa4f5707ecfb321ab9592717a319e37938e301d462f79b4e860c32a"}, - {file = "aiohttp-3.9.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2c59e0076ea31c08553e868cec02d22191c086f00b44610f8ab7363a11a5d9d8"}, - {file = "aiohttp-3.9.1-cp38-cp38-win32.whl", hash = "sha256:4831df72b053b1eed31eb00a2e1aff6896fb4485301d4ccb208cac264b648db4"}, - {file = "aiohttp-3.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:3135713c5562731ee18f58d3ad1bf41e1d8883eb68b363f2ffde5b2ea4b84cc7"}, - {file = "aiohttp-3.9.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:cfeadf42840c1e870dc2042a232a8748e75a36b52d78968cda6736de55582766"}, - {file = "aiohttp-3.9.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:70907533db712f7aa791effb38efa96f044ce3d4e850e2d7691abd759f4f0ae0"}, - {file = "aiohttp-3.9.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cdefe289681507187e375a5064c7599f52c40343a8701761c802c1853a504558"}, - {file = "aiohttp-3.9.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7481f581251bb5558ba9f635db70908819caa221fc79ee52a7f58392778c636"}, - {file = "aiohttp-3.9.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:49f0c1b3c2842556e5de35f122fc0f0b721334ceb6e78c3719693364d4af8499"}, - {file = "aiohttp-3.9.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0d406b01a9f5a7e232d1b0d161b40c05275ffbcbd772dc18c1d5a570961a1ca4"}, - {file = "aiohttp-3.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d8e4450e7fe24d86e86b23cc209e0023177b6d59502e33807b732d2deb6975f"}, - {file = "aiohttp-3.9.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c0266cd6f005e99f3f51e583012de2778e65af6b73860038b968a0a8888487a"}, - {file = "aiohttp-3.9.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab221850108a4a063c5b8a70f00dd7a1975e5a1713f87f4ab26a46e5feac5a0e"}, - {file = "aiohttp-3.9.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c88a15f272a0ad3d7773cf3a37cc7b7d077cbfc8e331675cf1346e849d97a4e5"}, - {file = "aiohttp-3.9.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:237533179d9747080bcaad4d02083ce295c0d2eab3e9e8ce103411a4312991a0"}, - {file = "aiohttp-3.9.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:02ab6006ec3c3463b528374c4cdce86434e7b89ad355e7bf29e2f16b46c7dd6f"}, - {file = "aiohttp-3.9.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04fa38875e53eb7e354ece1607b1d2fdee2d175ea4e4d745f6ec9f751fe20c7c"}, - {file = "aiohttp-3.9.1-cp39-cp39-win32.whl", hash = "sha256:82eefaf1a996060602f3cc1112d93ba8b201dbf5d8fd9611227de2003dddb3b7"}, - {file = "aiohttp-3.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:9b05d33ff8e6b269e30a7957bd3244ffbce2a7a35a81b81c382629b80af1a8bf"}, - {file = "aiohttp-3.9.1.tar.gz", hash = "sha256:8fc49a87ac269d4529da45871e2ffb6874e87779c3d0e2ccd813c0899221239d"}, -] - -[package.dependencies] -aiosignal = ">=1.1.2" -async-timeout = {version = ">=4.0,<5.0", markers = "python_version < \"3.11\""} -attrs = ">=17.3.0" -frozenlist = ">=1.1.1" -multidict = ">=4.5,<7.0" -yarl = ">=1.0,<2.0" - -[package.extras] -speedups = ["Brotli", "aiodns", "brotlicffi"] - -[[package]] -name = "aiosignal" -version = "1.3.1" -description = "aiosignal: a list of registered asynchronous callbacks" -optional = false -python-versions = ">=3.7" -files = [ - {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, - {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, -] - -[package.dependencies] -frozenlist = ">=1.1.0" - [[package]] name = "annotated-types" version = "0.6.0" @@ -145,46 +35,6 @@ doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphin test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] trio = ["trio (>=0.23)"] -[[package]] -name = "async-timeout" -version = "4.0.3" -description = "Timeout context manager for asyncio programs" -optional = false -python-versions = ">=3.7" -files = [ - {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, - {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, -] - -[[package]] -name = "attrs" -version = "23.1.0" -description = "Classes Without Boilerplate" -optional = false -python-versions = ">=3.7" -files = [ - {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, - {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, -] - -[package.extras] -cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] -dev = ["attrs[docs,tests]", "pre-commit"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] -tests = ["attrs[tests-no-zope]", "zope-interface"] -tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] - -[[package]] -name = "backoff" -version = "2.2.1" -description = "Function decoration for backoff and retry" -optional = false -python-versions = ">=3.7,<4.0" -files = [ - {file = "backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8"}, - {file = "backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba"}, -] - [[package]] name = "certifi" version = "2023.11.17" @@ -338,91 +188,61 @@ files = [ test = ["pytest (>=6)"] [[package]] -name = "frozenlist" -version = "1.4.1" -description = "A list-like structure which implements collections.abc.MutableSequence" +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "httpcore" +version = "1.0.2" +description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" files = [ - {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac"}, - {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868"}, - {file = "frozenlist-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc"}, - {file = "frozenlist-1.4.1-cp310-cp310-win32.whl", hash = "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1"}, - {file = "frozenlist-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439"}, - {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0"}, - {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49"}, - {file = "frozenlist-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2"}, - {file = "frozenlist-1.4.1-cp311-cp311-win32.whl", hash = "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17"}, - {file = "frozenlist-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825"}, - {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae"}, - {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb"}, - {file = "frozenlist-1.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8"}, - {file = "frozenlist-1.4.1-cp312-cp312-win32.whl", hash = "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89"}, - {file = "frozenlist-1.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5"}, - {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d"}, - {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826"}, - {file = "frozenlist-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7"}, - {file = "frozenlist-1.4.1-cp38-cp38-win32.whl", hash = "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497"}, - {file = "frozenlist-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09"}, - {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e"}, - {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d"}, - {file = "frozenlist-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6"}, - {file = "frozenlist-1.4.1-cp39-cp39-win32.whl", hash = "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932"}, - {file = "frozenlist-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0"}, - {file = "frozenlist-1.4.1-py3-none-any.whl", hash = "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7"}, - {file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"}, + {file = "httpcore-1.0.2-py3-none-any.whl", hash = "sha256:096cc05bca73b8e459a1fc3dcf585148f63e534eae4339559c9b8a8d6399acc7"}, + {file = "httpcore-1.0.2.tar.gz", hash = "sha256:9fc092e4799b26174648e54b74ed5f683132a464e95643b226e00c2ed2fa6535"}, ] +[package.dependencies] +certifi = "*" +h11 = ">=0.13,<0.15" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<0.23.0)"] + +[[package]] +name = "httpx" +version = "0.25.2" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpx-0.25.2-py3-none-any.whl", hash = "sha256:a05d3d052d9b2dfce0e3896636467f8a5342fb2b902c819428e1ac65413ca118"}, + {file = "httpx-0.25.2.tar.gz", hash = "sha256:8b8fcaa0c8ea7b05edd69a094e63a2094c4efcb48129fb757361bc423c0ad9e8"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" +sniffio = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] + [[package]] name = "idna" version = "3.6" @@ -513,104 +333,19 @@ requests = ">=2,<3" [[package]] name = "mistralai" -version = "0.0.8" +version = "0.0.11" description = "" optional = false python-versions = ">=3.8,<4.0" files = [ - {file = "mistralai-0.0.8-py3-none-any.whl", hash = "sha256:288d31c30d40aacef46c98f05676813153938e36e1a3d49c3943eba227faf8b3"}, - {file = "mistralai-0.0.8.tar.gz", hash = "sha256:c1d9f53f75d6b99f614ce3d08cf90d99927c1af73ec986859ebe058431a18a5b"}, + {file = "mistralai-0.0.11-py3-none-any.whl", hash = "sha256:fb2a240a3985420c4e7db48eb5077d6d6dbc5e83cac0dd948c20342fb48087ee"}, + {file = "mistralai-0.0.11.tar.gz", hash = "sha256:383072715531198305dab829ab3749b64933bbc2549354f3c9ebc43c17b912cf"}, ] [package.dependencies] -aiohttp = ">=3.9.1,<4.0.0" -backoff = ">=2.2.1,<3.0.0" +httpx = ">=0.25.2,<0.26.0" orjson = ">=3.9.10,<4.0.0" pydantic = ">=2.5.2,<3.0.0" -requests = ">=2.31.0,<3.0.0" - -[[package]] -name = "multidict" -version = "6.0.4" -description = "multidict implementation" -optional = false -python-versions = ">=3.7" -files = [ - {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8"}, - {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171"}, - {file = "multidict-6.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c048099e4c9e9d615545e2001d3d8a4380bd403e1a0578734e0d31703d1b0c0b"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea20853c6dbbb53ed34cb4d080382169b6f4554d394015f1bef35e881bf83547"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16d232d4e5396c2efbbf4f6d4df89bfa905eb0d4dc5b3549d872ab898451f569"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36c63aaa167f6c6b04ef2c85704e93af16c11d20de1d133e39de6a0e84582a93"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64bdf1086b6043bf519869678f5f2757f473dee970d7abf6da91ec00acb9cb98"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:43644e38f42e3af682690876cff722d301ac585c5b9e1eacc013b7a3f7b696a0"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7582a1d1030e15422262de9f58711774e02fa80df0d1578995c76214f6954988"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ddff9c4e225a63a5afab9dd15590432c22e8057e1a9a13d28ed128ecf047bbdc"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ee2a1ece51b9b9e7752e742cfb661d2a29e7bcdba2d27e66e28a99f1890e4fa0"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a2e4369eb3d47d2034032a26c7a80fcb21a2cb22e1173d761a162f11e562caa5"}, - {file = "multidict-6.0.4-cp310-cp310-win32.whl", hash = "sha256:574b7eae1ab267e5f8285f0fe881f17efe4b98c39a40858247720935b893bba8"}, - {file = "multidict-6.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:4dcbb0906e38440fa3e325df2359ac6cb043df8e58c965bb45f4e406ecb162cc"}, - {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0dfad7a5a1e39c53ed00d2dd0c2e36aed4650936dc18fd9a1826a5ae1cad6f03"}, - {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:64da238a09d6039e3bd39bb3aee9c21a5e34f28bfa5aa22518581f910ff94af3"}, - {file = "multidict-6.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff959bee35038c4624250473988b24f846cbeb2c6639de3602c073f10410ceba"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5cb09abb18c1ea940fb99360ea0396f34d46566f157122c92dfa069d3e0e982"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666daae833559deb2d609afa4490b85830ab0dfca811a98b70a205621a6109fe"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11bdf3f5e1518b24530b8241529d2050014c884cf18b6fc69c0c2b30ca248710"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d18748f2d30f94f498e852c67d61261c643b349b9d2a581131725595c45ec6c"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:458f37be2d9e4c95e2d8866a851663cbc76e865b78395090786f6cd9b3bbf4f4"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b1a2eeedcead3a41694130495593a559a668f382eee0727352b9a41e1c45759a"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7d6ae9d593ef8641544d6263c7fa6408cc90370c8cb2bbb65f8d43e5b0351d9c"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5979b5632c3e3534e42ca6ff856bb24b2e3071b37861c2c727ce220d80eee9ed"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dcfe792765fab89c365123c81046ad4103fcabbc4f56d1c1997e6715e8015461"}, - {file = "multidict-6.0.4-cp311-cp311-win32.whl", hash = "sha256:3601a3cece3819534b11d4efc1eb76047488fddd0c85a3948099d5da4d504636"}, - {file = "multidict-6.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:81a4f0b34bd92df3da93315c6a59034df95866014ac08535fc819f043bfd51f0"}, - {file = "multidict-6.0.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:67040058f37a2a51ed8ea8f6b0e6ee5bd78ca67f169ce6122f3e2ec80dfe9b78"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:853888594621e6604c978ce2a0444a1e6e70c8d253ab65ba11657659dcc9100f"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:39ff62e7d0f26c248b15e364517a72932a611a9b75f35b45be078d81bdb86603"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af048912e045a2dc732847d33821a9d84ba553f5c5f028adbd364dd4765092ac"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1e8b901e607795ec06c9e42530788c45ac21ef3aaa11dbd0c69de543bfb79a9"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62501642008a8b9871ddfccbf83e4222cf8ac0d5aeedf73da36153ef2ec222d2"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:99b76c052e9f1bc0721f7541e5e8c05db3941eb9ebe7b8553c625ef88d6eefde"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:509eac6cf09c794aa27bcacfd4d62c885cce62bef7b2c3e8b2e49d365b5003fe"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:21a12c4eb6ddc9952c415f24eef97e3e55ba3af61f67c7bc388dcdec1404a067"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:5cad9430ab3e2e4fa4a2ef4450f548768400a2ac635841bc2a56a2052cdbeb87"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab55edc2e84460694295f401215f4a58597f8f7c9466faec545093045476327d"}, - {file = "multidict-6.0.4-cp37-cp37m-win32.whl", hash = "sha256:5a4dcf02b908c3b8b17a45fb0f15b695bf117a67b76b7ad18b73cf8e92608775"}, - {file = "multidict-6.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:6ed5f161328b7df384d71b07317f4d8656434e34591f20552c7bcef27b0ab88e"}, - {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5fc1b16f586f049820c5c5b17bb4ee7583092fa0d1c4e28b5239181ff9532e0c"}, - {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1502e24330eb681bdaa3eb70d6358e818e8e8f908a22a1851dfd4e15bc2f8161"}, - {file = "multidict-6.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b692f419760c0e65d060959df05f2a531945af31fda0c8a3b3195d4efd06de11"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45e1ecb0379bfaab5eef059f50115b54571acfbe422a14f668fc8c27ba410e7e"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddd3915998d93fbcd2566ddf9cf62cdb35c9e093075f862935573d265cf8f65d"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:59d43b61c59d82f2effb39a93c48b845efe23a3852d201ed2d24ba830d0b4cf2"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc8e1d0c705233c5dd0c5e6460fbad7827d5d36f310a0fadfd45cc3029762258"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6aa0418fcc838522256761b3415822626f866758ee0bc6632c9486b179d0b52"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6748717bb10339c4760c1e63da040f5f29f5ed6e59d76daee30305894069a660"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4d1a3d7ef5e96b1c9e92f973e43aa5e5b96c659c9bc3124acbbd81b0b9c8a951"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4372381634485bec7e46718edc71528024fcdc6f835baefe517b34a33c731d60"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:fc35cb4676846ef752816d5be2193a1e8367b4c1397b74a565a9d0389c433a1d"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b9d9e4e2b37daddb5c23ea33a3417901fa7c7b3dee2d855f63ee67a0b21e5b1"}, - {file = "multidict-6.0.4-cp38-cp38-win32.whl", hash = "sha256:e41b7e2b59679edfa309e8db64fdf22399eec4b0b24694e1b2104fb789207779"}, - {file = "multidict-6.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:d6c254ba6e45d8e72739281ebc46ea5eb5f101234f3ce171f0e9f5cc86991480"}, - {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:16ab77bbeb596e14212e7bab8429f24c1579234a3a462105cda4a66904998664"}, - {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc779e9e6f7fda81b3f9aa58e3a6091d49ad528b11ed19f6621408806204ad35"}, - {file = "multidict-6.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ceef517eca3e03c1cceb22030a3e39cb399ac86bff4e426d4fc6ae49052cc60"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:281af09f488903fde97923c7744bb001a9b23b039a909460d0f14edc7bf59706"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52f2dffc8acaba9a2f27174c41c9e57f60b907bb9f096b36b1a1f3be71c6284d"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b41156839806aecb3641f3208c0dafd3ac7775b9c4c422d82ee2a45c34ba81ca"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3fc56f88cc98ef8139255cf8cd63eb2c586531e43310ff859d6bb3a6b51f1"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8316a77808c501004802f9beebde51c9f857054a0c871bd6da8280e718444449"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f70b98cd94886b49d91170ef23ec5c0e8ebb6f242d734ed7ed677b24d50c82cf"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bf6774e60d67a9efe02b3616fee22441d86fab4c6d335f9d2051d19d90a40063"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e69924bfcdda39b722ef4d9aa762b2dd38e4632b3641b1d9a57ca9cd18f2f83a"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:6b181d8c23da913d4ff585afd1155a0e1194c0b50c54fcfe286f70cdaf2b7176"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:52509b5be062d9eafc8170e53026fbc54cf3b32759a23d07fd935fb04fc22d95"}, - {file = "multidict-6.0.4-cp39-cp39-win32.whl", hash = "sha256:27c523fbfbdfd19c6867af7346332b62b586eed663887392cff78d614f9ec313"}, - {file = "multidict-6.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2"}, - {file = "multidict-6.0.4.tar.gz", hash = "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49"}, -] [[package]] name = "mypy" @@ -1093,110 +828,7 @@ brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] -[[package]] -name = "yarl" -version = "1.9.4" -description = "Yet another URL library" -optional = false -python-versions = ">=3.7" -files = [ - {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e"}, - {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2"}, - {file = "yarl-1.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541"}, - {file = "yarl-1.9.4-cp310-cp310-win32.whl", hash = "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d"}, - {file = "yarl-1.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b"}, - {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099"}, - {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c"}, - {file = "yarl-1.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98"}, - {file = "yarl-1.9.4-cp311-cp311-win32.whl", hash = "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31"}, - {file = "yarl-1.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1"}, - {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81"}, - {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142"}, - {file = "yarl-1.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10"}, - {file = "yarl-1.9.4-cp312-cp312-win32.whl", hash = "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7"}, - {file = "yarl-1.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984"}, - {file = "yarl-1.9.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434"}, - {file = "yarl-1.9.4-cp37-cp37m-win32.whl", hash = "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749"}, - {file = "yarl-1.9.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2"}, - {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be"}, - {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f"}, - {file = "yarl-1.9.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3"}, - {file = "yarl-1.9.4-cp38-cp38-win32.whl", hash = "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece"}, - {file = "yarl-1.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b"}, - {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27"}, - {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1"}, - {file = "yarl-1.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0"}, - {file = "yarl-1.9.4-cp39-cp39-win32.whl", hash = "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575"}, - {file = "yarl-1.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15"}, - {file = "yarl-1.9.4-py3-none-any.whl", hash = "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad"}, - {file = "yarl-1.9.4.tar.gz", hash = "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf"}, -] - -[package.dependencies] -idna = ">=2.0" -multidict = ">=4.0" - [metadata] lock-version = "2.0" python-versions = ">=3.8.1,<4.0" -content-hash = "7773b2b3f8241dfbeff01c93e9ea16daabaa287ab4f11fdc7a1f673d476b2930" +content-hash = "72f02f84025d4cda9edb7f7105aecf65cc6341541143c45e5f2885c30aea5a0d" diff --git a/libs/partners/mistralai/pyproject.toml b/libs/partners/mistralai/pyproject.toml index 5ea94e070f330..75c87ec329d26 100644 --- a/libs/partners/mistralai/pyproject.toml +++ b/libs/partners/mistralai/pyproject.toml @@ -1,10 +1,11 @@ [tool.poetry] name = "langchain-mistralai" -version = "0.0.2.post1" +version = "0.0.3" description = "An integration package connecting Mistral and LangChain" authors = [] readme = "README.md" repository = "https://github.com/langchain-ai/langchain" +license = "MIT" [tool.poetry.urls] "Source Code" = "https://github.com/langchain-ai/langchain/tree/master/libs/partners/mistralai" @@ -12,7 +13,7 @@ repository = "https://github.com/langchain-ai/langchain" [tool.poetry.dependencies] python = ">=3.8.1,<4.0" langchain-core = "^0.1" -mistralai = "^0.0.8" +mistralai = "^0.0.11" [tool.poetry.group.test] optional = true diff --git a/libs/partners/mistralai/tests/integration_tests/test_embeddings.py b/libs/partners/mistralai/tests/integration_tests/test_embeddings.py new file mode 100644 index 0000000000000..166bf5c5c15be --- /dev/null +++ b/libs/partners/mistralai/tests/integration_tests/test_embeddings.py @@ -0,0 +1,53 @@ +"""Test MistralAI Embedding""" +from langchain_mistralai import MistralAIEmbeddings + + +def test_mistralai_embedding_documents() -> None: + """Test MistralAI embeddings for documents.""" + documents = ["foo bar", "test document"] + embedding = MistralAIEmbeddings() + output = embedding.embed_documents(documents) + assert len(output) == 2 + assert len(output[0]) == 1024 + + +def test_mistralai_embedding_query() -> None: + """Test MistralAI embeddings for query.""" + document = "foo bar" + embedding = MistralAIEmbeddings() + output = embedding.embed_query(document) + assert len(output) == 1024 + + +async def test_mistralai_embedding_documents_async() -> None: + """Test MistralAI embeddings for documents.""" + documents = ["foo bar", "test document"] + embedding = MistralAIEmbeddings() + output = await embedding.aembed_documents(documents) + assert len(output) == 2 + assert len(output[0]) == 1024 + + +async def test_mistralai_embedding_query_async() -> None: + """Test MistralAI embeddings for query.""" + document = "foo bar" + embedding = MistralAIEmbeddings() + output = await embedding.aembed_query(document) + assert len(output) == 1024 + + +def test_mistralai_embedding_documents_long() -> None: + """Test MistralAI embeddings for documents.""" + documents = ["foo bar " * 1000, "test document " * 1000] + embedding = MistralAIEmbeddings() + output = embedding.embed_documents(documents) + assert len(output) == 2 + assert len(output[0]) == 1024 + + +def test_mistralai_embed_query_character() -> None: + """Test MistralAI embeddings for query.""" + document = "😳" + embedding = MistralAIEmbeddings() + output = embedding.embed_query(document) + assert len(output) == 1024 diff --git a/libs/partners/mistralai/tests/unit_tests/test_embeddings.py b/libs/partners/mistralai/tests/unit_tests/test_embeddings.py new file mode 100644 index 0000000000000..14055af4ed7d5 --- /dev/null +++ b/libs/partners/mistralai/tests/unit_tests/test_embeddings.py @@ -0,0 +1,10 @@ +import os + +from langchain_mistralai import MistralAIEmbeddings + +os.environ["MISTRAL_API_KEY"] = "foo" + + +def test_mistral_init() -> None: + embeddings = MistralAIEmbeddings() + assert embeddings.model == "mistral-embed" diff --git a/libs/partners/mistralai/tests/unit_tests/test_imports.py b/libs/partners/mistralai/tests/unit_tests/test_imports.py index d1caf29c067ae..01c220d64c9d2 100644 --- a/libs/partners/mistralai/tests/unit_tests/test_imports.py +++ b/libs/partners/mistralai/tests/unit_tests/test_imports.py @@ -1,6 +1,6 @@ from langchain_mistralai import __all__ -EXPECTED_ALL = ["ChatMistralAI"] +EXPECTED_ALL = ["ChatMistralAI", "MistralAIEmbeddings"] def test_all_imports() -> None: diff --git a/libs/partners/nvidia-ai-endpoints/pyproject.toml b/libs/partners/nvidia-ai-endpoints/pyproject.toml index 6ba0e0ca90cd5..0146a6f75f521 100644 --- a/libs/partners/nvidia-ai-endpoints/pyproject.toml +++ b/libs/partners/nvidia-ai-endpoints/pyproject.toml @@ -1,10 +1,11 @@ [tool.poetry] name = "langchain-nvidia-ai-endpoints" -version = "0.0.1.post1" +version = "0.0.1.post2" description = "An integration package connecting NVIDIA AI Endpoints and LangChain" authors = [] readme = "README.md" repository = "https://github.com/langchain-ai/langchain" +license = "MIT" [tool.poetry.urls] "Source Code" = "https://github.com/langchain-ai/langchain/tree/master/libs/partners/nvidia-ai-endpoints" diff --git a/libs/partners/nvidia-trt/pyproject.toml b/libs/partners/nvidia-trt/pyproject.toml index a8847942f7648..2cbaaa6be1403 100644 --- a/libs/partners/nvidia-trt/pyproject.toml +++ b/libs/partners/nvidia-trt/pyproject.toml @@ -5,6 +5,7 @@ description = "An integration package connecting TritonTensorRT and LangChain" authors = [] readme = "README.md" repository = "https://github.com/langchain-ai/langchain" +license = "MIT" [tool.poetry.urls] "Source Code" = "https://github.com/langchain-ai/langchain/tree/master/libs/partners/nvidia-trt" diff --git a/libs/partners/openai/langchain_openai/chat_models/azure.py b/libs/partners/openai/langchain_openai/chat_models/azure.py index 8c57825c6ffdc..149a5a66c91ed 100644 --- a/libs/partners/openai/langchain_openai/chat_models/azure.py +++ b/libs/partners/openai/langchain_openai/chat_models/azure.py @@ -151,11 +151,16 @@ def validate_environment(cls, values: Dict) -> Dict: ) if values["deployment_name"]: raise ValueError( - "As of openai>=1.0.0, if `deployment_name` (or alias " - "`azure_deployment`) is specified then " - "`openai_api_base` (or alias `base_url`) should not be. " - "Instead use `deployment_name` (or alias `azure_deployment`) " - "and `azure_endpoint`." + "As of openai>=1.0.0, if `azure_deployment` (or alias " + "`deployment_name`) is specified then " + "`base_url` (or alias `openai_api_base`) should not be. " + "If specifying `azure_deployment`/`deployment_name` then use " + "`azure_endpoint` instead of `base_url`.\n\n" + "For example, you could specify:\n\n" + 'azure_deployment="https://xxx.openai.azure.com/", ' + 'deployment_name="my-deployment"\n\n' + "Or you can equivalently specify:\n\n" + 'base_url="https://xxx.openai.azure.com/openai/deployments/my-deployment"' # noqa: E501 ) client_params = { "api_version": values["openai_api_version"], diff --git a/libs/partners/openai/langchain_openai/chat_models/base.py b/libs/partners/openai/langchain_openai/chat_models/base.py index 06bd22485cf2d..10d1dadf22ead 100644 --- a/libs/partners/openai/langchain_openai/chat_models/base.py +++ b/libs/partners/openai/langchain_openai/chat_models/base.py @@ -404,15 +404,19 @@ def _stream( chunk = _convert_delta_to_message_chunk( choice["delta"], default_chunk_class ) - finish_reason = choice.get("finish_reason") - generation_info = ( - dict(finish_reason=finish_reason) if finish_reason is not None else None - ) + generation_info = {} + if finish_reason := choice.get("finish_reason"): + generation_info["finish_reason"] = finish_reason + logprobs = choice.get("logprobs") + if logprobs: + generation_info["logprobs"] = logprobs default_chunk_class = chunk.__class__ - chunk = ChatGenerationChunk(message=chunk, generation_info=generation_info) + chunk = ChatGenerationChunk( + message=chunk, generation_info=generation_info or None + ) yield chunk if run_manager: - run_manager.on_llm_new_token(chunk.text, chunk=chunk) + run_manager.on_llm_new_token(chunk.text, chunk=chunk, logprobs=logprobs) def _generate( self, @@ -492,15 +496,21 @@ async def _astream( chunk = _convert_delta_to_message_chunk( choice["delta"], default_chunk_class ) - finish_reason = choice.get("finish_reason") - generation_info = ( - dict(finish_reason=finish_reason) if finish_reason is not None else None - ) + generation_info = {} + if finish_reason := choice.get("finish_reason"): + generation_info["finish_reason"] = finish_reason + logprobs = choice.get("logprobs") + if logprobs: + generation_info["logprobs"] = logprobs default_chunk_class = chunk.__class__ - chunk = ChatGenerationChunk(message=chunk, generation_info=generation_info) + chunk = ChatGenerationChunk( + message=chunk, generation_info=generation_info or None + ) yield chunk if run_manager: - await run_manager.on_llm_new_token(token=chunk.text, chunk=chunk) + await run_manager.on_llm_new_token( + token=chunk.text, chunk=chunk, logprobs=logprobs + ) async def _agenerate( self, diff --git a/libs/partners/openai/poetry.lock b/libs/partners/openai/poetry.lock index 8f4681543466e..465391d040454 100644 --- a/libs/partners/openai/poetry.lock +++ b/libs/partners/openai/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. [[package]] name = "annotated-types" @@ -318,7 +318,7 @@ files = [ [[package]] name = "langchain-core" -version = "0.1.7" +version = "0.1.13" description = "Building applications with LLMs through composability" optional = false python-versions = ">=3.8.1,<4.0" @@ -328,7 +328,7 @@ develop = true [package.dependencies] anyio = ">=3,<5" jsonpatch = "^1.33" -langsmith = "~0.0.63" +langsmith = "^0.0.83" packaging = "^23.2" pydantic = ">=1,<3" PyYAML = ">=5.3" @@ -344,13 +344,13 @@ url = "../../core" [[package]] name = "langsmith" -version = "0.0.75" +version = "0.0.83" description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform." optional = false python-versions = ">=3.8.1,<4.0" files = [ - {file = "langsmith-0.0.75-py3-none-any.whl", hash = "sha256:3e008854204c5eaae007f34c7e249059218605689c385c037f6a40cac044833b"}, - {file = "langsmith-0.0.75.tar.gz", hash = "sha256:3fd44c58bd53cb9366af3de129c7f11b6947914f1bb598a585240df0e2c566eb"}, + {file = "langsmith-0.0.83-py3-none-any.whl", hash = "sha256:a5bb7ac58c19a415a9d5f51db56dd32ee2cd7343a00825bbc2018312eb3d122a"}, + {file = "langsmith-0.0.83.tar.gz", hash = "sha256:94427846b334ad9bdbec3266fee12903fe9f5448f628667689d0412012aaf392"}, ] [package.dependencies] @@ -738,6 +738,7 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -745,8 +746,15 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -763,6 +771,7 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -770,6 +779,7 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -1137,4 +1147,4 @@ watchmedo = ["PyYAML (>=3.10)"] [metadata] lock-version = "2.0" python-versions = ">=3.8.1,<4.0" -content-hash = "19354033ba1c0b24094244fb6429e814dea20bdfde4dc67254710cbb7f410d50" +content-hash = "864d5c8b19403aae2a5658e042d4c0deb64fb9ce89b2bd3e751b5c0a1bf8dc68" diff --git a/libs/partners/openai/pyproject.toml b/libs/partners/openai/pyproject.toml index 3cb369ca2eda9..a6f419780037b 100644 --- a/libs/partners/openai/pyproject.toml +++ b/libs/partners/openai/pyproject.toml @@ -1,17 +1,18 @@ [tool.poetry] name = "langchain-openai" -version = "0.0.2.post1" +version = "0.0.3" description = "An integration package connecting OpenAI and LangChain" authors = [] readme = "README.md" repository = "https://github.com/langchain-ai/langchain" +license = "MIT" [tool.poetry.urls] "Source Code" = "https://github.com/langchain-ai/langchain/tree/master/libs/partners/openai" [tool.poetry.dependencies] python = ">=3.8.1,<4.0" -langchain-core = ">=0.1.7,<0.2" +langchain-core = ">=0.1.13,<0.2" openai = "^1.6.1" numpy = "^1" tiktoken = "^0.5.2" diff --git a/libs/partners/openai/tests/integration_tests/chat_models/test_base.py b/libs/partners/openai/tests/integration_tests/chat_models/test_base.py index c86112891f139..33006a624cf32 100644 --- a/libs/partners/openai/tests/integration_tests/chat_models/test_base.py +++ b/libs/partners/openai/tests/integration_tests/chat_models/test_base.py @@ -391,3 +391,37 @@ def test_invoke() -> None: result = llm.invoke("I'm Pickle Rick", config=dict(tags=["foo"])) assert isinstance(result.content, str) + + +def test_logprobs() -> None: + llm = ChatOpenAI() + result = llm.generate([[HumanMessage(content="I'm PickleRick")]], logprobs=True) + assert result.generations[0][0].generation_info + assert "content" in result.generations[0][0].generation_info["logprobs"] + + +async def test_async_logprobs() -> None: + llm = ChatOpenAI() + result = await llm.agenerate( + [[HumanMessage(content="I'm PickleRick")]], logprobs=True + ) + assert result.generations[0][0].generation_info + assert "content" in result.generations[0][0].generation_info["logprobs"] + + +def test_logprobs_streaming() -> None: + llm = ChatOpenAI() + result = llm.generate( + [[HumanMessage(content="I'm PickleRick")]], logprobs=True, stream=True + ) + assert result.generations[0][0].generation_info + assert "content" in result.generations[0][0].generation_info["logprobs"] + + +async def test_async_logprobs_streaming() -> None: + llm = ChatOpenAI() + result = await llm.agenerate( + [[HumanMessage(content="I'm PickleRick")]], logprobs=True, stream=True + ) + assert result.generations[0][0].generation_info + assert "content" in result.generations[0][0].generation_info["logprobs"] diff --git a/libs/partners/robocorp/poetry.lock b/libs/partners/robocorp/poetry.lock index a7ce2dbefcea1..468bfa069324c 100644 --- a/libs/partners/robocorp/poetry.lock +++ b/libs/partners/robocorp/poetry.lock @@ -251,7 +251,7 @@ files = [ [[package]] name = "langchain-core" -version = "0.1.8" +version = "0.1.14" description = "Building applications with LLMs through composability" optional = false python-versions = ">=3.8.1,<4.0" @@ -261,7 +261,7 @@ develop = true [package.dependencies] anyio = ">=3,<5" jsonpatch = "^1.33" -langsmith = "~0.0.63" +langsmith = ">=0.0.83,<0.1" packaging = "^23.2" pydantic = ">=1,<3" PyYAML = ">=5.3" @@ -277,13 +277,13 @@ url = "../../core" [[package]] name = "langsmith" -version = "0.0.77" +version = "0.0.83" description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform." optional = false python-versions = ">=3.8.1,<4.0" files = [ - {file = "langsmith-0.0.77-py3-none-any.whl", hash = "sha256:750c0aa9177240c64e131d831e009ed08dd59038f7cabbd0bbcf62ccb7c8dcac"}, - {file = "langsmith-0.0.77.tar.gz", hash = "sha256:c4c8d3a96ad8671a41064f3ccc673e2e22a4153e823b19f915c9c9b8a4f33a2c"}, + {file = "langsmith-0.0.83-py3-none-any.whl", hash = "sha256:a5bb7ac58c19a415a9d5f51db56dd32ee2cd7343a00825bbc2018312eb3d122a"}, + {file = "langsmith-0.0.83.tar.gz", hash = "sha256:94427846b334ad9bdbec3266fee12903fe9f5448f628667689d0412012aaf392"}, ] [package.dependencies] @@ -839,4 +839,4 @@ watchmedo = ["PyYAML (>=3.10)"] [metadata] lock-version = "2.0" python-versions = ">=3.8.1,<4.0" -content-hash = "09efecb6b12a87a6f0662b33a341e069c09413fb35d99cc9b7da447bb2298bd2" +content-hash = "e64f42191e7c016d9d198a2b183d03fd245add9dc8e14632451e56bcf8a2aecd" diff --git a/libs/partners/robocorp/pyproject.toml b/libs/partners/robocorp/pyproject.toml index c80e5d81ed51b..add0f6d9cb490 100644 --- a/libs/partners/robocorp/pyproject.toml +++ b/libs/partners/robocorp/pyproject.toml @@ -1,10 +1,11 @@ [tool.poetry] name = "langchain-robocorp" -version = "0.0.1.post2" +version = "0.0.1.post3" description = "An integration package connecting Robocorp and LangChain" authors = [] readme = "README.md" repository = "https://github.com/langchain-ai/langchain" +license = "MIT" [tool.poetry.urls] "Source Code" = "https://github.com/langchain-ai/langchain/tree/master/libs/partners/robocorp" @@ -14,7 +15,7 @@ python = ">=3.8.1,<4.0" langchain-core = ">=0.0.12" requests = "^2.31.0" types-requests = "^2.31.0.20231231" -langsmith = "^0.0.77" +langsmith = ">=0.0.83,<0.1" [tool.poetry.group.test] optional = true diff --git a/libs/partners/together/pyproject.toml b/libs/partners/together/pyproject.toml index 1193130016117..6bf4795ce1437 100644 --- a/libs/partners/together/pyproject.toml +++ b/libs/partners/together/pyproject.toml @@ -1,10 +1,11 @@ [tool.poetry] name = "langchain-together" -version = "0.0.2.post1" +version = "0.0.2.post2" description = "An integration package connecting Together and LangChain" authors = [] readme = "README.md" repository = "https://github.com/langchain-ai/langchain" +license = "MIT" [tool.poetry.urls] "Source Code" = "https://github.com/langchain-ai/langchain/tree/master/libs/partners/together" diff --git a/pg_essay.txt b/pg_essay.txt new file mode 100644 index 0000000000000..0bce3830b9968 --- /dev/null +++ b/pg_essay.txt @@ -0,0 +1,351 @@ +What I Worked On + +February 2021 + +Before college the two main things I worked on, outside of school, were writing and programming. I didn't write essays. I wrote what beginning writers were supposed to write then, and probably still are: short stories. My stories were awful. They had hardly any plot, just characters with strong feelings, which I imagined made them deep. + +The first programs I tried writing were on the IBM 1401 that our school district used for what was then called "data processing." This was in 9th grade, so I was 13 or 14. The school district's 1401 happened to be in the basement of our junior high school, and my friend Rich Draves and I got permission to use it. It was like a mini Bond villain's lair down there, with all these alien-looking machines — CPU, disk drives, printer, card reader — sitting up on a raised floor under bright fluorescent lights. + +The language we used was an early version of Fortran. You had to type programs on punch cards, then stack them in the card reader and press a button to load the program into memory and run it. The result would ordinarily be to print something on the spectacularly loud printer. + +I was puzzled by the 1401. I couldn't figure out what to do with it. And in retrospect there's not much I could have done with it. The only form of input to programs was data stored on punched cards, and I didn't have any data stored on punched cards. The only other option was to do things that didn't rely on any input, like calculate approximations of pi, but I didn't know enough math to do anything interesting of that type. So I'm not surprised I can't remember any programs I wrote, because they can't have done much. My clearest memory is of the moment I learned it was possible for programs not to terminate, when one of mine didn't. On a machine without time-sharing, this was a social as well as a technical error, as the data center manager's expression made clear. + +With microcomputers, everything changed. Now you could have a computer sitting right in front of you, on a desk, that could respond to your keystrokes as it was running instead of just churning through a stack of punch cards and then stopping. [1] + +The first of my friends to get a microcomputer built it himself. It was sold as a kit by Heathkit. I remember vividly how impressed and envious I felt watching him sitting in front of it, typing programs right into the computer. + +Computers were expensive in those days and it took me years of nagging before I convinced my father to buy one, a TRS-80, in about 1980. The gold standard then was the Apple II, but a TRS-80 was good enough. This was when I really started programming. I wrote simple games, a program to predict how high my model rockets would fly, and a word processor that my father used to write at least one book. There was only room in memory for about 2 pages of text, so he'd write 2 pages at a time and then print them out, but it was a lot better than a typewriter. + +Though I liked programming, I didn't plan to study it in college. In college I was going to study philosophy, which sounded much more powerful. It seemed, to my naive high school self, to be the study of the ultimate truths, compared to which the things studied in other fields would be mere domain knowledge. What I discovered when I got to college was that the other fields took up so much of the space of ideas that there wasn't much left for these supposed ultimate truths. All that seemed left for philosophy were edge cases that people in other fields felt could safely be ignored. + +I couldn't have put this into words when I was 18. All I knew at the time was that I kept taking philosophy courses and they kept being boring. So I decided to switch to AI. + +AI was in the air in the mid 1980s, but there were two things especially that made me want to work on it: a novel by Heinlein called The Moon is a Harsh Mistress, which featured an intelligent computer called Mike, and a PBS documentary that showed Terry Winograd using SHRDLU. I haven't tried rereading The Moon is a Harsh Mistress, so I don't know how well it has aged, but when I read it I was drawn entirely into its world. It seemed only a matter of time before we'd have Mike, and when I saw Winograd using SHRDLU, it seemed like that time would be a few years at most. All you had to do was teach SHRDLU more words. + +There weren't any classes in AI at Cornell then, not even graduate classes, so I started trying to teach myself. Which meant learning Lisp, since in those days Lisp was regarded as the language of AI. The commonly used programming languages then were pretty primitive, and programmers' ideas correspondingly so. The default language at Cornell was a Pascal-like language called PL/I, and the situation was similar elsewhere. Learning Lisp expanded my concept of a program so fast that it was years before I started to have a sense of where the new limits were. This was more like it; this was what I had expected college to do. It wasn't happening in a class, like it was supposed to, but that was ok. For the next couple years I was on a roll. I knew what I was going to do. + +For my undergraduate thesis, I reverse-engineered SHRDLU. My God did I love working on that program. It was a pleasing bit of code, but what made it even more exciting was my belief — hard to imagine now, but not unique in 1985 — that it was already climbing the lower slopes of intelligence. + +I had gotten into a program at Cornell that didn't make you choose a major. You could take whatever classes you liked, and choose whatever you liked to put on your degree. I of course chose "Artificial Intelligence." When I got the actual physical diploma, I was dismayed to find that the quotes had been included, which made them read as scare-quotes. At the time this bothered me, but now it seems amusingly accurate, for reasons I was about to discover. + +I applied to 3 grad schools: MIT and Yale, which were renowned for AI at the time, and Harvard, which I'd visited because Rich Draves went there, and was also home to Bill Woods, who'd invented the type of parser I used in my SHRDLU clone. Only Harvard accepted me, so that was where I went. + +I don't remember the moment it happened, or if there even was a specific moment, but during the first year of grad school I realized that AI, as practiced at the time, was a hoax. By which I mean the sort of AI in which a program that's told "the dog is sitting on the chair" translates this into some formal representation and adds it to the list of things it knows. + +What these programs really showed was that there's a subset of natural language that's a formal language. But a very proper subset. It was clear that there was an unbridgeable gap between what they could do and actually understanding natural language. It was not, in fact, simply a matter of teaching SHRDLU more words. That whole way of doing AI, with explicit data structures representing concepts, was not going to work. Its brokenness did, as so often happens, generate a lot of opportunities to write papers about various band-aids that could be applied to it, but it was never going to get us Mike. + +So I looked around to see what I could salvage from the wreckage of my plans, and there was Lisp. I knew from experience that Lisp was interesting for its own sake and not just for its association with AI, even though that was the main reason people cared about it at the time. So I decided to focus on Lisp. In fact, I decided to write a book about Lisp hacking. It's scary to think how little I knew about Lisp hacking when I started writing that book. But there's nothing like writing a book about something to help you learn it. The book, On Lisp, wasn't published till 1993, but I wrote much of it in grad school. + +Computer Science is an uneasy alliance between two halves, theory and systems. The theory people prove things, and the systems people build things. I wanted to build things. I had plenty of respect for theory — indeed, a sneaking suspicion that it was the more admirable of the two halves — but building things seemed so much more exciting. + +The problem with systems work, though, was that it didn't last. Any program you wrote today, no matter how good, would be obsolete in a couple decades at best. People might mention your software in footnotes, but no one would actually use it. And indeed, it would seem very feeble work. Only people with a sense of the history of the field would even realize that, in its time, it had been good. + +There were some surplus Xerox Dandelions floating around the computer lab at one point. Anyone who wanted one to play around with could have one. I was briefly tempted, but they were so slow by present standards; what was the point? No one else wanted one either, so off they went. That was what happened to systems work. + +I wanted not just to build things, but to build things that would last. + +In this dissatisfied state I went in 1988 to visit Rich Draves at CMU, where he was in grad school. One day I went to visit the Carnegie Institute, where I'd spent a lot of time as a kid. While looking at a painting there I realized something that might seem obvious, but was a big surprise to me. There, right on the wall, was something you could make that would last. Paintings didn't become obsolete. Some of the best ones were hundreds of years old. + +And moreover this was something you could make a living doing. Not as easily as you could by writing software, of course, but I thought if you were really industrious and lived really cheaply, it had to be possible to make enough to survive. And as an artist you could be truly independent. You wouldn't have a boss, or even need to get research funding. + +I had always liked looking at paintings. Could I make them? I had no idea. I'd never imagined it was even possible. I knew intellectually that people made art — that it didn't just appear spontaneously — but it was as if the people who made it were a different species. They either lived long ago or were mysterious geniuses doing strange things in profiles in Life magazine. The idea of actually being able to make art, to put that verb before that noun, seemed almost miraculous. + +That fall I started taking art classes at Harvard. Grad students could take classes in any department, and my advisor, Tom Cheatham, was very easy going. If he even knew about the strange classes I was taking, he never said anything. + +So now I was in a PhD program in computer science, yet planning to be an artist, yet also genuinely in love with Lisp hacking and working away at On Lisp. In other words, like many a grad student, I was working energetically on multiple projects that were not my thesis. + +I didn't see a way out of this situation. I didn't want to drop out of grad school, but how else was I going to get out? I remember when my friend Robert Morris got kicked out of Cornell for writing the internet worm of 1988, I was envious that he'd found such a spectacular way to get out of grad school. + +Then one day in April 1990 a crack appeared in the wall. I ran into professor Cheatham and he asked if I was far enough along to graduate that June. I didn't have a word of my dissertation written, but in what must have been the quickest bit of thinking in my life, I decided to take a shot at writing one in the 5 weeks or so that remained before the deadline, reusing parts of On Lisp where I could, and I was able to respond, with no perceptible delay "Yes, I think so. I'll give you something to read in a few days." + +I picked applications of continuations as the topic. In retrospect I should have written about macros and embedded languages. There's a whole world there that's barely been explored. But all I wanted was to get out of grad school, and my rapidly written dissertation sufficed, just barely. + +Meanwhile I was applying to art schools. I applied to two: RISD in the US, and the Accademia di Belli Arti in Florence, which, because it was the oldest art school, I imagined would be good. RISD accepted me, and I never heard back from the Accademia, so off to Providence I went. + +I'd applied for the BFA program at RISD, which meant in effect that I had to go to college again. This was not as strange as it sounds, because I was only 25, and art schools are full of people of different ages. RISD counted me as a transfer sophomore and said I had to do the foundation that summer. The foundation means the classes that everyone has to take in fundamental subjects like drawing, color, and design. + +Toward the end of the summer I got a big surprise: a letter from the Accademia, which had been delayed because they'd sent it to Cambridge England instead of Cambridge Massachusetts, inviting me to take the entrance exam in Florence that fall. This was now only weeks away. My nice landlady let me leave my stuff in her attic. I had some money saved from consulting work I'd done in grad school; there was probably enough to last a year if I lived cheaply. Now all I had to do was learn Italian. + +Only stranieri (foreigners) had to take this entrance exam. In retrospect it may well have been a way of excluding them, because there were so many stranieri attracted by the idea of studying art in Florence that the Italian students would otherwise have been outnumbered. I was in decent shape at painting and drawing from the RISD foundation that summer, but I still don't know how I managed to pass the written exam. I remember that I answered the essay question by writing about Cezanne, and that I cranked up the intellectual level as high as I could to make the most of my limited vocabulary. [2] + +I'm only up to age 25 and already there are such conspicuous patterns. Here I was, yet again about to attend some august institution in the hopes of learning about some prestigious subject, and yet again about to be disappointed. The students and faculty in the painting department at the Accademia were the nicest people you could imagine, but they had long since arrived at an arrangement whereby the students wouldn't require the faculty to teach anything, and in return the faculty wouldn't require the students to learn anything. And at the same time all involved would adhere outwardly to the conventions of a 19th century atelier. We actually had one of those little stoves, fed with kindling, that you see in 19th century studio paintings, and a nude model sitting as close to it as possible without getting burned. Except hardly anyone else painted her besides me. The rest of the students spent their time chatting or occasionally trying to imitate things they'd seen in American art magazines. + +Our model turned out to live just down the street from me. She made a living from a combination of modelling and making fakes for a local antique dealer. She'd copy an obscure old painting out of a book, and then he'd take the copy and maltreat it to make it look old. [3] + +While I was a student at the Accademia I started painting still lives in my bedroom at night. These paintings were tiny, because the room was, and because I painted them on leftover scraps of canvas, which was all I could afford at the time. Painting still lives is different from painting people, because the subject, as its name suggests, can't move. People can't sit for more than about 15 minutes at a time, and when they do they don't sit very still. So the traditional m.o. for painting people is to know how to paint a generic person, which you then modify to match the specific person you're painting. Whereas a still life you can, if you want, copy pixel by pixel from what you're seeing. You don't want to stop there, of course, or you get merely photographic accuracy, and what makes a still life interesting is that it's been through a head. You want to emphasize the visual cues that tell you, for example, that the reason the color changes suddenly at a certain point is that it's the edge of an object. By subtly emphasizing such things you can make paintings that are more realistic than photographs not just in some metaphorical sense, but in the strict information-theoretic sense. [4] + +I liked painting still lives because I was curious about what I was seeing. In everyday life, we aren't consciously aware of much we're seeing. Most visual perception is handled by low-level processes that merely tell your brain "that's a water droplet" without telling you details like where the lightest and darkest points are, or "that's a bush" without telling you the shape and position of every leaf. This is a feature of brains, not a bug. In everyday life it would be distracting to notice every leaf on every bush. But when you have to paint something, you have to look more closely, and when you do there's a lot to see. You can still be noticing new things after days of trying to paint something people usually take for granted, just as you can after days of trying to write an essay about something people usually take for granted. + +This is not the only way to paint. I'm not 100% sure it's even a good way to paint. But it seemed a good enough bet to be worth trying. + +Our teacher, professor Ulivi, was a nice guy. He could see I worked hard, and gave me a good grade, which he wrote down in a sort of passport each student had. But the Accademia wasn't teaching me anything except Italian, and my money was running out, so at the end of the first year I went back to the US. + +I wanted to go back to RISD, but I was now broke and RISD was very expensive, so I decided to get a job for a year and then return to RISD the next fall. I got one at a company called Interleaf, which made software for creating documents. You mean like Microsoft Word? Exactly. That was how I learned that low end software tends to eat high end software. But Interleaf still had a few years to live yet. [5] + +Interleaf had done something pretty bold. Inspired by Emacs, they'd added a scripting language, and even made the scripting language a dialect of Lisp. Now they wanted a Lisp hacker to write things in it. This was the closest thing I've had to a normal job, and I hereby apologize to my boss and coworkers, because I was a bad employee. Their Lisp was the thinnest icing on a giant C cake, and since I didn't know C and didn't want to learn it, I never understood most of the software. Plus I was terribly irresponsible. This was back when a programming job meant showing up every day during certain working hours. That seemed unnatural to me, and on this point the rest of the world is coming around to my way of thinking, but at the time it caused a lot of friction. Toward the end of the year I spent much of my time surreptitiously working on On Lisp, which I had by this time gotten a contract to publish. + +The good part was that I got paid huge amounts of money, especially by art student standards. In Florence, after paying my part of the rent, my budget for everything else had been $7 a day. Now I was getting paid more than 4 times that every hour, even when I was just sitting in a meeting. By living cheaply I not only managed to save enough to go back to RISD, but also paid off my college loans. + +I learned some useful things at Interleaf, though they were mostly about what not to do. I learned that it's better for technology companies to be run by product people than sales people (though sales is a real skill and people who are good at it are really good at it), that it leads to bugs when code is edited by too many people, that cheap office space is no bargain if it's depressing, that planned meetings are inferior to corridor conversations, that big, bureaucratic customers are a dangerous source of money, and that there's not much overlap between conventional office hours and the optimal time for hacking, or conventional offices and the optimal place for it. + +But the most important thing I learned, and which I used in both Viaweb and Y Combinator, is that the low end eats the high end: that it's good to be the "entry level" option, even though that will be less prestigious, because if you're not, someone else will be, and will squash you against the ceiling. Which in turn means that prestige is a danger sign. + +When I left to go back to RISD the next fall, I arranged to do freelance work for the group that did projects for customers, and this was how I survived for the next several years. When I came back to visit for a project later on, someone told me about a new thing called HTML, which was, as he described it, a derivative of SGML. Markup language enthusiasts were an occupational hazard at Interleaf and I ignored him, but this HTML thing later became a big part of my life. + +In the fall of 1992 I moved back to Providence to continue at RISD. The foundation had merely been intro stuff, and the Accademia had been a (very civilized) joke. Now I was going to see what real art school was like. But alas it was more like the Accademia than not. Better organized, certainly, and a lot more expensive, but it was now becoming clear that art school did not bear the same relationship to art that medical school bore to medicine. At least not the painting department. The textile department, which my next door neighbor belonged to, seemed to be pretty rigorous. No doubt illustration and architecture were too. But painting was post-rigorous. Painting students were supposed to express themselves, which to the more worldly ones meant to try to cook up some sort of distinctive signature style. + +A signature style is the visual equivalent of what in show business is known as a "schtick": something that immediately identifies the work as yours and no one else's. For example, when you see a painting that looks like a certain kind of cartoon, you know it's by Roy Lichtenstein. So if you see a big painting of this type hanging in the apartment of a hedge fund manager, you know he paid millions of dollars for it. That's not always why artists have a signature style, but it's usually why buyers pay a lot for such work. [6] + +There were plenty of earnest students too: kids who "could draw" in high school, and now had come to what was supposed to be the best art school in the country, to learn to draw even better. They tended to be confused and demoralized by what they found at RISD, but they kept going, because painting was what they did. I was not one of the kids who could draw in high school, but at RISD I was definitely closer to their tribe than the tribe of signature style seekers. + +I learned a lot in the color class I took at RISD, but otherwise I was basically teaching myself to paint, and I could do that for free. So in 1993 I dropped out. I hung around Providence for a bit, and then my college friend Nancy Parmet did me a big favor. A rent-controlled apartment in a building her mother owned in New York was becoming vacant. Did I want it? It wasn't much more than my current place, and New York was supposed to be where the artists were. So yes, I wanted it! [7] + +Asterix comics begin by zooming in on a tiny corner of Roman Gaul that turns out not to be controlled by the Romans. You can do something similar on a map of New York City: if you zoom in on the Upper East Side, there's a tiny corner that's not rich, or at least wasn't in 1993. It's called Yorkville, and that was my new home. Now I was a New York artist — in the strictly technical sense of making paintings and living in New York. + +I was nervous about money, because I could sense that Interleaf was on the way down. Freelance Lisp hacking work was very rare, and I didn't want to have to program in another language, which in those days would have meant C++ if I was lucky. So with my unerring nose for financial opportunity, I decided to write another book on Lisp. This would be a popular book, the sort of book that could be used as a textbook. I imagined myself living frugally off the royalties and spending all my time painting. (The painting on the cover of this book, ANSI Common Lisp, is one that I painted around this time.) + +The best thing about New York for me was the presence of Idelle and Julian Weber. Idelle Weber was a painter, one of the early photorealists, and I'd taken her painting class at Harvard. I've never known a teacher more beloved by her students. Large numbers of former students kept in touch with her, including me. After I moved to New York I became her de facto studio assistant. + +She liked to paint on big, square canvases, 4 to 5 feet on a side. One day in late 1994 as I was stretching one of these monsters there was something on the radio about a famous fund manager. He wasn't that much older than me, and was super rich. The thought suddenly occurred to me: why don't I become rich? Then I'll be able to work on whatever I want. + +Meanwhile I'd been hearing more and more about this new thing called the World Wide Web. Robert Morris showed it to me when I visited him in Cambridge, where he was now in grad school at Harvard. It seemed to me that the web would be a big deal. I'd seen what graphical user interfaces had done for the popularity of microcomputers. It seemed like the web would do the same for the internet. + +If I wanted to get rich, here was the next train leaving the station. I was right about that part. What I got wrong was the idea. I decided we should start a company to put art galleries online. I can't honestly say, after reading so many Y Combinator applications, that this was the worst startup idea ever, but it was up there. Art galleries didn't want to be online, and still don't, not the fancy ones. That's not how they sell. I wrote some software to generate web sites for galleries, and Robert wrote some to resize images and set up an http server to serve the pages. Then we tried to sign up galleries. To call this a difficult sale would be an understatement. It was difficult to give away. A few galleries let us make sites for them for free, but none paid us. + +Then some online stores started to appear, and I realized that except for the order buttons they were identical to the sites we'd been generating for galleries. This impressive-sounding thing called an "internet storefront" was something we already knew how to build. + +So in the summer of 1995, after I submitted the camera-ready copy of ANSI Common Lisp to the publishers, we started trying to write software to build online stores. At first this was going to be normal desktop software, which in those days meant Windows software. That was an alarming prospect, because neither of us knew how to write Windows software or wanted to learn. We lived in the Unix world. But we decided we'd at least try writing a prototype store builder on Unix. Robert wrote a shopping cart, and I wrote a new site generator for stores — in Lisp, of course. + +We were working out of Robert's apartment in Cambridge. His roommate was away for big chunks of time, during which I got to sleep in his room. For some reason there was no bed frame or sheets, just a mattress on the floor. One morning as I was lying on this mattress I had an idea that made me sit up like a capital L. What if we ran the software on the server, and let users control it by clicking on links? Then we'd never have to write anything to run on users' computers. We could generate the sites on the same server we'd serve them from. Users wouldn't need anything more than a browser. + +This kind of software, known as a web app, is common now, but at the time it wasn't clear that it was even possible. To find out, we decided to try making a version of our store builder that you could control through the browser. A couple days later, on August 12, we had one that worked. The UI was horrible, but it proved you could build a whole store through the browser, without any client software or typing anything into the command line on the server. + +Now we felt like we were really onto something. I had visions of a whole new generation of software working this way. You wouldn't need versions, or ports, or any of that crap. At Interleaf there had been a whole group called Release Engineering that seemed to be at least as big as the group that actually wrote the software. Now you could just update the software right on the server. + +We started a new company we called Viaweb, after the fact that our software worked via the web, and we got $10,000 in seed funding from Idelle's husband Julian. In return for that and doing the initial legal work and giving us business advice, we gave him 10% of the company. Ten years later this deal became the model for Y Combinator's. We knew founders needed something like this, because we'd needed it ourselves. + +At this stage I had a negative net worth, because the thousand dollars or so I had in the bank was more than counterbalanced by what I owed the government in taxes. (Had I diligently set aside the proper proportion of the money I'd made consulting for Interleaf? No, I had not.) So although Robert had his graduate student stipend, I needed that seed funding to live on. + +We originally hoped to launch in September, but we got more ambitious about the software as we worked on it. Eventually we managed to build a WYSIWYG site builder, in the sense that as you were creating pages, they looked exactly like the static ones that would be generated later, except that instead of leading to static pages, the links all referred to closures stored in a hash table on the server. + +It helped to have studied art, because the main goal of an online store builder is to make users look legit, and the key to looking legit is high production values. If you get page layouts and fonts and colors right, you can make a guy running a store out of his bedroom look more legit than a big company. + +(If you're curious why my site looks so old-fashioned, it's because it's still made with this software. It may look clunky today, but in 1996 it was the last word in slick.) + +In September, Robert rebelled. "We've been working on this for a month," he said, "and it's still not done." This is funny in retrospect, because he would still be working on it almost 3 years later. But I decided it might be prudent to recruit more programmers, and I asked Robert who else in grad school with him was really good. He recommended Trevor Blackwell, which surprised me at first, because at that point I knew Trevor mainly for his plan to reduce everything in his life to a stack of notecards, which he carried around with him. But Rtm was right, as usual. Trevor turned out to be a frighteningly effective hacker. + +It was a lot of fun working with Robert and Trevor. They're the two most independent-minded people I know, and in completely different ways. If you could see inside Rtm's brain it would look like a colonial New England church, and if you could see inside Trevor's it would look like the worst excesses of Austrian Rococo. + +We opened for business, with 6 stores, in January 1996. It was just as well we waited a few months, because although we worried we were late, we were actually almost fatally early. There was a lot of talk in the press then about ecommerce, but not many people actually wanted online stores. [8] + +There were three main parts to the software: the editor, which people used to build sites and which I wrote, the shopping cart, which Robert wrote, and the manager, which kept track of orders and statistics, and which Trevor wrote. In its time, the editor was one of the best general-purpose site builders. I kept the code tight and didn't have to integrate with any other software except Robert's and Trevor's, so it was quite fun to work on. If all I'd had to do was work on this software, the next 3 years would have been the easiest of my life. Unfortunately I had to do a lot more, all of it stuff I was worse at than programming, and the next 3 years were instead the most stressful. + +There were a lot of startups making ecommerce software in the second half of the 90s. We were determined to be the Microsoft Word, not the Interleaf. Which meant being easy to use and inexpensive. It was lucky for us that we were poor, because that caused us to make Viaweb even more inexpensive than we realized. We charged $100 a month for a small store and $300 a month for a big one. This low price was a big attraction, and a constant thorn in the sides of competitors, but it wasn't because of some clever insight that we set the price low. We had no idea what businesses paid for things. $300 a month seemed like a lot of money to us. + +We did a lot of things right by accident like that. For example, we did what's now called "doing things that don't scale," although at the time we would have described it as "being so lame that we're driven to the most desperate measures to get users." The most common of which was building stores for them. This seemed particularly humiliating, since the whole reason d'etre of our software was that people could use it to make their own stores. But anything to get users. + +We learned a lot more about retail than we wanted to know. For example, that if you could only have a small image of a man's shirt (and all images were small then by present standards), it was better to have a closeup of the collar than a picture of the whole shirt. The reason I remember learning this was that it meant I had to rescan about 30 images of men's shirts. My first set of scans were so beautiful too. + +Though this felt wrong, it was exactly the right thing to be doing. Building stores for users taught us about retail, and about how it felt to use our software. I was initially both mystified and repelled by "business" and thought we needed a "business person" to be in charge of it, but once we started to get users, I was converted, in much the same way I was converted to fatherhood once I had kids. Whatever users wanted, I was all theirs. Maybe one day we'd have so many users that I couldn't scan their images for them, but in the meantime there was nothing more important to do. + +Another thing I didn't get at the time is that growth rate is the ultimate test of a startup. Our growth rate was fine. We had about 70 stores at the end of 1996 and about 500 at the end of 1997. I mistakenly thought the thing that mattered was the absolute number of users. And that is the thing that matters in the sense that that's how much money you're making, and if you're not making enough, you might go out of business. But in the long term the growth rate takes care of the absolute number. If we'd been a startup I was advising at Y Combinator, I would have said: Stop being so stressed out, because you're doing fine. You're growing 7x a year. Just don't hire too many more people and you'll soon be profitable, and then you'll control your own destiny. + +Alas I hired lots more people, partly because our investors wanted me to, and partly because that's what startups did during the Internet Bubble. A company with just a handful of employees would have seemed amateurish. So we didn't reach breakeven until about when Yahoo bought us in the summer of 1998. Which in turn meant we were at the mercy of investors for the entire life of the company. And since both we and our investors were noobs at startups, the result was a mess even by startup standards. + +It was a huge relief when Yahoo bought us. In principle our Viaweb stock was valuable. It was a share in a business that was profitable and growing rapidly. But it didn't feel very valuable to me; I had no idea how to value a business, but I was all too keenly aware of the near-death experiences we seemed to have every few months. Nor had I changed my grad student lifestyle significantly since we started. So when Yahoo bought us it felt like going from rags to riches. Since we were going to California, I bought a car, a yellow 1998 VW GTI. I remember thinking that its leather seats alone were by far the most luxurious thing I owned. + +The next year, from the summer of 1998 to the summer of 1999, must have been the least productive of my life. I didn't realize it at the time, but I was worn out from the effort and stress of running Viaweb. For a while after I got to California I tried to continue my usual m.o. of programming till 3 in the morning, but fatigue combined with Yahoo's prematurely aged culture and grim cube farm in Santa Clara gradually dragged me down. After a few months it felt disconcertingly like working at Interleaf. + +Yahoo had given us a lot of options when they bought us. At the time I thought Yahoo was so overvalued that they'd never be worth anything, but to my astonishment the stock went up 5x in the next year. I hung on till the first chunk of options vested, then in the summer of 1999 I left. It had been so long since I'd painted anything that I'd half forgotten why I was doing this. My brain had been entirely full of software and men's shirts for 4 years. But I had done this to get rich so I could paint, I reminded myself, and now I was rich, so I should go paint. + +When I said I was leaving, my boss at Yahoo had a long conversation with me about my plans. I told him all about the kinds of pictures I wanted to paint. At the time I was touched that he took such an interest in me. Now I realize it was because he thought I was lying. My options at that point were worth about $2 million a month. If I was leaving that kind of money on the table, it could only be to go and start some new startup, and if I did, I might take people with me. This was the height of the Internet Bubble, and Yahoo was ground zero of it. My boss was at that moment a billionaire. Leaving then to start a new startup must have seemed to him an insanely, and yet also plausibly, ambitious plan. + +But I really was quitting to paint, and I started immediately. There was no time to lose. I'd already burned 4 years getting rich. Now when I talk to founders who are leaving after selling their companies, my advice is always the same: take a vacation. That's what I should have done, just gone off somewhere and done nothing for a month or two, but the idea never occurred to me. + +So I tried to paint, but I just didn't seem to have any energy or ambition. Part of the problem was that I didn't know many people in California. I'd compounded this problem by buying a house up in the Santa Cruz Mountains, with a beautiful view but miles from anywhere. I stuck it out for a few more months, then in desperation I went back to New York, where unless you understand about rent control you'll be surprised to hear I still had my apartment, sealed up like a tomb of my old life. Idelle was in New York at least, and there were other people trying to paint there, even though I didn't know any of them. + +When I got back to New York I resumed my old life, except now I was rich. It was as weird as it sounds. I resumed all my old patterns, except now there were doors where there hadn't been. Now when I was tired of walking, all I had to do was raise my hand, and (unless it was raining) a taxi would stop to pick me up. Now when I walked past charming little restaurants I could go in and order lunch. It was exciting for a while. Painting started to go better. I experimented with a new kind of still life where I'd paint one painting in the old way, then photograph it and print it, blown up, on canvas, and then use that as the underpainting for a second still life, painted from the same objects (which hopefully hadn't rotted yet). + +Meanwhile I looked for an apartment to buy. Now I could actually choose what neighborhood to live in. Where, I asked myself and various real estate agents, is the Cambridge of New York? Aided by occasional visits to actual Cambridge, I gradually realized there wasn't one. Huh. + +Around this time, in the spring of 2000, I had an idea. It was clear from our experience with Viaweb that web apps were the future. Why not build a web app for making web apps? Why not let people edit code on our server through the browser, and then host the resulting applications for them? [9] You could run all sorts of services on the servers that these applications could use just by making an API call: making and receiving phone calls, manipulating images, taking credit card payments, etc. + +I got so excited about this idea that I couldn't think about anything else. It seemed obvious that this was the future. I didn't particularly want to start another company, but it was clear that this idea would have to be embodied as one, so I decided to move to Cambridge and start it. I hoped to lure Robert into working on it with me, but there I ran into a hitch. Robert was now a postdoc at MIT, and though he'd made a lot of money the last time I'd lured him into working on one of my schemes, it had also been a huge time sink. So while he agreed that it sounded like a plausible idea, he firmly refused to work on it. + +Hmph. Well, I'd do it myself then. I recruited Dan Giffin, who had worked for Viaweb, and two undergrads who wanted summer jobs, and we got to work trying to build what it's now clear is about twenty companies and several open-source projects worth of software. The language for defining applications would of course be a dialect of Lisp. But I wasn't so naive as to assume I could spring an overt Lisp on a general audience; we'd hide the parentheses, like Dylan did. + +By then there was a name for the kind of company Viaweb was, an "application service provider," or ASP. This name didn't last long before it was replaced by "software as a service," but it was current for long enough that I named this new company after it: it was going to be called Aspra. + +I started working on the application builder, Dan worked on network infrastructure, and the two undergrads worked on the first two services (images and phone calls). But about halfway through the summer I realized I really didn't want to run a company — especially not a big one, which it was looking like this would have to be. I'd only started Viaweb because I needed the money. Now that I didn't need money anymore, why was I doing this? If this vision had to be realized as a company, then screw the vision. I'd build a subset that could be done as an open-source project. + +Much to my surprise, the time I spent working on this stuff was not wasted after all. After we started Y Combinator, I would often encounter startups working on parts of this new architecture, and it was very useful to have spent so much time thinking about it and even trying to write some of it. + +The subset I would build as an open-source project was the new Lisp, whose parentheses I now wouldn't even have to hide. A lot of Lisp hackers dream of building a new Lisp, partly because one of the distinctive features of the language is that it has dialects, and partly, I think, because we have in our minds a Platonic form of Lisp that all existing dialects fall short of. I certainly did. So at the end of the summer Dan and I switched to working on this new dialect of Lisp, which I called Arc, in a house I bought in Cambridge. + +The following spring, lightning struck. I was invited to give a talk at a Lisp conference, so I gave one about how we'd used Lisp at Viaweb. Afterward I put a postscript file of this talk online, on paulgraham.com, which I'd created years before using Viaweb but had never used for anything. In one day it got 30,000 page views. What on earth had happened? The referring urls showed that someone had posted it on Slashdot. [10] + +Wow, I thought, there's an audience. If I write something and put it on the web, anyone can read it. That may seem obvious now, but it was surprising then. In the print era there was a narrow channel to readers, guarded by fierce monsters known as editors. The only way to get an audience for anything you wrote was to get it published as a book, or in a newspaper or magazine. Now anyone could publish anything. + +This had been possible in principle since 1993, but not many people had realized it yet. I had been intimately involved with building the infrastructure of the web for most of that time, and a writer as well, and it had taken me 8 years to realize it. Even then it took me several years to understand the implications. It meant there would be a whole new generation of essays. [11] + +In the print era, the channel for publishing essays had been vanishingly small. Except for a few officially anointed thinkers who went to the right parties in New York, the only people allowed to publish essays were specialists writing about their specialties. There were so many essays that had never been written, because there had been no way to publish them. Now they could be, and I was going to write them. [12] + +I've worked on several different things, but to the extent there was a turning point where I figured out what to work on, it was when I started publishing essays online. From then on I knew that whatever else I did, I'd always write essays too. + +I knew that online essays would be a marginal medium at first. Socially they'd seem more like rants posted by nutjobs on their GeoCities sites than the genteel and beautifully typeset compositions published in The New Yorker. But by this point I knew enough to find that encouraging instead of discouraging. + +One of the most conspicuous patterns I've noticed in my life is how well it has worked, for me at least, to work on things that weren't prestigious. Still life has always been the least prestigious form of painting. Viaweb and Y Combinator both seemed lame when we started them. I still get the glassy eye from strangers when they ask what I'm writing, and I explain that it's an essay I'm going to publish on my web site. Even Lisp, though prestigious intellectually in something like the way Latin is, also seems about as hip. + +It's not that unprestigious types of work are good per se. But when you find yourself drawn to some kind of work despite its current lack of prestige, it's a sign both that there's something real to be discovered there, and that you have the right kind of motives. Impure motives are a big danger for the ambitious. If anything is going to lead you astray, it will be the desire to impress people. So while working on things that aren't prestigious doesn't guarantee you're on the right track, it at least guarantees you're not on the most common type of wrong one. + +Over the next several years I wrote lots of essays about all kinds of different topics. O'Reilly reprinted a collection of them as a book, called Hackers & Painters after one of the essays in it. I also worked on spam filters, and did some more painting. I used to have dinners for a group of friends every thursday night, which taught me how to cook for groups. And I bought another building in Cambridge, a former candy factory (and later, twas said, porn studio), to use as an office. + +One night in October 2003 there was a big party at my house. It was a clever idea of my friend Maria Daniels, who was one of the thursday diners. Three separate hosts would all invite their friends to one party. So for every guest, two thirds of the other guests would be people they didn't know but would probably like. One of the guests was someone I didn't know but would turn out to like a lot: a woman called Jessica Livingston. A couple days later I asked her out. + +Jessica was in charge of marketing at a Boston investment bank. This bank thought it understood startups, but over the next year, as she met friends of mine from the startup world, she was surprised how different reality was. And how colorful their stories were. So she decided to compile a book of interviews with startup founders. + +When the bank had financial problems and she had to fire half her staff, she started looking for a new job. In early 2005 she interviewed for a marketing job at a Boston VC firm. It took them weeks to make up their minds, and during this time I started telling her about all the things that needed to be fixed about venture capital. They should make a larger number of smaller investments instead of a handful of giant ones, they should be funding younger, more technical founders instead of MBAs, they should let the founders remain as CEO, and so on. + +One of my tricks for writing essays had always been to give talks. The prospect of having to stand up in front of a group of people and tell them something that won't waste their time is a great spur to the imagination. When the Harvard Computer Society, the undergrad computer club, asked me to give a talk, I decided I would tell them how to start a startup. Maybe they'd be able to avoid the worst of the mistakes we'd made. + +So I gave this talk, in the course of which I told them that the best sources of seed funding were successful startup founders, because then they'd be sources of advice too. Whereupon it seemed they were all looking expectantly at me. Horrified at the prospect of having my inbox flooded by business plans (if I'd only known), I blurted out "But not me!" and went on with the talk. But afterward it occurred to me that I should really stop procrastinating about angel investing. I'd been meaning to since Yahoo bought us, and now it was 7 years later and I still hadn't done one angel investment. + +Meanwhile I had been scheming with Robert and Trevor about projects we could work on together. I missed working with them, and it seemed like there had to be something we could collaborate on. + +As Jessica and I were walking home from dinner on March 11, at the corner of Garden and Walker streets, these three threads converged. Screw the VCs who were taking so long to make up their minds. We'd start our own investment firm and actually implement the ideas we'd been talking about. I'd fund it, and Jessica could quit her job and work for it, and we'd get Robert and Trevor as partners too. [13] + +Once again, ignorance worked in our favor. We had no idea how to be angel investors, and in Boston in 2005 there were no Ron Conways to learn from. So we just made what seemed like the obvious choices, and some of the things we did turned out to be novel. + +There are multiple components to Y Combinator, and we didn't figure them all out at once. The part we got first was to be an angel firm. In those days, those two words didn't go together. There were VC firms, which were organized companies with people whose job it was to make investments, but they only did big, million dollar investments. And there were angels, who did smaller investments, but these were individuals who were usually focused on other things and made investments on the side. And neither of them helped founders enough in the beginning. We knew how helpless founders were in some respects, because we remembered how helpless we'd been. For example, one thing Julian had done for us that seemed to us like magic was to get us set up as a company. We were fine writing fairly difficult software, but actually getting incorporated, with bylaws and stock and all that stuff, how on earth did you do that? Our plan was not only to make seed investments, but to do for startups everything Julian had done for us. + +YC was not organized as a fund. It was cheap enough to run that we funded it with our own money. That went right by 99% of readers, but professional investors are thinking "Wow, that means they got all the returns." But once again, this was not due to any particular insight on our part. We didn't know how VC firms were organized. It never occurred to us to try to raise a fund, and if it had, we wouldn't have known where to start. [14] + +The most distinctive thing about YC is the batch model: to fund a bunch of startups all at once, twice a year, and then to spend three months focusing intensively on trying to help them. That part we discovered by accident, not merely implicitly but explicitly due to our ignorance about investing. We needed to get experience as investors. What better way, we thought, than to fund a whole bunch of startups at once? We knew undergrads got temporary jobs at tech companies during the summer. Why not organize a summer program where they'd start startups instead? We wouldn't feel guilty for being in a sense fake investors, because they would in a similar sense be fake founders. So while we probably wouldn't make much money out of it, we'd at least get to practice being investors on them, and they for their part would probably have a more interesting summer than they would working at Microsoft. + +We'd use the building I owned in Cambridge as our headquarters. We'd all have dinner there once a week — on tuesdays, since I was already cooking for the thursday diners on thursdays — and after dinner we'd bring in experts on startups to give talks. + +We knew undergrads were deciding then about summer jobs, so in a matter of days we cooked up something we called the Summer Founders Program, and I posted an announcement on my site, inviting undergrads to apply. I had never imagined that writing essays would be a way to get "deal flow," as investors call it, but it turned out to be the perfect source. [15] We got 225 applications for the Summer Founders Program, and we were surprised to find that a lot of them were from people who'd already graduated, or were about to that spring. Already this SFP thing was starting to feel more serious than we'd intended. + +We invited about 20 of the 225 groups to interview in person, and from those we picked 8 to fund. They were an impressive group. That first batch included reddit, Justin Kan and Emmett Shear, who went on to found Twitch, Aaron Swartz, who had already helped write the RSS spec and would a few years later become a martyr for open access, and Sam Altman, who would later become the second president of YC. I don't think it was entirely luck that the first batch was so good. You had to be pretty bold to sign up for a weird thing like the Summer Founders Program instead of a summer job at a legit place like Microsoft or Goldman Sachs. + +The deal for startups was based on a combination of the deal we did with Julian ($10k for 10%) and what Robert said MIT grad students got for the summer ($6k). We invested $6k per founder, which in the typical two-founder case was $12k, in return for 6%. That had to be fair, because it was twice as good as the deal we ourselves had taken. Plus that first summer, which was really hot, Jessica brought the founders free air conditioners. [16] + +Fairly quickly I realized that we had stumbled upon the way to scale startup funding. Funding startups in batches was more convenient for us, because it meant we could do things for a lot of startups at once, but being part of a batch was better for the startups too. It solved one of the biggest problems faced by founders: the isolation. Now you not only had colleagues, but colleagues who understood the problems you were facing and could tell you how they were solving them. + +As YC grew, we started to notice other advantages of scale. The alumni became a tight community, dedicated to helping one another, and especially the current batch, whose shoes they remembered being in. We also noticed that the startups were becoming one another's customers. We used to refer jokingly to the "YC GDP," but as YC grows this becomes less and less of a joke. Now lots of startups get their initial set of customers almost entirely from among their batchmates. + +I had not originally intended YC to be a full-time job. I was going to do three things: hack, write essays, and work on YC. As YC grew, and I grew more excited about it, it started to take up a lot more than a third of my attention. But for the first few years I was still able to work on other things. + +In the summer of 2006, Robert and I started working on a new version of Arc. This one was reasonably fast, because it was compiled into Scheme. To test this new Arc, I wrote Hacker News in it. It was originally meant to be a news aggregator for startup founders and was called Startup News, but after a few months I got tired of reading about nothing but startups. Plus it wasn't startup founders we wanted to reach. It was future startup founders. So I changed the name to Hacker News and the topic to whatever engaged one's intellectual curiosity. + +HN was no doubt good for YC, but it was also by far the biggest source of stress for me. If all I'd had to do was select and help founders, life would have been so easy. And that implies that HN was a mistake. Surely the biggest source of stress in one's work should at least be something close to the core of the work. Whereas I was like someone who was in pain while running a marathon not from the exertion of running, but because I had a blister from an ill-fitting shoe. When I was dealing with some urgent problem during YC, there was about a 60% chance it had to do with HN, and a 40% chance it had do with everything else combined. [17] + +As well as HN, I wrote all of YC's internal software in Arc. But while I continued to work a good deal in Arc, I gradually stopped working on Arc, partly because I didn't have time to, and partly because it was a lot less attractive to mess around with the language now that we had all this infrastructure depending on it. So now my three projects were reduced to two: writing essays and working on YC. + +YC was different from other kinds of work I've done. Instead of deciding for myself what to work on, the problems came to me. Every 6 months there was a new batch of startups, and their problems, whatever they were, became our problems. It was very engaging work, because their problems were quite varied, and the good founders were very effective. If you were trying to learn the most you could about startups in the shortest possible time, you couldn't have picked a better way to do it. + +There were parts of the job I didn't like. Disputes between cofounders, figuring out when people were lying to us, fighting with people who maltreated the startups, and so on. But I worked hard even at the parts I didn't like. I was haunted by something Kevin Hale once said about companies: "No one works harder than the boss." He meant it both descriptively and prescriptively, and it was the second part that scared me. I wanted YC to be good, so if how hard I worked set the upper bound on how hard everyone else worked, I'd better work very hard. + +One day in 2010, when he was visiting California for interviews, Robert Morris did something astonishing: he offered me unsolicited advice. I can only remember him doing that once before. One day at Viaweb, when I was bent over double from a kidney stone, he suggested that it would be a good idea for him to take me to the hospital. That was what it took for Rtm to offer unsolicited advice. So I remember his exact words very clearly. "You know," he said, "you should make sure Y Combinator isn't the last cool thing you do." + +At the time I didn't understand what he meant, but gradually it dawned on me that he was saying I should quit. This seemed strange advice, because YC was doing great. But if there was one thing rarer than Rtm offering advice, it was Rtm being wrong. So this set me thinking. It was true that on my current trajectory, YC would be the last thing I did, because it was only taking up more of my attention. It had already eaten Arc, and was in the process of eating essays too. Either YC was my life's work or I'd have to leave eventually. And it wasn't, so I would. + +In the summer of 2012 my mother had a stroke, and the cause turned out to be a blood clot caused by colon cancer. The stroke destroyed her balance, and she was put in a nursing home, but she really wanted to get out of it and back to her house, and my sister and I were determined to help her do it. I used to fly up to Oregon to visit her regularly, and I had a lot of time to think on those flights. On one of them I realized I was ready to hand YC over to someone else. + +I asked Jessica if she wanted to be president, but she didn't, so we decided we'd try to recruit Sam Altman. We talked to Robert and Trevor and we agreed to make it a complete changing of the guard. Up till that point YC had been controlled by the original LLC we four had started. But we wanted YC to last for a long time, and to do that it couldn't be controlled by the founders. So if Sam said yes, we'd let him reorganize YC. Robert and I would retire, and Jessica and Trevor would become ordinary partners. + +When we asked Sam if he wanted to be president of YC, initially he said no. He wanted to start a startup to make nuclear reactors. But I kept at it, and in October 2013 he finally agreed. We decided he'd take over starting with the winter 2014 batch. For the rest of 2013 I left running YC more and more to Sam, partly so he could learn the job, and partly because I was focused on my mother, whose cancer had returned. + +She died on January 15, 2014. We knew this was coming, but it was still hard when it did. + +I kept working on YC till March, to help get that batch of startups through Demo Day, then I checked out pretty completely. (I still talk to alumni and to new startups working on things I'm interested in, but that only takes a few hours a week.) + +What should I do next? Rtm's advice hadn't included anything about that. I wanted to do something completely different, so I decided I'd paint. I wanted to see how good I could get if I really focused on it. So the day after I stopped working on YC, I started painting. I was rusty and it took a while to get back into shape, but it was at least completely engaging. [18] + +I spent most of the rest of 2014 painting. I'd never been able to work so uninterruptedly before, and I got to be better than I had been. Not good enough, but better. Then in November, right in the middle of a painting, I ran out of steam. Up till that point I'd always been curious to see how the painting I was working on would turn out, but suddenly finishing this one seemed like a chore. So I stopped working on it and cleaned my brushes and haven't painted since. So far anyway. + +I realize that sounds rather wimpy. But attention is a zero sum game. If you can choose what to work on, and you choose a project that's not the best one (or at least a good one) for you, then it's getting in the way of another project that is. And at 50 there was some opportunity cost to screwing around. + +I started writing essays again, and wrote a bunch of new ones over the next few months. I even wrote a couple that weren't about startups. Then in March 2015 I started working on Lisp again. + +The distinctive thing about Lisp is that its core is a language defined by writing an interpreter in itself. It wasn't originally intended as a programming language in the ordinary sense. It was meant to be a formal model of computation, an alternative to the Turing machine. If you want to write an interpreter for a language in itself, what's the minimum set of predefined operators you need? The Lisp that John McCarthy invented, or more accurately discovered, is an answer to that question. [19] + +McCarthy didn't realize this Lisp could even be used to program computers till his grad student Steve Russell suggested it. Russell translated McCarthy's interpreter into IBM 704 machine language, and from that point Lisp started also to be a programming language in the ordinary sense. But its origins as a model of computation gave it a power and elegance that other languages couldn't match. It was this that attracted me in college, though I didn't understand why at the time. + +McCarthy's 1960 Lisp did nothing more than interpret Lisp expressions. It was missing a lot of things you'd want in a programming language. So these had to be added, and when they were, they weren't defined using McCarthy's original axiomatic approach. That wouldn't have been feasible at the time. McCarthy tested his interpreter by hand-simulating the execution of programs. But it was already getting close to the limit of interpreters you could test that way — indeed, there was a bug in it that McCarthy had overlooked. To test a more complicated interpreter, you'd have had to run it, and computers then weren't powerful enough. + +Now they are, though. Now you could continue using McCarthy's axiomatic approach till you'd defined a complete programming language. And as long as every change you made to McCarthy's Lisp was a discoveredness-preserving transformation, you could, in principle, end up with a complete language that had this quality. Harder to do than to talk about, of course, but if it was possible in principle, why not try? So I decided to take a shot at it. It took 4 years, from March 26, 2015 to October 12, 2019. It was fortunate that I had a precisely defined goal, or it would have been hard to keep at it for so long. + +I wrote this new Lisp, called Bel, in itself in Arc. That may sound like a contradiction, but it's an indication of the sort of trickery I had to engage in to make this work. By means of an egregious collection of hacks I managed to make something close enough to an interpreter written in itself that could actually run. Not fast, but fast enough to test. + +I had to ban myself from writing essays during most of this time, or I'd never have finished. In late 2015 I spent 3 months writing essays, and when I went back to working on Bel I could barely understand the code. Not so much because it was badly written as because the problem is so convoluted. When you're working on an interpreter written in itself, it's hard to keep track of what's happening at what level, and errors can be practically encrypted by the time you get them. + +So I said no more essays till Bel was done. But I told few people about Bel while I was working on it. So for years it must have seemed that I was doing nothing, when in fact I was working harder than I'd ever worked on anything. Occasionally after wrestling for hours with some gruesome bug I'd check Twitter or HN and see someone asking "Does Paul Graham still code?" + +Working on Bel was hard but satisfying. I worked on it so intensively that at any given time I had a decent chunk of the code in my head and could write more there. I remember taking the boys to the coast on a sunny day in 2015 and figuring out how to deal with some problem involving continuations while I watched them play in the tide pools. It felt like I was doing life right. I remember that because I was slightly dismayed at how novel it felt. The good news is that I had more moments like this over the next few years. + +In the summer of 2016 we moved to England. We wanted our kids to see what it was like living in another country, and since I was a British citizen by birth, that seemed the obvious choice. We only meant to stay for a year, but we liked it so much that we still live there. So most of Bel was written in England. + +In the fall of 2019, Bel was finally finished. Like McCarthy's original Lisp, it's a spec rather than an implementation, although like McCarthy's Lisp it's a spec expressed as code. + +Now that I could write essays again, I wrote a bunch about topics I'd had stacked up. I kept writing essays through 2020, but I also started to think about other things I could work on. How should I choose what to do? Well, how had I chosen what to work on in the past? I wrote an essay for myself to answer that question, and I was surprised how long and messy the answer turned out to be. If this surprised me, who'd lived it, then I thought perhaps it would be interesting to other people, and encouraging to those with similarly messy lives. So I wrote a more detailed version for others to read, and this is the last sentence of it. + + + + + + + + + +Notes + +[1] My experience skipped a step in the evolution of computers: time-sharing machines with interactive OSes. I went straight from batch processing to microcomputers, which made microcomputers seem all the more exciting. + +[2] Italian words for abstract concepts can nearly always be predicted from their English cognates (except for occasional traps like polluzione). It's the everyday words that differ. So if you string together a lot of abstract concepts with a few simple verbs, you can make a little Italian go a long way. + +[3] I lived at Piazza San Felice 4, so my walk to the Accademia went straight down the spine of old Florence: past the Pitti, across the bridge, past Orsanmichele, between the Duomo and the Baptistery, and then up Via Ricasoli to Piazza San Marco. I saw Florence at street level in every possible condition, from empty dark winter evenings to sweltering summer days when the streets were packed with tourists. + +[4] You can of course paint people like still lives if you want to, and they're willing. That sort of portrait is arguably the apex of still life painting, though the long sitting does tend to produce pained expressions in the sitters. + +[5] Interleaf was one of many companies that had smart people and built impressive technology, and yet got crushed by Moore's Law. In the 1990s the exponential growth in the power of commodity (i.e. Intel) processors rolled up high-end, special-purpose hardware and software companies like a bulldozer. + +[6] The signature style seekers at RISD weren't specifically mercenary. In the art world, money and coolness are tightly coupled. Anything expensive comes to be seen as cool, and anything seen as cool will soon become equally expensive. + +[7] Technically the apartment wasn't rent-controlled but rent-stabilized, but this is a refinement only New Yorkers would know or care about. The point is that it was really cheap, less than half market price. + +[8] Most software you can launch as soon as it's done. But when the software is an online store builder and you're hosting the stores, if you don't have any users yet, that fact will be painfully obvious. So before we could launch publicly we had to launch privately, in the sense of recruiting an initial set of users and making sure they had decent-looking stores. + +[9] We'd had a code editor in Viaweb for users to define their own page styles. They didn't know it, but they were editing Lisp expressions underneath. But this wasn't an app editor, because the code ran when the merchants' sites were generated, not when shoppers visited them. + +[10] This was the first instance of what is now a familiar experience, and so was what happened next, when I read the comments and found they were full of angry people. How could I claim that Lisp was better than other languages? Weren't they all Turing complete? People who see the responses to essays I write sometimes tell me how sorry they feel for me, but I'm not exaggerating when I reply that it has always been like this, since the very beginning. It comes with the territory. An essay must tell readers things they don't already know, and some people dislike being told such things. + +[11] People put plenty of stuff on the internet in the 90s of course, but putting something online is not the same as publishing it online. Publishing online means you treat the online version as the (or at least a) primary version. + +[12] There is a general lesson here that our experience with Y Combinator also teaches: Customs continue to constrain you long after the restrictions that caused them have disappeared. Customary VC practice had once, like the customs about publishing essays, been based on real constraints. Startups had once been much more expensive to start, and proportionally rare. Now they could be cheap and common, but the VCs' customs still reflected the old world, just as customs about writing essays still reflected the constraints of the print era. + +Which in turn implies that people who are independent-minded (i.e. less influenced by custom) will have an advantage in fields affected by rapid change (where customs are more likely to be obsolete). + +Here's an interesting point, though: you can't always predict which fields will be affected by rapid change. Obviously software and venture capital will be, but who would have predicted that essay writing would be? + +[13] Y Combinator was not the original name. At first we were called Cambridge Seed. But we didn't want a regional name, in case someone copied us in Silicon Valley, so we renamed ourselves after one of the coolest tricks in the lambda calculus, the Y combinator. + +I picked orange as our color partly because it's the warmest, and partly because no VC used it. In 2005 all the VCs used staid colors like maroon, navy blue, and forest green, because they were trying to appeal to LPs, not founders. The YC logo itself is an inside joke: the Viaweb logo had been a white V on a red circle, so I made the YC logo a white Y on an orange square. + +[14] YC did become a fund for a couple years starting in 2009, because it was getting so big I could no longer afford to fund it personally. But after Heroku got bought we had enough money to go back to being self-funded. + +[15] I've never liked the term "deal flow," because it implies that the number of new startups at any given time is fixed. This is not only false, but it's the purpose of YC to falsify it, by causing startups to be founded that would not otherwise have existed. + +[16] She reports that they were all different shapes and sizes, because there was a run on air conditioners and she had to get whatever she could, but that they were all heavier than she could carry now. + +[17] Another problem with HN was a bizarre edge case that occurs when you both write essays and run a forum. When you run a forum, you're assumed to see if not every conversation, at least every conversation involving you. And when you write essays, people post highly imaginative misinterpretations of them on forums. Individually these two phenomena are tedious but bearable, but the combination is disastrous. You actually have to respond to the misinterpretations, because the assumption that you're present in the conversation means that not responding to any sufficiently upvoted misinterpretation reads as a tacit admission that it's correct. But that in turn encourages more; anyone who wants to pick a fight with you senses that now is their chance. + +[18] The worst thing about leaving YC was not working with Jessica anymore. We'd been working on YC almost the whole time we'd known each other, and we'd neither tried nor wanted to separate it from our personal lives, so leaving was like pulling up a deeply rooted tree. + +[19] One way to get more precise about the concept of invented vs discovered is to talk about space aliens. Any sufficiently advanced alien civilization would certainly know about the Pythagorean theorem, for example. I believe, though with less certainty, that they would also know about the Lisp in McCarthy's 1960 paper. + +But if so there's no reason to suppose that this is the limit of the language that might be known to them. Presumably aliens need numbers and errors and I/O too. So it seems likely there exists at least one path out of McCarthy's Lisp along which discoveredness is preserved. + + + +Thanks to Trevor Blackwell, John Collison, Patrick Collison, Daniel Gackle, Ralph Hazell, Jessica Livingston, Robert Morris, and Harj Taggar for reading drafts of this. diff --git a/poetry.lock b/poetry.lock index bd91830a98f9c..6cb99a0e57466 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. [[package]] name = "aiohttp" @@ -742,6 +742,17 @@ files = [ {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, ] +[[package]] +name = "distro" +version = "1.9.0" +description = "Distro - an OS platform information API" +optional = false +python-versions = ">=3.6" +files = [ + {file = "distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2"}, + {file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"}, +] + [[package]] name = "dnspython" version = "2.4.2" @@ -1036,6 +1047,62 @@ files = [ docs = ["Sphinx"] test = ["objgraph", "psutil"] +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "httpcore" +version = "1.0.2" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpcore-1.0.2-py3-none-any.whl", hash = "sha256:096cc05bca73b8e459a1fc3dcf585148f63e534eae4339559c9b8a8d6399acc7"}, + {file = "httpcore-1.0.2.tar.gz", hash = "sha256:9fc092e4799b26174648e54b74ed5f683132a464e95643b226e00c2ed2fa6535"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.13,<0.15" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<0.23.0)"] + +[[package]] +name = "httpx" +version = "0.26.0" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpx-0.26.0-py3-none-any.whl", hash = "sha256:8915f5a3627c4d47b73e8202457cb28f1266982d1159bd5779d86a80c0eab1cd"}, + {file = "httpx-0.26.0.tar.gz", hash = "sha256:451b55c30d5185ea6b23c2c793abf9bb237d2a7dfb901ced6ff69ad37ec1dfaf"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" +sniffio = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] + [[package]] name = "idna" version = "3.4" @@ -1627,7 +1694,7 @@ files = [ [[package]] name = "langchain" -version = "0.0.352" +version = "0.1.0" description = "Building applications with LLMs through composability" optional = false python-versions = ">=3.8.1,<4.0" @@ -1639,9 +1706,9 @@ aiohttp = "^3.8.3" async-timeout = {version = "^4.0.0", markers = "python_version < \"3.11\""} dataclasses-json = ">= 0.5.7, < 0.7" jsonpatch = "^1.33" -langchain-community = ">=0.0.2,<0.1" -langchain-core = "^0.1" -langsmith = "~0.0.70" +langchain-community = ">=0.0.9,<0.1" +langchain-core = ">=0.1.7,<0.2" +langsmith = "~0.0.77" numpy = "^1" pydantic = ">=1,<3" PyYAML = ">=5.3" @@ -1657,7 +1724,7 @@ cli = ["typer (>=0.9.0,<0.10.0)"] cohere = ["cohere (>=4,<5)"] docarray = ["docarray[hnswlib] (>=0.32.0,<0.33.0)"] embeddings = ["sentence-transformers (>=2,<3)"] -extended-testing = ["aiosqlite (>=0.19.0,<0.20.0)", "aleph-alpha-client (>=2.15.0,<3.0.0)", "anthropic (>=0.3.11,<0.4.0)", "arxiv (>=1.4,<2.0)", "assemblyai (>=0.17.0,<0.18.0)", "atlassian-python-api (>=3.36.0,<4.0.0)", "beautifulsoup4 (>=4,<5)", "bibtexparser (>=1.4.0,<2.0.0)", "cassio (>=0.1.0,<0.2.0)", "chardet (>=5.1.0,<6.0.0)", "cohere (>=4,<5)", "couchbase (>=4.1.9,<5.0.0)", "dashvector (>=1.0.1,<2.0.0)", "databricks-vectorsearch (>=0.21,<0.22)", "datasets (>=2.15.0,<3.0.0)", "dgml-utils (>=0.3.0,<0.4.0)", "esprima (>=4.0.1,<5.0.0)", "faiss-cpu (>=1,<2)", "feedparser (>=6.0.10,<7.0.0)", "fireworks-ai (>=0.9.0,<0.10.0)", "geopandas (>=0.13.1,<0.14.0)", "gitpython (>=3.1.32,<4.0.0)", "google-cloud-documentai (>=2.20.1,<3.0.0)", "gql (>=3.4.1,<4.0.0)", "hologres-vector (>=0.0.6,<0.0.7)", "html2text (>=2020.1.16,<2021.0.0)", "javelin-sdk (>=0.1.8,<0.2.0)", "jinja2 (>=3,<4)", "jq (>=1.4.1,<2.0.0)", "jsonschema (>1)", "lxml (>=4.9.2,<5.0.0)", "markdownify (>=0.11.6,<0.12.0)", "motor (>=3.3.1,<4.0.0)", "msal (>=1.25.0,<2.0.0)", "mwparserfromhell (>=0.6.4,<0.7.0)", "mwxml (>=0.3.3,<0.4.0)", "newspaper3k (>=0.2.8,<0.3.0)", "numexpr (>=2.8.6,<3.0.0)", "openai (<2)", "openai (<2)", "openapi-pydantic (>=0.3.2,<0.4.0)", "pandas (>=2.0.1,<3.0.0)", "pdfminer-six (>=20221105,<20221106)", "pgvector (>=0.1.6,<0.2.0)", "praw (>=7.7.1,<8.0.0)", "psychicapi (>=0.8.0,<0.9.0)", "py-trello (>=0.19.0,<0.20.0)", "pymupdf (>=1.22.3,<2.0.0)", "pypdf (>=3.4.0,<4.0.0)", "pypdfium2 (>=4.10.0,<5.0.0)", "pyspark (>=3.4.0,<4.0.0)", "rank-bm25 (>=0.2.2,<0.3.0)", "rapidfuzz (>=3.1.1,<4.0.0)", "rapidocr-onnxruntime (>=1.3.2,<2.0.0)", "requests-toolbelt (>=1.0.0,<2.0.0)", "rspace_client (>=2.5.0,<3.0.0)", "scikit-learn (>=1.2.2,<2.0.0)", "sqlite-vss (>=0.1.2,<0.2.0)", "streamlit (>=1.18.0,<2.0.0)", "sympy (>=1.12,<2.0)", "telethon (>=1.28.5,<2.0.0)", "timescale-vector (>=0.0.1,<0.0.2)", "tqdm (>=4.48.0)", "upstash-redis (>=0.15.0,<0.16.0)", "xata (>=1.0.0a7,<2.0.0)", "xmltodict (>=0.13.0,<0.14.0)"] +extended-testing = ["aiosqlite (>=0.19.0,<0.20.0)", "aleph-alpha-client (>=2.15.0,<3.0.0)", "anthropic (>=0.3.11,<0.4.0)", "arxiv (>=1.4,<2.0)", "assemblyai (>=0.17.0,<0.18.0)", "atlassian-python-api (>=3.36.0,<4.0.0)", "beautifulsoup4 (>=4,<5)", "bibtexparser (>=1.4.0,<2.0.0)", "cassio (>=0.1.0,<0.2.0)", "chardet (>=5.1.0,<6.0.0)", "cohere (>=4,<5)", "couchbase (>=4.1.9,<5.0.0)", "dashvector (>=1.0.1,<2.0.0)", "databricks-vectorsearch (>=0.21,<0.22)", "datasets (>=2.15.0,<3.0.0)", "dgml-utils (>=0.3.0,<0.4.0)", "esprima (>=4.0.1,<5.0.0)", "faiss-cpu (>=1,<2)", "feedparser (>=6.0.10,<7.0.0)", "fireworks-ai (>=0.9.0,<0.10.0)", "geopandas (>=0.13.1,<0.14.0)", "gitpython (>=3.1.32,<4.0.0)", "google-cloud-documentai (>=2.20.1,<3.0.0)", "gql (>=3.4.1,<4.0.0)", "hologres-vector (>=0.0.6,<0.0.7)", "html2text (>=2020.1.16,<2021.0.0)", "javelin-sdk (>=0.1.8,<0.2.0)", "jinja2 (>=3,<4)", "jq (>=1.4.1,<2.0.0)", "jsonschema (>1)", "langchain-openai (>=0.0.2,<0.1)", "lxml (>=4.9.2,<5.0.0)", "markdownify (>=0.11.6,<0.12.0)", "motor (>=3.3.1,<4.0.0)", "msal (>=1.25.0,<2.0.0)", "mwparserfromhell (>=0.6.4,<0.7.0)", "mwxml (>=0.3.3,<0.4.0)", "newspaper3k (>=0.2.8,<0.3.0)", "numexpr (>=2.8.6,<3.0.0)", "openai (<2)", "openai (<2)", "openapi-pydantic (>=0.3.2,<0.4.0)", "pandas (>=2.0.1,<3.0.0)", "pdfminer-six (>=20221105,<20221106)", "pgvector (>=0.1.6,<0.2.0)", "praw (>=7.7.1,<8.0.0)", "psychicapi (>=0.8.0,<0.9.0)", "py-trello (>=0.19.0,<0.20.0)", "pymupdf (>=1.22.3,<2.0.0)", "pypdf (>=3.4.0,<4.0.0)", "pypdfium2 (>=4.10.0,<5.0.0)", "pyspark (>=3.4.0,<4.0.0)", "rank-bm25 (>=0.2.2,<0.3.0)", "rapidfuzz (>=3.1.1,<4.0.0)", "rapidocr-onnxruntime (>=1.3.2,<2.0.0)", "requests-toolbelt (>=1.0.0,<2.0.0)", "rspace_client (>=2.5.0,<3.0.0)", "scikit-learn (>=1.2.2,<2.0.0)", "sqlite-vss (>=0.1.2,<0.2.0)", "streamlit (>=1.18.0,<2.0.0)", "sympy (>=1.12,<2.0)", "telethon (>=1.28.5,<2.0.0)", "timescale-vector (>=0.0.1,<0.0.2)", "tqdm (>=4.48.0)", "upstash-redis (>=0.15.0,<0.16.0)", "xata (>=1.0.0a7,<2.0.0)", "xmltodict (>=0.13.0,<0.14.0)"] javascript = ["esprima (>=4.0.1,<5.0.0)"] llms = ["clarifai (>=9.1.0)", "cohere (>=4,<5)", "huggingface_hub (>=0,<1)", "manifest-ml (>=0.0.1,<0.0.2)", "nlpcloud (>=1,<2)", "openai (<2)", "openlm (>=0.0.5,<0.0.6)", "torch (>=1,<3)", "transformers (>=4,<5)"] openai = ["openai (<2)", "tiktoken (>=0.3.2,<0.6.0)"] @@ -1670,7 +1737,7 @@ url = "libs/langchain" [[package]] name = "langchain-community" -version = "0.0.6" +version = "0.0.11" description = "Community contributed LangChain integrations." optional = false python-versions = ">=3.8.1,<4.0" @@ -1680,7 +1747,7 @@ develop = true [package.dependencies] aiohttp = "^3.8.3" dataclasses-json = ">= 0.5.7, < 0.7" -langchain-core = "^0.1" +langchain-core = ">=0.1.8,<0.2" langsmith = "~0.0.63" numpy = "^1" PyYAML = ">=5.3" @@ -1690,7 +1757,7 @@ tenacity = "^8.1.0" [package.extras] cli = ["typer (>=0.9.0,<0.10.0)"] -extended-testing = ["aiosqlite (>=0.19.0,<0.20.0)", "aleph-alpha-client (>=2.15.0,<3.0.0)", "anthropic (>=0.3.11,<0.4.0)", "arxiv (>=1.4,<2.0)", "assemblyai (>=0.17.0,<0.18.0)", "atlassian-python-api (>=3.36.0,<4.0.0)", "beautifulsoup4 (>=4,<5)", "bibtexparser (>=1.4.0,<2.0.0)", "cassio (>=0.1.0,<0.2.0)", "chardet (>=5.1.0,<6.0.0)", "cohere (>=4,<5)", "dashvector (>=1.0.1,<2.0.0)", "databricks-vectorsearch (>=0.21,<0.22)", "datasets (>=2.15.0,<3.0.0)", "dgml-utils (>=0.3.0,<0.4.0)", "esprima (>=4.0.1,<5.0.0)", "faiss-cpu (>=1,<2)", "feedparser (>=6.0.10,<7.0.0)", "fireworks-ai (>=0.9.0,<0.10.0)", "geopandas (>=0.13.1,<0.14.0)", "gitpython (>=3.1.32,<4.0.0)", "google-cloud-documentai (>=2.20.1,<3.0.0)", "gql (>=3.4.1,<4.0.0)", "gradientai (>=1.4.0,<2.0.0)", "hologres-vector (>=0.0.6,<0.0.7)", "html2text (>=2020.1.16,<2021.0.0)", "javelin-sdk (>=0.1.8,<0.2.0)", "jinja2 (>=3,<4)", "jq (>=1.4.1,<2.0.0)", "jsonschema (>1)", "lxml (>=4.9.2,<5.0.0)", "markdownify (>=0.11.6,<0.12.0)", "motor (>=3.3.1,<4.0.0)", "msal (>=1.25.0,<2.0.0)", "mwparserfromhell (>=0.6.4,<0.7.0)", "mwxml (>=0.3.3,<0.4.0)", "newspaper3k (>=0.2.8,<0.3.0)", "numexpr (>=2.8.6,<3.0.0)", "openai (<2)", "openapi-pydantic (>=0.3.2,<0.4.0)", "oracle-ads (>=2.9.1,<3.0.0)", "pandas (>=2.0.1,<3.0.0)", "pdfminer-six (>=20221105,<20221106)", "pgvector (>=0.1.6,<0.2.0)", "praw (>=7.7.1,<8.0.0)", "psychicapi (>=0.8.0,<0.9.0)", "py-trello (>=0.19.0,<0.20.0)", "pymupdf (>=1.22.3,<2.0.0)", "pypdf (>=3.4.0,<4.0.0)", "pypdfium2 (>=4.10.0,<5.0.0)", "pyspark (>=3.4.0,<4.0.0)", "rank-bm25 (>=0.2.2,<0.3.0)", "rapidfuzz (>=3.1.1,<4.0.0)", "rapidocr-onnxruntime (>=1.3.2,<2.0.0)", "requests-toolbelt (>=1.0.0,<2.0.0)", "rspace_client (>=2.5.0,<3.0.0)", "scikit-learn (>=1.2.2,<2.0.0)", "sqlite-vss (>=0.1.2,<0.2.0)", "streamlit (>=1.18.0,<2.0.0)", "sympy (>=1.12,<2.0)", "telethon (>=1.28.5,<2.0.0)", "timescale-vector (>=0.0.1,<0.0.2)", "tqdm (>=4.48.0)", "upstash-redis (>=0.15.0,<0.16.0)", "xata (>=1.0.0a7,<2.0.0)", "xmltodict (>=0.13.0,<0.14.0)"] +extended-testing = ["aiosqlite (>=0.19.0,<0.20.0)", "aleph-alpha-client (>=2.15.0,<3.0.0)", "anthropic (>=0.3.11,<0.4.0)", "arxiv (>=1.4,<2.0)", "assemblyai (>=0.17.0,<0.18.0)", "atlassian-python-api (>=3.36.0,<4.0.0)", "azure-ai-documentintelligence (>=1.0.0b1,<2.0.0)", "beautifulsoup4 (>=4,<5)", "bibtexparser (>=1.4.0,<2.0.0)", "cassio (>=0.1.0,<0.2.0)", "chardet (>=5.1.0,<6.0.0)", "cohere (>=4,<5)", "dashvector (>=1.0.1,<2.0.0)", "databricks-vectorsearch (>=0.21,<0.22)", "datasets (>=2.15.0,<3.0.0)", "dgml-utils (>=0.3.0,<0.4.0)", "esprima (>=4.0.1,<5.0.0)", "faiss-cpu (>=1,<2)", "feedparser (>=6.0.10,<7.0.0)", "fireworks-ai (>=0.9.0,<0.10.0)", "geopandas (>=0.13.1,<0.14.0)", "gitpython (>=3.1.32,<4.0.0)", "google-cloud-documentai (>=2.20.1,<3.0.0)", "gql (>=3.4.1,<4.0.0)", "gradientai (>=1.4.0,<2.0.0)", "hologres-vector (>=0.0.6,<0.0.7)", "html2text (>=2020.1.16,<2021.0.0)", "javelin-sdk (>=0.1.8,<0.2.0)", "jinja2 (>=3,<4)", "jq (>=1.4.1,<2.0.0)", "jsonschema (>1)", "lxml (>=4.9.2,<5.0.0)", "markdownify (>=0.11.6,<0.12.0)", "motor (>=3.3.1,<4.0.0)", "msal (>=1.25.0,<2.0.0)", "mwparserfromhell (>=0.6.4,<0.7.0)", "mwxml (>=0.3.3,<0.4.0)", "newspaper3k (>=0.2.8,<0.3.0)", "numexpr (>=2.8.6,<3.0.0)", "openai (<2)", "openapi-pydantic (>=0.3.2,<0.4.0)", "oracle-ads (>=2.9.1,<3.0.0)", "pandas (>=2.0.1,<3.0.0)", "pdfminer-six (>=20221105,<20221106)", "pgvector (>=0.1.6,<0.2.0)", "praw (>=7.7.1,<8.0.0)", "psychicapi (>=0.8.0,<0.9.0)", "py-trello (>=0.19.0,<0.20.0)", "pymupdf (>=1.22.3,<2.0.0)", "pypdf (>=3.4.0,<4.0.0)", "pypdfium2 (>=4.10.0,<5.0.0)", "pyspark (>=3.4.0,<4.0.0)", "rank-bm25 (>=0.2.2,<0.3.0)", "rapidfuzz (>=3.1.1,<4.0.0)", "rapidocr-onnxruntime (>=1.3.2,<2.0.0)", "requests-toolbelt (>=1.0.0,<2.0.0)", "rspace_client (>=2.5.0,<3.0.0)", "scikit-learn (>=1.2.2,<2.0.0)", "sqlite-vss (>=0.1.2,<0.2.0)", "streamlit (>=1.18.0,<2.0.0)", "sympy (>=1.12,<2.0)", "telethon (>=1.28.5,<2.0.0)", "timescale-vector (>=0.0.1,<0.0.2)", "tqdm (>=4.48.0)", "upstash-redis (>=0.15.0,<0.16.0)", "xata (>=1.0.0a7,<2.0.0)", "xmltodict (>=0.13.0,<0.14.0)", "zhipuai (>=1.0.7,<2.0.0)"] [package.source] type = "directory" @@ -1698,7 +1765,7 @@ url = "libs/community" [[package]] name = "langchain-core" -version = "0.1.3" +version = "0.1.8" description = "Building applications with LLMs through composability" optional = false python-versions = ">=3.8.1,<4.0" @@ -1724,7 +1791,7 @@ url = "libs/core" [[package]] name = "langchain-experimental" -version = "0.0.47" +version = "0.0.48" description = "Building applications with LLMs through composability" optional = false python-versions = ">=3.8.1,<4.0" @@ -1732,8 +1799,8 @@ files = [] develop = true [package.dependencies] -langchain = ">=0.0.350,<0.1" -langchain-core = "^0.1" +langchain = "^0.1" +langchain-core = "^0.1.7" [package.extras] extended-testing = ["faker (>=19.3.1,<20.0.0)", "jinja2 (>=3,<4)", "presidio-analyzer (>=2.2.33,<3.0.0)", "presidio-anonymizer (>=2.2.33,<3.0.0)", "sentence-transformers (>=2,<3)", "vowpal-wabbit-next (==0.6.0)"] @@ -1742,15 +1809,34 @@ extended-testing = ["faker (>=19.3.1,<20.0.0)", "jinja2 (>=3,<4)", "presidio-ana type = "directory" url = "libs/experimental" +[[package]] +name = "langchain-openai" +version = "0.0.2" +description = "An integration package connecting OpenAI and LangChain" +optional = false +python-versions = ">=3.8.1,<4.0" +files = [] +develop = true + +[package.dependencies] +langchain-core = ">=0.1.7,<0.2" +numpy = "^1" +openai = "^1.6.1" +tiktoken = "^0.5.2" + +[package.source] +type = "directory" +url = "libs/partners/openai" + [[package]] name = "langsmith" -version = "0.0.74" +version = "0.0.79" description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform." optional = false python-versions = ">=3.8.1,<4.0" files = [ - {file = "langsmith-0.0.74-py3-none-any.whl", hash = "sha256:5d573dae3c59c84aca9e4d30a79ef49906151a32bf43830ff83863a825993ac2"}, - {file = "langsmith-0.0.74.tar.gz", hash = "sha256:249dae3625580fc9c1477be447ecd8dc1db76d1c8b59d0296abc027a37a0ce0b"}, + {file = "langsmith-0.0.79-py3-none-any.whl", hash = "sha256:be0374e913c36d9f6a13dd6b6e20a506066d5a0f3abfd476f9cf9e0b086ed744"}, + {file = "langsmith-0.0.79.tar.gz", hash = "sha256:d32639ccd18a92533b302f6f482255619afc8eb007fff91e37ee699d947c5e29"}, ] [package.dependencies] @@ -2354,6 +2440,29 @@ sphinx = ">=1.8" [package.extras] testing = ["matplotlib", "pytest", "pytest-cov"] +[[package]] +name = "openai" +version = "1.7.0" +description = "The official Python library for the openai API" +optional = false +python-versions = ">=3.7.1" +files = [ + {file = "openai-1.7.0-py3-none-any.whl", hash = "sha256:2282e8e15acb05df79cccba330c025b8e84284c7ec1f3fa31f167a8479066333"}, + {file = "openai-1.7.0.tar.gz", hash = "sha256:f2a8dcb739e8620c9318a2c6304ea72aebb572ba02fa1d586344405e80d567d3"}, +] + +[package.dependencies] +anyio = ">=3.5.0,<5" +distro = ">=1.7.0,<2" +httpx = ">=0.23.0,<1" +pydantic = ">=1.9.0,<3" +sniffio = "*" +tqdm = ">4" +typing-extensions = ">=4.7,<5" + +[package.extras] +datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"] + [[package]] name = "overrides" version = "7.4.0" @@ -3034,6 +3143,108 @@ files = [ attrs = ">=22.2.0" rpds-py = ">=0.7.0" +[[package]] +name = "regex" +version = "2023.12.25" +description = "Alternative regular expression module, to replace re." +optional = false +python-versions = ">=3.7" +files = [ + {file = "regex-2023.12.25-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0694219a1d54336fd0445ea382d49d36882415c0134ee1e8332afd1529f0baa5"}, + {file = "regex-2023.12.25-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b014333bd0217ad3d54c143de9d4b9a3ca1c5a29a6d0d554952ea071cff0f1f8"}, + {file = "regex-2023.12.25-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d865984b3f71f6d0af64d0d88f5733521698f6c16f445bb09ce746c92c97c586"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e0eabac536b4cc7f57a5f3d095bfa557860ab912f25965e08fe1545e2ed8b4c"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c25a8ad70e716f96e13a637802813f65d8a6760ef48672aa3502f4c24ea8b400"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9b6d73353f777630626f403b0652055ebfe8ff142a44ec2cf18ae470395766e"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9cc99d6946d750eb75827cb53c4371b8b0fe89c733a94b1573c9dd16ea6c9e4"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88d1f7bef20c721359d8675f7d9f8e414ec5003d8f642fdfd8087777ff7f94b5"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cb3fe77aec8f1995611f966d0c656fdce398317f850d0e6e7aebdfe61f40e1cd"}, + {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7aa47c2e9ea33a4a2a05f40fcd3ea36d73853a2aae7b4feab6fc85f8bf2c9704"}, + {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:df26481f0c7a3f8739fecb3e81bc9da3fcfae34d6c094563b9d4670b047312e1"}, + {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c40281f7d70baf6e0db0c2f7472b31609f5bc2748fe7275ea65a0b4601d9b392"}, + {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:d94a1db462d5690ebf6ae86d11c5e420042b9898af5dcf278bd97d6bda065423"}, + {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ba1b30765a55acf15dce3f364e4928b80858fa8f979ad41f862358939bdd1f2f"}, + {file = "regex-2023.12.25-cp310-cp310-win32.whl", hash = "sha256:150c39f5b964e4d7dba46a7962a088fbc91f06e606f023ce57bb347a3b2d4630"}, + {file = "regex-2023.12.25-cp310-cp310-win_amd64.whl", hash = "sha256:09da66917262d9481c719599116c7dc0c321ffcec4b1f510c4f8a066f8768105"}, + {file = "regex-2023.12.25-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1b9d811f72210fa9306aeb88385b8f8bcef0dfbf3873410413c00aa94c56c2b6"}, + {file = "regex-2023.12.25-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d902a43085a308cef32c0d3aea962524b725403fd9373dea18110904003bac97"}, + {file = "regex-2023.12.25-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d166eafc19f4718df38887b2bbe1467a4f74a9830e8605089ea7a30dd4da8887"}, + {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7ad32824b7f02bb3c9f80306d405a1d9b7bb89362d68b3c5a9be53836caebdb"}, + {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:636ba0a77de609d6510235b7f0e77ec494d2657108f777e8765efc060094c98c"}, + {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fda75704357805eb953a3ee15a2b240694a9a514548cd49b3c5124b4e2ad01b"}, + {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f72cbae7f6b01591f90814250e636065850c5926751af02bb48da94dfced7baa"}, + {file = "regex-2023.12.25-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db2a0b1857f18b11e3b0e54ddfefc96af46b0896fb678c85f63fb8c37518b3e7"}, + {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7502534e55c7c36c0978c91ba6f61703faf7ce733715ca48f499d3dbbd7657e0"}, + {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e8c7e08bb566de4faaf11984af13f6bcf6a08f327b13631d41d62592681d24fe"}, + {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:283fc8eed679758de38fe493b7d7d84a198b558942b03f017b1f94dda8efae80"}, + {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:f44dd4d68697559d007462b0a3a1d9acd61d97072b71f6d1968daef26bc744bd"}, + {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:67d3ccfc590e5e7197750fcb3a2915b416a53e2de847a728cfa60141054123d4"}, + {file = "regex-2023.12.25-cp311-cp311-win32.whl", hash = "sha256:68191f80a9bad283432385961d9efe09d783bcd36ed35a60fb1ff3f1ec2efe87"}, + {file = "regex-2023.12.25-cp311-cp311-win_amd64.whl", hash = "sha256:7d2af3f6b8419661a0c421584cfe8aaec1c0e435ce7e47ee2a97e344b98f794f"}, + {file = "regex-2023.12.25-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8a0ccf52bb37d1a700375a6b395bff5dd15c50acb745f7db30415bae3c2b0715"}, + {file = "regex-2023.12.25-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c3c4a78615b7762740531c27cf46e2f388d8d727d0c0c739e72048beb26c8a9d"}, + {file = "regex-2023.12.25-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ad83e7545b4ab69216cef4cc47e344d19622e28aabec61574b20257c65466d6a"}, + {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7a635871143661feccce3979e1727c4e094f2bdfd3ec4b90dfd4f16f571a87a"}, + {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d498eea3f581fbe1b34b59c697512a8baef88212f92e4c7830fcc1499f5b45a5"}, + {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:43f7cd5754d02a56ae4ebb91b33461dc67be8e3e0153f593c509e21d219c5060"}, + {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51f4b32f793812714fd5307222a7f77e739b9bc566dc94a18126aba3b92b98a3"}, + {file = "regex-2023.12.25-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba99d8077424501b9616b43a2d208095746fb1284fc5ba490139651f971d39d9"}, + {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4bfc2b16e3ba8850e0e262467275dd4d62f0d045e0e9eda2bc65078c0110a11f"}, + {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8c2c19dae8a3eb0ea45a8448356ed561be843b13cbc34b840922ddf565498c1c"}, + {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:60080bb3d8617d96f0fb7e19796384cc2467447ef1c491694850ebd3670bc457"}, + {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b77e27b79448e34c2c51c09836033056a0547aa360c45eeeb67803da7b0eedaf"}, + {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:518440c991f514331f4850a63560321f833979d145d7d81186dbe2f19e27ae3d"}, + {file = "regex-2023.12.25-cp312-cp312-win32.whl", hash = "sha256:e2610e9406d3b0073636a3a2e80db05a02f0c3169b5632022b4e81c0364bcda5"}, + {file = "regex-2023.12.25-cp312-cp312-win_amd64.whl", hash = "sha256:cc37b9aeebab425f11f27e5e9e6cf580be7206c6582a64467a14dda211abc232"}, + {file = "regex-2023.12.25-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:da695d75ac97cb1cd725adac136d25ca687da4536154cdc2815f576e4da11c69"}, + {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d126361607b33c4eb7b36debc173bf25d7805847346dd4d99b5499e1fef52bc7"}, + {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4719bb05094d7d8563a450cf8738d2e1061420f79cfcc1fa7f0a44744c4d8f73"}, + {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5dd58946bce44b53b06d94aa95560d0b243eb2fe64227cba50017a8d8b3cd3e2"}, + {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22a86d9fff2009302c440b9d799ef2fe322416d2d58fc124b926aa89365ec482"}, + {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2aae8101919e8aa05ecfe6322b278f41ce2994c4a430303c4cd163fef746e04f"}, + {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e692296c4cc2873967771345a876bcfc1c547e8dd695c6b89342488b0ea55cd8"}, + {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:263ef5cc10979837f243950637fffb06e8daed7f1ac1e39d5910fd29929e489a"}, + {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:d6f7e255e5fa94642a0724e35406e6cb7001c09d476ab5fce002f652b36d0c39"}, + {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:88ad44e220e22b63b0f8f81f007e8abbb92874d8ced66f32571ef8beb0643b2b"}, + {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:3a17d3ede18f9cedcbe23d2daa8a2cd6f59fe2bf082c567e43083bba3fb00347"}, + {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d15b274f9e15b1a0b7a45d2ac86d1f634d983ca40d6b886721626c47a400bf39"}, + {file = "regex-2023.12.25-cp37-cp37m-win32.whl", hash = "sha256:ed19b3a05ae0c97dd8f75a5d8f21f7723a8c33bbc555da6bbe1f96c470139d3c"}, + {file = "regex-2023.12.25-cp37-cp37m-win_amd64.whl", hash = "sha256:a6d1047952c0b8104a1d371f88f4ab62e6275567d4458c1e26e9627ad489b445"}, + {file = "regex-2023.12.25-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b43523d7bc2abd757119dbfb38af91b5735eea45537ec6ec3a5ec3f9562a1c53"}, + {file = "regex-2023.12.25-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:efb2d82f33b2212898f1659fb1c2e9ac30493ac41e4d53123da374c3b5541e64"}, + {file = "regex-2023.12.25-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b7fca9205b59c1a3d5031f7e64ed627a1074730a51c2a80e97653e3e9fa0d415"}, + {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086dd15e9435b393ae06f96ab69ab2d333f5d65cbe65ca5a3ef0ec9564dfe770"}, + {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e81469f7d01efed9b53740aedd26085f20d49da65f9c1f41e822a33992cb1590"}, + {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:34e4af5b27232f68042aa40a91c3b9bb4da0eeb31b7632e0091afc4310afe6cb"}, + {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9852b76ab558e45b20bf1893b59af64a28bd3820b0c2efc80e0a70a4a3ea51c1"}, + {file = "regex-2023.12.25-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff100b203092af77d1a5a7abe085b3506b7eaaf9abf65b73b7d6905b6cb76988"}, + {file = "regex-2023.12.25-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cc038b2d8b1470364b1888a98fd22d616fba2b6309c5b5f181ad4483e0017861"}, + {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:094ba386bb5c01e54e14434d4caabf6583334090865b23ef58e0424a6286d3dc"}, + {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5cd05d0f57846d8ba4b71d9c00f6f37d6b97d5e5ef8b3c3840426a475c8f70f4"}, + {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:9aa1a67bbf0f957bbe096375887b2505f5d8ae16bf04488e8b0f334c36e31360"}, + {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:98a2636994f943b871786c9e82bfe7883ecdaba2ef5df54e1450fa9869d1f756"}, + {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:37f8e93a81fc5e5bd8db7e10e62dc64261bcd88f8d7e6640aaebe9bc180d9ce2"}, + {file = "regex-2023.12.25-cp38-cp38-win32.whl", hash = "sha256:d78bd484930c1da2b9679290a41cdb25cc127d783768a0369d6b449e72f88beb"}, + {file = "regex-2023.12.25-cp38-cp38-win_amd64.whl", hash = "sha256:b521dcecebc5b978b447f0f69b5b7f3840eac454862270406a39837ffae4e697"}, + {file = "regex-2023.12.25-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f7bc09bc9c29ebead055bcba136a67378f03d66bf359e87d0f7c759d6d4ffa31"}, + {file = "regex-2023.12.25-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e14b73607d6231f3cc4622809c196b540a6a44e903bcfad940779c80dffa7be7"}, + {file = "regex-2023.12.25-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9eda5f7a50141291beda3edd00abc2d4a5b16c29c92daf8d5bd76934150f3edc"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc6bb9aa69aacf0f6032c307da718f61a40cf970849e471254e0e91c56ffca95"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:298dc6354d414bc921581be85695d18912bea163a8b23cac9a2562bbcd5088b1"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f4e475a80ecbd15896a976aa0b386c5525d0ed34d5c600b6d3ebac0a67c7ddf"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:531ac6cf22b53e0696f8e1d56ce2396311254eb806111ddd3922c9d937151dae"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22f3470f7524b6da61e2020672df2f3063676aff444db1daa283c2ea4ed259d6"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:89723d2112697feaa320c9d351e5f5e7b841e83f8b143dba8e2d2b5f04e10923"}, + {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0ecf44ddf9171cd7566ef1768047f6e66975788258b1c6c6ca78098b95cf9a3d"}, + {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:905466ad1702ed4acfd67a902af50b8db1feeb9781436372261808df7a2a7bca"}, + {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:4558410b7a5607a645e9804a3e9dd509af12fb72b9825b13791a37cd417d73a5"}, + {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:7e316026cc1095f2a3e8cc012822c99f413b702eaa2ca5408a513609488cb62f"}, + {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3b1de218d5375cd6ac4b5493e0b9f3df2be331e86520f23382f216c137913d20"}, + {file = "regex-2023.12.25-cp39-cp39-win32.whl", hash = "sha256:11a963f8e25ab5c61348d090bf1b07f1953929c13bd2309a0662e9ff680763c9"}, + {file = "regex-2023.12.25-cp39-cp39-win_amd64.whl", hash = "sha256:e693e233ac92ba83a87024e1d32b5f9ab15ca55ddd916d878146f4e3406b5c91"}, + {file = "regex-2023.12.25.tar.gz", hash = "sha256:29171aa128da69afdf4bde412d5bedc335f2ca8fcfe4489038577d05f16181e5"}, +] + [[package]] name = "requests" version = "2.31.0" @@ -3675,6 +3886,58 @@ tornado = ">=6.1.0" docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] test = ["pre-commit", "pytest (>=7.0)", "pytest-timeout"] +[[package]] +name = "tiktoken" +version = "0.5.2" +description = "tiktoken is a fast BPE tokeniser for use with OpenAI's models" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tiktoken-0.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8c4e654282ef05ec1bd06ead22141a9a1687991cef2c6a81bdd1284301abc71d"}, + {file = "tiktoken-0.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7b3134aa24319f42c27718c6967f3c1916a38a715a0fa73d33717ba121231307"}, + {file = "tiktoken-0.5.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6092e6e77730929c8c6a51bb0d7cfdf1b72b63c4d033d6258d1f2ee81052e9e5"}, + {file = "tiktoken-0.5.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72ad8ae2a747622efae75837abba59be6c15a8f31b4ac3c6156bc56ec7a8e631"}, + {file = "tiktoken-0.5.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:51cba7c8711afa0b885445f0637f0fcc366740798c40b981f08c5f984e02c9d1"}, + {file = "tiktoken-0.5.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3d8c7d2c9313f8e92e987d585ee2ba0f7c40a0de84f4805b093b634f792124f5"}, + {file = "tiktoken-0.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:692eca18c5fd8d1e0dde767f895c17686faaa102f37640e884eecb6854e7cca7"}, + {file = "tiktoken-0.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:138d173abbf1ec75863ad68ca289d4da30caa3245f3c8d4bfb274c4d629a2f77"}, + {file = "tiktoken-0.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7388fdd684690973fdc450b47dfd24d7f0cbe658f58a576169baef5ae4658607"}, + {file = "tiktoken-0.5.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a114391790113bcff670c70c24e166a841f7ea8f47ee2fe0e71e08b49d0bf2d4"}, + {file = "tiktoken-0.5.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca96f001e69f6859dd52926d950cfcc610480e920e576183497ab954e645e6ac"}, + {file = "tiktoken-0.5.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:15fed1dd88e30dfadcdd8e53a8927f04e1f6f81ad08a5ca824858a593ab476c7"}, + {file = "tiktoken-0.5.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:93f8e692db5756f7ea8cb0cfca34638316dcf0841fb8469de8ed7f6a015ba0b0"}, + {file = "tiktoken-0.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:bcae1c4c92df2ffc4fe9f475bf8148dbb0ee2404743168bbeb9dcc4b79dc1fdd"}, + {file = "tiktoken-0.5.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b76a1e17d4eb4357d00f0622d9a48ffbb23401dcf36f9716d9bd9c8e79d421aa"}, + {file = "tiktoken-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:01d8b171bb5df4035580bc26d4f5339a6fd58d06f069091899d4a798ea279d3e"}, + {file = "tiktoken-0.5.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42adf7d4fb1ed8de6e0ff2e794a6a15005f056a0d83d22d1d6755a39bffd9e7f"}, + {file = "tiktoken-0.5.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c3f894dbe0adb44609f3d532b8ea10820d61fdcb288b325a458dfc60fefb7db"}, + {file = "tiktoken-0.5.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:58ccfddb4e62f0df974e8f7e34a667981d9bb553a811256e617731bf1d007d19"}, + {file = "tiktoken-0.5.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58902a8bad2de4268c2a701f1c844d22bfa3cbcc485b10e8e3e28a050179330b"}, + {file = "tiktoken-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:5e39257826d0647fcac403d8fa0a474b30d02ec8ffc012cfaf13083e9b5e82c5"}, + {file = "tiktoken-0.5.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8bde3b0fbf09a23072d39c1ede0e0821f759b4fa254a5f00078909158e90ae1f"}, + {file = "tiktoken-0.5.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2ddee082dcf1231ccf3a591d234935e6acf3e82ee28521fe99af9630bc8d2a60"}, + {file = "tiktoken-0.5.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35c057a6a4e777b5966a7540481a75a31429fc1cb4c9da87b71c8b75b5143037"}, + {file = "tiktoken-0.5.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c4a049b87e28f1dc60509f8eb7790bc8d11f9a70d99b9dd18dfdd81a084ffe6"}, + {file = "tiktoken-0.5.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5bf5ce759089f4f6521ea6ed89d8f988f7b396e9f4afb503b945f5c949c6bec2"}, + {file = "tiktoken-0.5.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0c964f554af1a96884e01188f480dad3fc224c4bbcf7af75d4b74c4b74ae0125"}, + {file = "tiktoken-0.5.2-cp38-cp38-win_amd64.whl", hash = "sha256:368dd5726d2e8788e47ea04f32e20f72a2012a8a67af5b0b003d1e059f1d30a3"}, + {file = "tiktoken-0.5.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a2deef9115b8cd55536c0a02c0203512f8deb2447f41585e6d929a0b878a0dd2"}, + {file = "tiktoken-0.5.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2ed7d380195affbf886e2f8b92b14edfe13f4768ff5fc8de315adba5b773815e"}, + {file = "tiktoken-0.5.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c76fce01309c8140ffe15eb34ded2bb94789614b7d1d09e206838fc173776a18"}, + {file = "tiktoken-0.5.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60a5654d6a2e2d152637dd9a880b4482267dfc8a86ccf3ab1cec31a8c76bfae8"}, + {file = "tiktoken-0.5.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:41d4d3228e051b779245a8ddd21d4336f8975563e92375662f42d05a19bdff41"}, + {file = "tiktoken-0.5.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a5c1cdec2c92fcde8c17a50814b525ae6a88e8e5b02030dc120b76e11db93f13"}, + {file = "tiktoken-0.5.2-cp39-cp39-win_amd64.whl", hash = "sha256:84ddb36faedb448a50b246e13d1b6ee3437f60b7169b723a4b2abad75e914f3e"}, + {file = "tiktoken-0.5.2.tar.gz", hash = "sha256:f54c581f134a8ea96ce2023ab221d4d4d81ab614efa0b2fbce926387deb56c80"}, +] + +[package.dependencies] +regex = ">=2022.1.18" +requests = ">=2.26.0" + +[package.extras] +blobfile = ["blobfile (>=2)"] + [[package]] name = "tinycss2" version = "1.2.1" @@ -3746,6 +4009,26 @@ files = [ {file = "tornado-6.3.3.tar.gz", hash = "sha256:e7d8db41c0181c80d76c982aacc442c0783a2c54d6400fe028954201a2e032fe"}, ] +[[package]] +name = "tqdm" +version = "4.66.1" +description = "Fast, Extensible Progress Meter" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tqdm-4.66.1-py3-none-any.whl", hash = "sha256:d302b3c5b53d47bce91fea46679d9c3c6508cf6332229aa1e7d8653723793386"}, + {file = "tqdm-4.66.1.tar.gz", hash = "sha256:d88e651f9db8d8551a62556d3cff9e3034274ca5d66e93197cf2490e2dcb69c7"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"] +notebook = ["ipywidgets (>=6)"] +slack = ["slack-sdk"] +telegram = ["requests"] + [[package]] name = "traitlets" version = "5.11.1" @@ -3998,4 +4281,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = ">=3.8.1,<4.0" -content-hash = "581c178796dbb76589632e687d353a336ca23b3cdda7075720660b479dc85fa2" +content-hash = "0ee5840cf8fda328bd967f6bd3c9be4e698ccfdd4b8b14c970bad7f2d338ec81" diff --git a/pyproject.toml b/pyproject.toml index b133ad83c374c..1220209cb7fdb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,7 @@ langchain-core = { path = "libs/core/", develop = true } langchain-community = { path = "libs/community/", develop = true } langchain = { path = "libs/langchain/", develop = true } langchain-experimental = { path = "libs/experimental/", develop = true } +langchain-openai = { path = "libs/partners/openai", develop = true } [tool.poetry.group.codespell.dependencies] codespell = "^2.2.0" @@ -44,6 +45,7 @@ langchain-core = { path = "libs/core/", develop = true } langchain-community = { path = "libs/community/", develop = true } langchain = { path = "libs/langchain/", develop = true } langchain-experimental = { path = "libs/experimental/", develop = true } +langchain-openai = { path = "libs/partners/openai", develop = true } [tool.poetry.group.test.dependencies] diff --git a/templates/hyde/README.md b/templates/hyde/README.md index 8edfecb7f413c..35ea291eca18b 100644 --- a/templates/hyde/README.md +++ b/templates/hyde/README.md @@ -1,7 +1,7 @@ # hyde -This template HyDE with RAG. +This template uses HyDE with RAG. Hyde is a retrieval method that stands for Hypothetical Document Embeddings (HyDE). It is a method used to enhance retrieval by generating a hypothetical document for an incoming query. diff --git a/templates/neo4j-semantic-layer/README.md b/templates/neo4j-semantic-layer/README.md index c2dbe57fe8fa8..7019d627bce12 100644 --- a/templates/neo4j-semantic-layer/README.md +++ b/templates/neo4j-semantic-layer/README.md @@ -2,6 +2,7 @@ This template is designed to implement an agent capable of interacting with a graph database like Neo4j through a semantic layer using OpenAI function calling. The semantic layer equips the agent with a suite of robust tools, allowing it to interact with the graph databas based on the user's intent. +Learn more about the semantic layer template in the [corresponding blog post](https://medium.com/towards-data-science/enhancing-interaction-between-language-models-and-graph-databases-via-a-semantic-layer-0a78ad3eba49). ![Diagram illustrating the workflow of the Neo4j semantic layer with an agent interacting with tools like Information, Recommendation, and Memory, connected to a knowledge graph.](https://raw.githubusercontent.com/langchain-ai/langchain/master/templates/neo4j-semantic-layer/static/workflow.png "Neo4j Semantic Layer Workflow Diagram") diff --git a/templates/nvidia-rag-canonical/README.md b/templates/nvidia-rag-canonical/README.md index 8ad465813433a..6e095a4749625 100644 --- a/templates/nvidia-rag-canonical/README.md +++ b/templates/nvidia-rag-canonical/README.md @@ -45,16 +45,16 @@ langchain app add nvidia-rag-canonical And add the following code to your `server.py` file: ```python -from nvidia_rag_canonical import chain as rag_nvidia_chain +from nvidia_rag_canonical import chain as nvidia_rag_canonical_chain -add_routes(app, rag_nvidia_chain, path="/nvidia-rag") +add_routes(app, nvidia_rag_canonical_chain, path="/nvidia-rag-canonical") ``` If you want to set up an ingestion pipeline, you can add the following code to your `server.py` file: ```python -from rag_nvidia_canonical import ingest as rag_nvidia_ingest +from nvidia_rag_canonical import ingest as nvidia_rag_ingest -add_routes(app, rag_nvidia_ingest, path="/nvidia-rag-ingest") +add_routes(app, nvidia_rag_ingest, path="/nvidia-rag-ingest") ``` Note that for files ingested by the ingestion API, the server will need to be restarted for the newly ingested files to be accessible by the retriever. @@ -84,14 +84,14 @@ This will start the FastAPI app with a server is running locally at [http://localhost:8000](http://localhost:8000) We can see all templates at [http://127.0.0.1:8000/docs](http://127.0.0.1:8000/docs) -We can access the playground at [http://127.0.0.1:8000/nvidia-rag/playground](http://127.0.0.1:8000/nvidia-rag/playground) +We can access the playground at [http://127.0.0.1:8000/nvidia-rag-canonical/playground](http://127.0.0.1:8000/nvidia-rag-canonical/playground) We can access the template from code with: ```python from langserve.client import RemoteRunnable -runnable = RemoteRunnable("http://localhost:8000/nvidia-rag") +runnable = RemoteRunnable("http://localhost:8000/nvidia-rag-canonical") ``` diff --git a/templates/nvidia-rag-canonical/nvidia_rag_canonical.ipynb b/templates/nvidia-rag-canonical/nvidia_rag_canonical.ipynb new file mode 100644 index 0000000000000..eaff8b2e6a6d0 --- /dev/null +++ b/templates/nvidia-rag-canonical/nvidia_rag_canonical.ipynb @@ -0,0 +1,52 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "id": "681a5d1e", + "metadata": {}, + "source": [ + "## Connect to template\n", + "\n", + "In `server.py`, set -\n", + "```\n", + "add_routes(app, nvidia_rag_canonical_chain, path=\"/nvidia_rag_canonical\")\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d774be2a", + "metadata": {}, + "outputs": [], + "source": [ + "from langserve.client import RemoteRunnable\n", + "\n", + "rag_app = RemoteRunnable(\"http://0.0.0.0:8000/nvidia_rag_canonical\")\n", + "rag_app.invoke(\"How many Americans receive Social Security Benefits?\")" + ] + } + ], + "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.9.16" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/templates/nvidia-rag-canonical/poetry.lock b/templates/nvidia-rag-canonical/poetry.lock index f3c44dd28be40..de556e11e1467 100644 --- a/templates/nvidia-rag-canonical/poetry.lock +++ b/templates/nvidia-rag-canonical/poetry.lock @@ -1337,8 +1337,8 @@ files = [ [package.dependencies] numpy = [ {version = ">=1.20.3", markers = "python_version < \"3.10\""}, - {version = ">=1.21.0", markers = "python_version >= \"3.10\" and python_version < \"3.11\""}, {version = ">=1.23.2", markers = "python_version >= \"3.11\""}, + {version = ">=1.21.0", markers = "python_version >= \"3.10\" and python_version < \"3.11\""}, ] python-dateutil = ">=2.8.2" pytz = ">=2020.1" @@ -1660,6 +1660,27 @@ pyarrow = ">=12.0.0" requests = "*" ujson = ">=2.0.0" +[[package]] +name = "pypdf" +version = "3.17.4" +description = "A pure-python PDF library capable of splitting, merging, cropping, and transforming PDF files" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pypdf-3.17.4-py3-none-any.whl", hash = "sha256:6aa0f61b33779b64486de3f42835d3668badd48dac4a536aeb87da187a5eacd2"}, + {file = "pypdf-3.17.4.tar.gz", hash = "sha256:ec96e2e4fc9648ac609d19c00d41e9d606e0ae2ce5a0bbe7691426f5f157166a"}, +] + +[package.dependencies] +typing_extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.10\""} + +[package.extras] +crypto = ["PyCryptodome", "cryptography"] +dev = ["black", "flit", "pip-tools", "pre-commit (<2.18.0)", "pytest-cov", "pytest-socket", "pytest-timeout", "pytest-xdist", "wheel"] +docs = ["myst_parser", "sphinx", "sphinx_rtd_theme"] +full = ["Pillow (>=8.0.0)", "PyCryptodome", "cryptography"] +image = ["Pillow (>=8.0.0)"] + [[package]] name = "python-dateutil" version = "2.8.2" @@ -1711,6 +1732,7 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -1718,8 +1740,15 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -1736,6 +1765,7 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -1743,6 +1773,7 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -2255,4 +2286,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = ">=3.8.1,<4.0" -content-hash = "f5c0f88fe09baa6cb1ea4cf9d615e98efa3081fbccfc03688f1e2fbcf42273dd" +content-hash = "aa7dec65825e265779c9f00b10ffe8f34c96b25e8385e9e5235b89c411345a00" diff --git a/templates/nvidia-rag-canonical/pyproject.toml b/templates/nvidia-rag-canonical/pyproject.toml index 8e9b8944320c7..b444a706f6a04 100644 --- a/templates/nvidia-rag-canonical/pyproject.toml +++ b/templates/nvidia-rag-canonical/pyproject.toml @@ -10,6 +10,7 @@ python = ">=3.8.1,<4.0" langchain = "^0.1" pymilvus = ">=2.3.0" langchain-nvidia-aiplay = "^0.0.2" +pypdf = ">=3.1" [tool.poetry.group.dev.dependencies] langchain-cli = ">=0.0.20" diff --git a/templates/rag-vectara-multiquery/README.md b/templates/rag-vectara-multiquery/README.md index b12b67942d413..e2018a51937ea 100644 --- a/templates/rag-vectara-multiquery/README.md +++ b/templates/rag-vectara-multiquery/README.md @@ -23,20 +23,20 @@ pip install -U langchain-cli To create a new LangChain project and install this as the only package, you can do: ```shell -langchain app new my-app --package rag-vectara +langchain app new my-app --package rag-vectara-multiquery ``` If you want to add this to an existing project, you can just run: ```shell -langchain app add rag-vectara +langchain app add rag-vectara-multiquery ``` And add the following code to your `server.py` file: ```python from rag_vectara import chain as rag_vectara_chain -add_routes(app, rag_vectara_chain, path="/rag-vectara") +add_routes(app, rag_vectara_chain, path="/rag-vectara-multiquery") ``` (Optional) Let's now configure LangSmith. @@ -61,12 +61,12 @@ This will start the FastAPI app with a server is running locally at [http://localhost:8000](http://localhost:8000) We can see all templates at [http://127.0.0.1:8000/docs](http://127.0.0.1:8000/docs) -We can access the playground at [http://127.0.0.1:8000/rag-vectara/playground](http://127.0.0.1:8000/rag-vectara/playground) +We can access the playground at [http://127.0.0.1:8000/rag-vectara-multiquery/playground](http://127.0.0.1:8000/rag-vectara-multiquery/playground) We can access the template from code with: ```python from langserve.client import RemoteRunnable -runnable = RemoteRunnable("http://localhost:8000/rag-vectara") +runnable = RemoteRunnable("http://localhost:8000/rag-vectara-multiquery") ``` diff --git a/templates/rag-vectara-multiquery/rag_vectara_multiquery/chain.py b/templates/rag-vectara-multiquery/rag_vectara_multiquery/chain.py index 9b769e9bd04b5..1fd9354b7fa37 100644 --- a/templates/rag-vectara-multiquery/rag_vectara_multiquery/chain.py +++ b/templates/rag-vectara-multiquery/rag_vectara_multiquery/chain.py @@ -41,7 +41,6 @@ # We extract the summary from the RAG output, which is the last document # (if summary is enabled) # Note that if you want to extract the citation information, you can use res[:-1]] -model = ChatOpenAI() chain = ( RunnableParallel({"context": retriever, "question": RunnablePassthrough()}) | (lambda res: res[-1]) diff --git a/templates/retrieval-agent/README.md b/templates/retrieval-agent/README.md index 1e4fae3b9e006..7b3a32bd909c4 100644 --- a/templates/retrieval-agent/README.md +++ b/templates/retrieval-agent/README.md @@ -8,10 +8,9 @@ By default, this does retrieval over Arxiv. Since we are using Azure OpenAI, we will need to set the following environment variables: ```shell -export AZURE_OPENAI_API_BASE=... +export AZURE_OPENAI_ENDPOINT=... export AZURE_OPENAI_API_VERSION=... export AZURE_OPENAI_API_KEY=... -export AZURE_OPENAI_DEPLOYMENT_NAME=... ``` ## Usage diff --git a/templates/retrieval-agent/poetry.lock b/templates/retrieval-agent/poetry.lock index a482a7bd0c832..28eddbde4f15a 100644 --- a/templates/retrieval-agent/poetry.lock +++ b/templates/retrieval-agent/poetry.lock @@ -501,20 +501,20 @@ smmap = ">=3.0.1,<6" [[package]] name = "gitpython" -version = "3.1.40" +version = "3.1.41" description = "GitPython is a Python library used to interact with Git repositories" optional = false python-versions = ">=3.7" files = [ - {file = "GitPython-3.1.40-py3-none-any.whl", hash = "sha256:cf14627d5a8049ffbf49915732e5eddbe8134c3bdb9d476e6182b676fc573f8a"}, - {file = "GitPython-3.1.40.tar.gz", hash = "sha256:22b126e9ffb671fdd0c129796343a02bf67bf2994b35449ffc9321aa755e18a4"}, + {file = "GitPython-3.1.41-py3-none-any.whl", hash = "sha256:c36b6634d069b3f719610175020a9aed919421c87552185b085e04fbbdb10b7c"}, + {file = "GitPython-3.1.41.tar.gz", hash = "sha256:ed66e624884f76df22c8e16066d567aaa5a37d5b5fa19db2c6df6f7156db9048"}, ] [package.dependencies] gitdb = ">=4.0.1,<5" [package.extras] -test = ["black", "coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", "pytest", "pytest-cov", "pytest-instafail", "pytest-subtests", "pytest-sugar"] +test = ["black", "coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "sumtypes"] [[package]] name = "greenlet" @@ -692,13 +692,13 @@ files = [ [[package]] name = "langchain" -version = "0.1.0" +version = "0.1.1" description = "Building applications with LLMs through composability" optional = false python-versions = ">=3.8.1,<4.0" files = [ - {file = "langchain-0.1.0-py3-none-any.whl", hash = "sha256:8652e74b039333a55c79faff4400b077ba1bd0ddce5255574e42d301c05c1733"}, - {file = "langchain-0.1.0.tar.gz", hash = "sha256:d43119f8d3fda2c8ddf8c3a19bd5b94b347e27d1867ff14a921b90bdbed0668a"}, + {file = "langchain-0.1.1-py3-none-any.whl", hash = "sha256:3f1dcf458bbd603447e93ece99fe6611b1fafa16dc67464b1c8091dd475242f9"}, + {file = "langchain-0.1.1.tar.gz", hash = "sha256:a9616544b78ccf1a5b286fae7926e00beea6dc5b8fda983e5180313fefd3dfab"}, ] [package.dependencies] @@ -706,8 +706,8 @@ aiohttp = ">=3.8.3,<4.0.0" async-timeout = {version = ">=4.0.0,<5.0.0", markers = "python_version < \"3.11\""} dataclasses-json = ">=0.5.7,<0.7" jsonpatch = ">=1.33,<2.0" -langchain-community = ">=0.0.9,<0.1" -langchain-core = ">=0.1.7,<0.2" +langchain-community = ">=0.0.13,<0.1" +langchain-core = ">=0.1.9,<0.2" langsmith = ">=0.0.77,<0.1.0" numpy = ">=1,<2" pydantic = ">=1,<3" @@ -750,19 +750,19 @@ uvicorn = ">=0.23.2,<0.24.0" [[package]] name = "langchain-community" -version = "0.0.9" +version = "0.0.13" description = "Community contributed LangChain integrations." optional = false python-versions = ">=3.8.1,<4.0" files = [ - {file = "langchain_community-0.0.9-py3-none-any.whl", hash = "sha256:21e1f96c776541255b7067f32aafbf065f78a33be8f0e2660080ddc3e9ed48b7"}, - {file = "langchain_community-0.0.9.tar.gz", hash = "sha256:b14f10b249fd61b0b8e3d2896f85c2d577eb4a5e2ae01291e2a4ebbe1bb3c370"}, + {file = "langchain_community-0.0.13-py3-none-any.whl", hash = "sha256:655196e446e7f37f4882221b6f3f791d6add28ea596d521ccf6f4507386b9a13"}, + {file = "langchain_community-0.0.13.tar.gz", hash = "sha256:cf66c6ff7fcbeb582f5e88ee00decea7fdeca5ccddda725046f28efc697c41a7"}, ] [package.dependencies] aiohttp = ">=3.8.3,<4.0.0" dataclasses-json = ">=0.5.7,<0.7" -langchain-core = ">=0.1.7,<0.2" +langchain-core = ">=0.1.9,<0.2" langsmith = ">=0.0.63,<0.1.0" numpy = ">=1,<2" PyYAML = ">=5.3" @@ -776,13 +776,13 @@ extended-testing = ["aiosqlite (>=0.19.0,<0.20.0)", "aleph-alpha-client (>=2.15. [[package]] name = "langchain-core" -version = "0.1.7" +version = "0.1.12" description = "Building applications with LLMs through composability" optional = false python-versions = ">=3.8.1,<4.0" files = [ - {file = "langchain_core-0.1.7-py3-none-any.whl", hash = "sha256:c66327dbb4b7d4ab911556aa0511ebf4f40801ad66d98778fb5566dba45b0091"}, - {file = "langchain_core-0.1.7.tar.gz", hash = "sha256:c05211a309721d67aa5a681c946a2f010e14632a2bea3728da0a30a2534efa9e"}, + {file = "langchain_core-0.1.12-py3-none-any.whl", hash = "sha256:d11c6262f7a9deff7de8fdf14498b8a951020dfed3a80f2358ab731ad04abef0"}, + {file = "langchain_core-0.1.12.tar.gz", hash = "sha256:f18e9300e9a07589b3e280e51befbc5a4513f535949406e55eb7a2dc40c3ce66"}, ] [package.dependencies] @@ -798,15 +798,32 @@ tenacity = ">=8.1.0,<9.0.0" [package.extras] extended-testing = ["jinja2 (>=3,<4)"] +[[package]] +name = "langchain-openai" +version = "0.0.2.post1" +description = "An integration package connecting OpenAI and LangChain" +optional = false +python-versions = ">=3.8.1,<4.0" +files = [ + {file = "langchain_openai-0.0.2.post1-py3-none-any.whl", hash = "sha256:ba468b94c23da9d8ccefe5d5a3c1c65b4b9702292523e53acc689a9110022e26"}, + {file = "langchain_openai-0.0.2.post1.tar.gz", hash = "sha256:f8e78db4a663feeac71d9f036b9422406c199ea3ef4c97d99ff392c93530e073"}, +] + +[package.dependencies] +langchain-core = ">=0.1.7,<0.2" +numpy = ">=1,<2" +openai = ">=1.6.1,<2.0.0" +tiktoken = ">=0.5.2,<0.6.0" + [[package]] name = "langserve" -version = "0.0.38" +version = "0.0.39" description = "" optional = false python-versions = ">=3.8.1,<4.0.0" files = [ - {file = "langserve-0.0.38-py3-none-any.whl", hash = "sha256:c43019fb74dd50a95561cba5bdc9f6ac43e28ea08fba01a7f5910815fca60550"}, - {file = "langserve-0.0.38.tar.gz", hash = "sha256:384c56d0aabbc0af8f49edc1409a9ac0ba8fbab5d15e9f2312071dbdb053d47e"}, + {file = "langserve-0.0.39-py3-none-any.whl", hash = "sha256:3a8dc3af3c4a18b6867f1a32b75968035ea5ecf5481862fd9d4b51bb06f43e65"}, + {file = "langserve-0.0.39.tar.gz", hash = "sha256:c5ab2a539e7a008ca017cbe93dd6210aca9f382989906eda307464eac7182a41"}, ] [package.dependencies] @@ -825,13 +842,13 @@ server = ["fastapi (>=0.90.1,<1)", "sse-starlette (>=1.3.0,<2.0.0)"] [[package]] name = "langsmith" -version = "0.0.77" +version = "0.0.83" description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform." optional = false python-versions = ">=3.8.1,<4.0" files = [ - {file = "langsmith-0.0.77-py3-none-any.whl", hash = "sha256:750c0aa9177240c64e131d831e009ed08dd59038f7cabbd0bbcf62ccb7c8dcac"}, - {file = "langsmith-0.0.77.tar.gz", hash = "sha256:c4c8d3a96ad8671a41064f3ccc673e2e22a4153e823b19f915c9c9b8a4f33a2c"}, + {file = "langsmith-0.0.83-py3-none-any.whl", hash = "sha256:a5bb7ac58c19a415a9d5f51db56dd32ee2cd7343a00825bbc2018312eb3d122a"}, + {file = "langsmith-0.0.83.tar.gz", hash = "sha256:94427846b334ad9bdbec3266fee12903fe9f5448f628667689d0412012aaf392"}, ] [package.dependencies] @@ -864,22 +881,22 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] [[package]] name = "marshmallow" -version = "3.20.1" +version = "3.20.2" description = "A lightweight library for converting complex datatypes to and from native Python datatypes." optional = false python-versions = ">=3.8" files = [ - {file = "marshmallow-3.20.1-py3-none-any.whl", hash = "sha256:684939db93e80ad3561392f47be0230743131560a41c5110684c16e21ade0a5c"}, - {file = "marshmallow-3.20.1.tar.gz", hash = "sha256:5d2371bbe42000f2b3fb5eaa065224df7d8f8597bc19a1bbfa5bfe7fba8da889"}, + {file = "marshmallow-3.20.2-py3-none-any.whl", hash = "sha256:c21d4b98fee747c130e6bc8f45c4b3199ea66bc00c12ee1f639f0aeca034d5e9"}, + {file = "marshmallow-3.20.2.tar.gz", hash = "sha256:4c1daff273513dc5eb24b219a8035559dc573c8f322558ef85f5438ddd1236dd"}, ] [package.dependencies] packaging = ">=17.0" [package.extras] -dev = ["flake8 (==6.0.0)", "flake8-bugbear (==23.7.10)", "mypy (==1.4.1)", "pre-commit (>=2.4,<4.0)", "pytest", "pytz", "simplejson", "tox"] -docs = ["alabaster (==0.7.13)", "autodocsumm (==0.2.11)", "sphinx (==7.0.1)", "sphinx-issues (==3.0.1)", "sphinx-version-warning (==1.1.2)"] -lint = ["flake8 (==6.0.0)", "flake8-bugbear (==23.7.10)", "mypy (==1.4.1)", "pre-commit (>=2.4,<4.0)"] +dev = ["pre-commit (>=2.4,<4.0)", "pytest", "pytz", "simplejson", "tox"] +docs = ["alabaster (==0.7.15)", "autodocsumm (==0.2.12)", "sphinx (==7.2.6)", "sphinx-issues (==3.0.1)", "sphinx-version-warning (==1.1.2)"] +lint = ["pre-commit (>=2.4,<4.0)"] tests = ["pytest", "pytz", "simplejson"] [[package]] @@ -1026,13 +1043,13 @@ files = [ [[package]] name = "openai" -version = "1.6.1" +version = "1.8.0" description = "The official Python library for the openai API" optional = false python-versions = ">=3.7.1" files = [ - {file = "openai-1.6.1-py3-none-any.whl", hash = "sha256:bc9f774838d67ac29fb24cdeb2d58faf57de8b311085dcd1348f7aa02a96c7ee"}, - {file = "openai-1.6.1.tar.gz", hash = "sha256:d553ca9dbf9486b08e75b09e8671e4f638462aaadccfced632bf490fc3d75fa2"}, + {file = "openai-1.8.0-py3-none-any.whl", hash = "sha256:0f8f53805826103fdd8adaf379ad3ec23f9d867e698cbc14caf34b778d150175"}, + {file = "openai-1.8.0.tar.gz", hash = "sha256:93366be27802f517e89328801913d2a5ede45e3b86fdcab420385b8a1b88c767"}, ] [package.dependencies] @@ -1049,61 +1066,61 @@ datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"] [[package]] name = "orjson" -version = "3.9.10" +version = "3.9.12" description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" optional = false python-versions = ">=3.8" files = [ - {file = "orjson-3.9.10-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c18a4da2f50050a03d1da5317388ef84a16013302a5281d6f64e4a3f406aabc4"}, - {file = "orjson-3.9.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5148bab4d71f58948c7c39d12b14a9005b6ab35a0bdf317a8ade9a9e4d9d0bd5"}, - {file = "orjson-3.9.10-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4cf7837c3b11a2dfb589f8530b3cff2bd0307ace4c301e8997e95c7468c1378e"}, - {file = "orjson-3.9.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c62b6fa2961a1dcc51ebe88771be5319a93fd89bd247c9ddf732bc250507bc2b"}, - {file = "orjson-3.9.10-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deeb3922a7a804755bbe6b5be9b312e746137a03600f488290318936c1a2d4dc"}, - {file = "orjson-3.9.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1234dc92d011d3554d929b6cf058ac4a24d188d97be5e04355f1b9223e98bbe9"}, - {file = "orjson-3.9.10-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:06ad5543217e0e46fd7ab7ea45d506c76f878b87b1b4e369006bdb01acc05a83"}, - {file = "orjson-3.9.10-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4fd72fab7bddce46c6826994ce1e7de145ae1e9e106ebb8eb9ce1393ca01444d"}, - {file = "orjson-3.9.10-cp310-none-win32.whl", hash = "sha256:b5b7d4a44cc0e6ff98da5d56cde794385bdd212a86563ac321ca64d7f80c80d1"}, - {file = "orjson-3.9.10-cp310-none-win_amd64.whl", hash = "sha256:61804231099214e2f84998316f3238c4c2c4aaec302df12b21a64d72e2a135c7"}, - {file = "orjson-3.9.10-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:cff7570d492bcf4b64cc862a6e2fb77edd5e5748ad715f487628f102815165e9"}, - {file = "orjson-3.9.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed8bc367f725dfc5cabeed1ae079d00369900231fbb5a5280cf0736c30e2adf7"}, - {file = "orjson-3.9.10-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c812312847867b6335cfb264772f2a7e85b3b502d3a6b0586aa35e1858528ab1"}, - {file = "orjson-3.9.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9edd2856611e5050004f4722922b7b1cd6268da34102667bd49d2a2b18bafb81"}, - {file = "orjson-3.9.10-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:674eb520f02422546c40401f4efaf8207b5e29e420c17051cddf6c02783ff5ca"}, - {file = "orjson-3.9.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d0dc4310da8b5f6415949bd5ef937e60aeb0eb6b16f95041b5e43e6200821fb"}, - {file = "orjson-3.9.10-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e99c625b8c95d7741fe057585176b1b8783d46ed4b8932cf98ee145c4facf499"}, - {file = "orjson-3.9.10-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ec6f18f96b47299c11203edfbdc34e1b69085070d9a3d1f302810cc23ad36bf3"}, - {file = "orjson-3.9.10-cp311-none-win32.whl", hash = "sha256:ce0a29c28dfb8eccd0f16219360530bc3cfdf6bf70ca384dacd36e6c650ef8e8"}, - {file = "orjson-3.9.10-cp311-none-win_amd64.whl", hash = "sha256:cf80b550092cc480a0cbd0750e8189247ff45457e5a023305f7ef1bcec811616"}, - {file = "orjson-3.9.10-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:602a8001bdf60e1a7d544be29c82560a7b49319a0b31d62586548835bbe2c862"}, - {file = "orjson-3.9.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f295efcd47b6124b01255d1491f9e46f17ef40d3d7eabf7364099e463fb45f0f"}, - {file = "orjson-3.9.10-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:92af0d00091e744587221e79f68d617b432425a7e59328ca4c496f774a356071"}, - {file = "orjson-3.9.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5a02360e73e7208a872bf65a7554c9f15df5fe063dc047f79738998b0506a14"}, - {file = "orjson-3.9.10-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:858379cbb08d84fe7583231077d9a36a1a20eb72f8c9076a45df8b083724ad1d"}, - {file = "orjson-3.9.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666c6fdcaac1f13eb982b649e1c311c08d7097cbda24f32612dae43648d8db8d"}, - {file = "orjson-3.9.10-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3fb205ab52a2e30354640780ce4587157a9563a68c9beaf52153e1cea9aa0921"}, - {file = "orjson-3.9.10-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7ec960b1b942ee3c69323b8721df2a3ce28ff40e7ca47873ae35bfafeb4555ca"}, - {file = "orjson-3.9.10-cp312-none-win_amd64.whl", hash = "sha256:3e892621434392199efb54e69edfff9f699f6cc36dd9553c5bf796058b14b20d"}, - {file = "orjson-3.9.10-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:8b9ba0ccd5a7f4219e67fbbe25e6b4a46ceef783c42af7dbc1da548eb28b6531"}, - {file = "orjson-3.9.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e2ecd1d349e62e3960695214f40939bbfdcaeaaa62ccc638f8e651cf0970e5f"}, - {file = "orjson-3.9.10-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f433be3b3f4c66016d5a20e5b4444ef833a1f802ced13a2d852c637f69729c1"}, - {file = "orjson-3.9.10-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4689270c35d4bb3102e103ac43c3f0b76b169760aff8bcf2d401a3e0e58cdb7f"}, - {file = "orjson-3.9.10-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4bd176f528a8151a6efc5359b853ba3cc0e82d4cd1fab9c1300c5d957dc8f48c"}, - {file = "orjson-3.9.10-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a2ce5ea4f71681623f04e2b7dadede3c7435dfb5e5e2d1d0ec25b35530e277b"}, - {file = "orjson-3.9.10-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:49f8ad582da6e8d2cf663c4ba5bf9f83cc052570a3a767487fec6af839b0e777"}, - {file = "orjson-3.9.10-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2a11b4b1a8415f105d989876a19b173f6cdc89ca13855ccc67c18efbd7cbd1f8"}, - {file = "orjson-3.9.10-cp38-none-win32.whl", hash = "sha256:a353bf1f565ed27ba71a419b2cd3db9d6151da426b61b289b6ba1422a702e643"}, - {file = "orjson-3.9.10-cp38-none-win_amd64.whl", hash = "sha256:e28a50b5be854e18d54f75ef1bb13e1abf4bc650ab9d635e4258c58e71eb6ad5"}, - {file = "orjson-3.9.10-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ee5926746232f627a3be1cc175b2cfad24d0170d520361f4ce3fa2fd83f09e1d"}, - {file = "orjson-3.9.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a73160e823151f33cdc05fe2cea557c5ef12fdf276ce29bb4f1c571c8368a60"}, - {file = "orjson-3.9.10-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c338ed69ad0b8f8f8920c13f529889fe0771abbb46550013e3c3d01e5174deef"}, - {file = "orjson-3.9.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5869e8e130e99687d9e4be835116c4ebd83ca92e52e55810962446d841aba8de"}, - {file = "orjson-3.9.10-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2c1e559d96a7f94a4f581e2a32d6d610df5840881a8cba8f25e446f4d792df3"}, - {file = "orjson-3.9.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a3a3a72c9811b56adf8bcc829b010163bb2fc308877e50e9910c9357e78521"}, - {file = "orjson-3.9.10-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7f8fb7f5ecf4f6355683ac6881fd64b5bb2b8a60e3ccde6ff799e48791d8f864"}, - {file = "orjson-3.9.10-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c943b35ecdf7123b2d81d225397efddf0bce2e81db2f3ae633ead38e85cd5ade"}, - {file = "orjson-3.9.10-cp39-none-win32.whl", hash = "sha256:fb0b361d73f6b8eeceba47cd37070b5e6c9de5beaeaa63a1cb35c7e1a73ef088"}, - {file = "orjson-3.9.10-cp39-none-win_amd64.whl", hash = "sha256:b90f340cb6397ec7a854157fac03f0c82b744abdd1c0941a024c3c29d1340aff"}, - {file = "orjson-3.9.10.tar.gz", hash = "sha256:9ebbdbd6a046c304b1845e96fbcc5559cd296b4dfd3ad2509e33c4d9ce07d6a1"}, + {file = "orjson-3.9.12-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6b4e2bed7d00753c438e83b613923afdd067564ff7ed696bfe3a7b073a236e07"}, + {file = "orjson-3.9.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd1b8ec63f0bf54a50b498eedeccdca23bd7b658f81c524d18e410c203189365"}, + {file = "orjson-3.9.12-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ab8add018a53665042a5ae68200f1ad14c7953fa12110d12d41166f111724656"}, + {file = "orjson-3.9.12-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12756a108875526b76e505afe6d6ba34960ac6b8c5ec2f35faf73ef161e97e07"}, + {file = "orjson-3.9.12-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:890e7519c0c70296253660455f77e3a194554a3c45e42aa193cdebc76a02d82b"}, + {file = "orjson-3.9.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d664880d7f016efbae97c725b243b33c2cbb4851ddc77f683fd1eec4a7894146"}, + {file = "orjson-3.9.12-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:cfdaede0fa5b500314ec7b1249c7e30e871504a57004acd116be6acdda3b8ab3"}, + {file = "orjson-3.9.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6492ff5953011e1ba9ed1bf086835fd574bd0a3cbe252db8e15ed72a30479081"}, + {file = "orjson-3.9.12-cp310-none-win32.whl", hash = "sha256:29bf08e2eadb2c480fdc2e2daae58f2f013dff5d3b506edd1e02963b9ce9f8a9"}, + {file = "orjson-3.9.12-cp310-none-win_amd64.whl", hash = "sha256:0fc156fba60d6b50743337ba09f052d8afc8b64595112996d22f5fce01ab57da"}, + {file = "orjson-3.9.12-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:2849f88a0a12b8d94579b67486cbd8f3a49e36a4cb3d3f0ab352c596078c730c"}, + {file = "orjson-3.9.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3186b18754befa660b31c649a108a915493ea69b4fc33f624ed854ad3563ac65"}, + {file = "orjson-3.9.12-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cbbf313c9fb9d4f6cf9c22ced4b6682230457741daeb3d7060c5d06c2e73884a"}, + {file = "orjson-3.9.12-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99e8cd005b3926c3db9b63d264bd05e1bf4451787cc79a048f27f5190a9a0311"}, + {file = "orjson-3.9.12-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:59feb148392d9155f3bfed0a2a3209268e000c2c3c834fb8fe1a6af9392efcbf"}, + {file = "orjson-3.9.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4ae815a172a1f073b05b9e04273e3b23e608a0858c4e76f606d2d75fcabde0c"}, + {file = "orjson-3.9.12-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ed398f9a9d5a1bf55b6e362ffc80ac846af2122d14a8243a1e6510a4eabcb71e"}, + {file = "orjson-3.9.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d3cfb76600c5a1e6be91326b8f3b83035a370e727854a96d801c1ea08b708073"}, + {file = "orjson-3.9.12-cp311-none-win32.whl", hash = "sha256:a2b6f5252c92bcab3b742ddb3ac195c0fa74bed4319acd74f5d54d79ef4715dc"}, + {file = "orjson-3.9.12-cp311-none-win_amd64.whl", hash = "sha256:c95488e4aa1d078ff5776b58f66bd29d628fa59adcb2047f4efd3ecb2bd41a71"}, + {file = "orjson-3.9.12-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:d6ce2062c4af43b92b0221ed4f445632c6bf4213f8a7da5396a122931377acd9"}, + {file = "orjson-3.9.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:950951799967558c214cd6cceb7ceceed6f81d2c3c4135ee4a2c9c69f58aa225"}, + {file = "orjson-3.9.12-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2dfaf71499d6fd4153f5c86eebb68e3ec1bf95851b030a4b55c7637a37bbdee4"}, + {file = "orjson-3.9.12-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:659a8d7279e46c97661839035a1a218b61957316bf0202674e944ac5cfe7ed83"}, + {file = "orjson-3.9.12-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af17fa87bccad0b7f6fd8ac8f9cbc9ee656b4552783b10b97a071337616db3e4"}, + {file = "orjson-3.9.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd52dec9eddf4c8c74392f3fd52fa137b5f2e2bed1d9ae958d879de5f7d7cded"}, + {file = "orjson-3.9.12-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:640e2b5d8e36b970202cfd0799d11a9a4ab46cf9212332cd642101ec952df7c8"}, + {file = "orjson-3.9.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:daa438bd8024e03bcea2c5a92cd719a663a58e223fba967296b6ab9992259dbf"}, + {file = "orjson-3.9.12-cp312-none-win_amd64.whl", hash = "sha256:1bb8f657c39ecdb924d02e809f992c9aafeb1ad70127d53fb573a6a6ab59d549"}, + {file = "orjson-3.9.12-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:f4098c7674901402c86ba6045a551a2ee345f9f7ed54eeffc7d86d155c8427e5"}, + {file = "orjson-3.9.12-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5586a533998267458fad3a457d6f3cdbddbcce696c916599fa8e2a10a89b24d3"}, + {file = "orjson-3.9.12-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:54071b7398cd3f90e4bb61df46705ee96cb5e33e53fc0b2f47dbd9b000e238e1"}, + {file = "orjson-3.9.12-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:67426651faa671b40443ea6f03065f9c8e22272b62fa23238b3efdacd301df31"}, + {file = "orjson-3.9.12-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4a0cd56e8ee56b203abae7d482ac0d233dbfb436bb2e2d5cbcb539fe1200a312"}, + {file = "orjson-3.9.12-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a84a0c3d4841a42e2571b1c1ead20a83e2792644c5827a606c50fc8af7ca4bee"}, + {file = "orjson-3.9.12-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:09d60450cda3fa6c8ed17770c3a88473a16460cd0ff2ba74ef0df663b6fd3bb8"}, + {file = "orjson-3.9.12-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bc82a4db9934a78ade211cf2e07161e4f068a461c1796465d10069cb50b32a80"}, + {file = "orjson-3.9.12-cp38-none-win32.whl", hash = "sha256:61563d5d3b0019804d782137a4f32c72dc44c84e7d078b89d2d2a1adbaa47b52"}, + {file = "orjson-3.9.12-cp38-none-win_amd64.whl", hash = "sha256:410f24309fbbaa2fab776e3212a81b96a1ec6037259359a32ea79fbccfcf76aa"}, + {file = "orjson-3.9.12-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e773f251258dd82795fd5daeac081d00b97bacf1548e44e71245543374874bcf"}, + {file = "orjson-3.9.12-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b159baecfda51c840a619948c25817d37733a4d9877fea96590ef8606468b362"}, + {file = "orjson-3.9.12-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:975e72e81a249174840d5a8df977d067b0183ef1560a32998be340f7e195c730"}, + {file = "orjson-3.9.12-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:06e42e899dde61eb1851a9fad7f1a21b8e4be063438399b63c07839b57668f6c"}, + {file = "orjson-3.9.12-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c157e999e5694475a5515942aebeed6e43f7a1ed52267c1c93dcfde7d78d421"}, + {file = "orjson-3.9.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dde1bc7c035f2d03aa49dc8642d9c6c9b1a81f2470e02055e76ed8853cfae0c3"}, + {file = "orjson-3.9.12-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b0e9d73cdbdad76a53a48f563447e0e1ce34bcecef4614eb4b146383e6e7d8c9"}, + {file = "orjson-3.9.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:96e44b21fe407b8ed48afbb3721f3c8c8ce17e345fbe232bd4651ace7317782d"}, + {file = "orjson-3.9.12-cp39-none-win32.whl", hash = "sha256:cbd0f3555205bf2a60f8812133f2452d498dbefa14423ba90fe89f32276f7abf"}, + {file = "orjson-3.9.12-cp39-none-win_amd64.whl", hash = "sha256:03ea7ee7e992532c2f4a06edd7ee1553f0644790553a118e003e3c405add41fa"}, + {file = "orjson-3.9.12.tar.gz", hash = "sha256:da908d23a3b3243632b523344403b128722a5f45e278a8343c2bb67538dff0e4"}, ] [[package]] @@ -1317,6 +1334,108 @@ files = [ {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, ] +[[package]] +name = "regex" +version = "2023.12.25" +description = "Alternative regular expression module, to replace re." +optional = false +python-versions = ">=3.7" +files = [ + {file = "regex-2023.12.25-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0694219a1d54336fd0445ea382d49d36882415c0134ee1e8332afd1529f0baa5"}, + {file = "regex-2023.12.25-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b014333bd0217ad3d54c143de9d4b9a3ca1c5a29a6d0d554952ea071cff0f1f8"}, + {file = "regex-2023.12.25-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d865984b3f71f6d0af64d0d88f5733521698f6c16f445bb09ce746c92c97c586"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e0eabac536b4cc7f57a5f3d095bfa557860ab912f25965e08fe1545e2ed8b4c"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c25a8ad70e716f96e13a637802813f65d8a6760ef48672aa3502f4c24ea8b400"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9b6d73353f777630626f403b0652055ebfe8ff142a44ec2cf18ae470395766e"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9cc99d6946d750eb75827cb53c4371b8b0fe89c733a94b1573c9dd16ea6c9e4"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88d1f7bef20c721359d8675f7d9f8e414ec5003d8f642fdfd8087777ff7f94b5"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cb3fe77aec8f1995611f966d0c656fdce398317f850d0e6e7aebdfe61f40e1cd"}, + {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7aa47c2e9ea33a4a2a05f40fcd3ea36d73853a2aae7b4feab6fc85f8bf2c9704"}, + {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:df26481f0c7a3f8739fecb3e81bc9da3fcfae34d6c094563b9d4670b047312e1"}, + {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c40281f7d70baf6e0db0c2f7472b31609f5bc2748fe7275ea65a0b4601d9b392"}, + {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:d94a1db462d5690ebf6ae86d11c5e420042b9898af5dcf278bd97d6bda065423"}, + {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ba1b30765a55acf15dce3f364e4928b80858fa8f979ad41f862358939bdd1f2f"}, + {file = "regex-2023.12.25-cp310-cp310-win32.whl", hash = "sha256:150c39f5b964e4d7dba46a7962a088fbc91f06e606f023ce57bb347a3b2d4630"}, + {file = "regex-2023.12.25-cp310-cp310-win_amd64.whl", hash = "sha256:09da66917262d9481c719599116c7dc0c321ffcec4b1f510c4f8a066f8768105"}, + {file = "regex-2023.12.25-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1b9d811f72210fa9306aeb88385b8f8bcef0dfbf3873410413c00aa94c56c2b6"}, + {file = "regex-2023.12.25-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d902a43085a308cef32c0d3aea962524b725403fd9373dea18110904003bac97"}, + {file = "regex-2023.12.25-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d166eafc19f4718df38887b2bbe1467a4f74a9830e8605089ea7a30dd4da8887"}, + {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7ad32824b7f02bb3c9f80306d405a1d9b7bb89362d68b3c5a9be53836caebdb"}, + {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:636ba0a77de609d6510235b7f0e77ec494d2657108f777e8765efc060094c98c"}, + {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fda75704357805eb953a3ee15a2b240694a9a514548cd49b3c5124b4e2ad01b"}, + {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f72cbae7f6b01591f90814250e636065850c5926751af02bb48da94dfced7baa"}, + {file = "regex-2023.12.25-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db2a0b1857f18b11e3b0e54ddfefc96af46b0896fb678c85f63fb8c37518b3e7"}, + {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7502534e55c7c36c0978c91ba6f61703faf7ce733715ca48f499d3dbbd7657e0"}, + {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e8c7e08bb566de4faaf11984af13f6bcf6a08f327b13631d41d62592681d24fe"}, + {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:283fc8eed679758de38fe493b7d7d84a198b558942b03f017b1f94dda8efae80"}, + {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:f44dd4d68697559d007462b0a3a1d9acd61d97072b71f6d1968daef26bc744bd"}, + {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:67d3ccfc590e5e7197750fcb3a2915b416a53e2de847a728cfa60141054123d4"}, + {file = "regex-2023.12.25-cp311-cp311-win32.whl", hash = "sha256:68191f80a9bad283432385961d9efe09d783bcd36ed35a60fb1ff3f1ec2efe87"}, + {file = "regex-2023.12.25-cp311-cp311-win_amd64.whl", hash = "sha256:7d2af3f6b8419661a0c421584cfe8aaec1c0e435ce7e47ee2a97e344b98f794f"}, + {file = "regex-2023.12.25-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8a0ccf52bb37d1a700375a6b395bff5dd15c50acb745f7db30415bae3c2b0715"}, + {file = "regex-2023.12.25-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c3c4a78615b7762740531c27cf46e2f388d8d727d0c0c739e72048beb26c8a9d"}, + {file = "regex-2023.12.25-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ad83e7545b4ab69216cef4cc47e344d19622e28aabec61574b20257c65466d6a"}, + {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7a635871143661feccce3979e1727c4e094f2bdfd3ec4b90dfd4f16f571a87a"}, + {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d498eea3f581fbe1b34b59c697512a8baef88212f92e4c7830fcc1499f5b45a5"}, + {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:43f7cd5754d02a56ae4ebb91b33461dc67be8e3e0153f593c509e21d219c5060"}, + {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51f4b32f793812714fd5307222a7f77e739b9bc566dc94a18126aba3b92b98a3"}, + {file = "regex-2023.12.25-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba99d8077424501b9616b43a2d208095746fb1284fc5ba490139651f971d39d9"}, + {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4bfc2b16e3ba8850e0e262467275dd4d62f0d045e0e9eda2bc65078c0110a11f"}, + {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8c2c19dae8a3eb0ea45a8448356ed561be843b13cbc34b840922ddf565498c1c"}, + {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:60080bb3d8617d96f0fb7e19796384cc2467447ef1c491694850ebd3670bc457"}, + {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b77e27b79448e34c2c51c09836033056a0547aa360c45eeeb67803da7b0eedaf"}, + {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:518440c991f514331f4850a63560321f833979d145d7d81186dbe2f19e27ae3d"}, + {file = "regex-2023.12.25-cp312-cp312-win32.whl", hash = "sha256:e2610e9406d3b0073636a3a2e80db05a02f0c3169b5632022b4e81c0364bcda5"}, + {file = "regex-2023.12.25-cp312-cp312-win_amd64.whl", hash = "sha256:cc37b9aeebab425f11f27e5e9e6cf580be7206c6582a64467a14dda211abc232"}, + {file = "regex-2023.12.25-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:da695d75ac97cb1cd725adac136d25ca687da4536154cdc2815f576e4da11c69"}, + {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d126361607b33c4eb7b36debc173bf25d7805847346dd4d99b5499e1fef52bc7"}, + {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4719bb05094d7d8563a450cf8738d2e1061420f79cfcc1fa7f0a44744c4d8f73"}, + {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5dd58946bce44b53b06d94aa95560d0b243eb2fe64227cba50017a8d8b3cd3e2"}, + {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22a86d9fff2009302c440b9d799ef2fe322416d2d58fc124b926aa89365ec482"}, + {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2aae8101919e8aa05ecfe6322b278f41ce2994c4a430303c4cd163fef746e04f"}, + {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e692296c4cc2873967771345a876bcfc1c547e8dd695c6b89342488b0ea55cd8"}, + {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:263ef5cc10979837f243950637fffb06e8daed7f1ac1e39d5910fd29929e489a"}, + {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:d6f7e255e5fa94642a0724e35406e6cb7001c09d476ab5fce002f652b36d0c39"}, + {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:88ad44e220e22b63b0f8f81f007e8abbb92874d8ced66f32571ef8beb0643b2b"}, + {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:3a17d3ede18f9cedcbe23d2daa8a2cd6f59fe2bf082c567e43083bba3fb00347"}, + {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d15b274f9e15b1a0b7a45d2ac86d1f634d983ca40d6b886721626c47a400bf39"}, + {file = "regex-2023.12.25-cp37-cp37m-win32.whl", hash = "sha256:ed19b3a05ae0c97dd8f75a5d8f21f7723a8c33bbc555da6bbe1f96c470139d3c"}, + {file = "regex-2023.12.25-cp37-cp37m-win_amd64.whl", hash = "sha256:a6d1047952c0b8104a1d371f88f4ab62e6275567d4458c1e26e9627ad489b445"}, + {file = "regex-2023.12.25-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b43523d7bc2abd757119dbfb38af91b5735eea45537ec6ec3a5ec3f9562a1c53"}, + {file = "regex-2023.12.25-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:efb2d82f33b2212898f1659fb1c2e9ac30493ac41e4d53123da374c3b5541e64"}, + {file = "regex-2023.12.25-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b7fca9205b59c1a3d5031f7e64ed627a1074730a51c2a80e97653e3e9fa0d415"}, + {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086dd15e9435b393ae06f96ab69ab2d333f5d65cbe65ca5a3ef0ec9564dfe770"}, + {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e81469f7d01efed9b53740aedd26085f20d49da65f9c1f41e822a33992cb1590"}, + {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:34e4af5b27232f68042aa40a91c3b9bb4da0eeb31b7632e0091afc4310afe6cb"}, + {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9852b76ab558e45b20bf1893b59af64a28bd3820b0c2efc80e0a70a4a3ea51c1"}, + {file = "regex-2023.12.25-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff100b203092af77d1a5a7abe085b3506b7eaaf9abf65b73b7d6905b6cb76988"}, + {file = "regex-2023.12.25-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cc038b2d8b1470364b1888a98fd22d616fba2b6309c5b5f181ad4483e0017861"}, + {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:094ba386bb5c01e54e14434d4caabf6583334090865b23ef58e0424a6286d3dc"}, + {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5cd05d0f57846d8ba4b71d9c00f6f37d6b97d5e5ef8b3c3840426a475c8f70f4"}, + {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:9aa1a67bbf0f957bbe096375887b2505f5d8ae16bf04488e8b0f334c36e31360"}, + {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:98a2636994f943b871786c9e82bfe7883ecdaba2ef5df54e1450fa9869d1f756"}, + {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:37f8e93a81fc5e5bd8db7e10e62dc64261bcd88f8d7e6640aaebe9bc180d9ce2"}, + {file = "regex-2023.12.25-cp38-cp38-win32.whl", hash = "sha256:d78bd484930c1da2b9679290a41cdb25cc127d783768a0369d6b449e72f88beb"}, + {file = "regex-2023.12.25-cp38-cp38-win_amd64.whl", hash = "sha256:b521dcecebc5b978b447f0f69b5b7f3840eac454862270406a39837ffae4e697"}, + {file = "regex-2023.12.25-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f7bc09bc9c29ebead055bcba136a67378f03d66bf359e87d0f7c759d6d4ffa31"}, + {file = "regex-2023.12.25-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e14b73607d6231f3cc4622809c196b540a6a44e903bcfad940779c80dffa7be7"}, + {file = "regex-2023.12.25-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9eda5f7a50141291beda3edd00abc2d4a5b16c29c92daf8d5bd76934150f3edc"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc6bb9aa69aacf0f6032c307da718f61a40cf970849e471254e0e91c56ffca95"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:298dc6354d414bc921581be85695d18912bea163a8b23cac9a2562bbcd5088b1"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f4e475a80ecbd15896a976aa0b386c5525d0ed34d5c600b6d3ebac0a67c7ddf"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:531ac6cf22b53e0696f8e1d56ce2396311254eb806111ddd3922c9d937151dae"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22f3470f7524b6da61e2020672df2f3063676aff444db1daa283c2ea4ed259d6"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:89723d2112697feaa320c9d351e5f5e7b841e83f8b143dba8e2d2b5f04e10923"}, + {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0ecf44ddf9171cd7566ef1768047f6e66975788258b1c6c6ca78098b95cf9a3d"}, + {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:905466ad1702ed4acfd67a902af50b8db1feeb9781436372261808df7a2a7bca"}, + {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:4558410b7a5607a645e9804a3e9dd509af12fb72b9825b13791a37cd417d73a5"}, + {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:7e316026cc1095f2a3e8cc012822c99f413b702eaa2ca5408a513609488cb62f"}, + {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3b1de218d5375cd6ac4b5493e0b9f3df2be331e86520f23382f216c137913d20"}, + {file = "regex-2023.12.25-cp39-cp39-win32.whl", hash = "sha256:11a963f8e25ab5c61348d090bf1b07f1953929c13bd2309a0662e9ff680763c9"}, + {file = "regex-2023.12.25-cp39-cp39-win_amd64.whl", hash = "sha256:e693e233ac92ba83a87024e1d32b5f9ab15ca55ddd916d878146f4e3406b5c91"}, + {file = "regex-2023.12.25.tar.gz", hash = "sha256:29171aa128da69afdf4bde412d5bedc335f2ca8fcfe4489038577d05f16181e5"}, +] + [[package]] name = "requests" version = "2.31.0" @@ -1536,6 +1655,58 @@ files = [ [package.extras] doc = ["reno", "sphinx", "tornado (>=4.5)"] +[[package]] +name = "tiktoken" +version = "0.5.2" +description = "tiktoken is a fast BPE tokeniser for use with OpenAI's models" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tiktoken-0.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8c4e654282ef05ec1bd06ead22141a9a1687991cef2c6a81bdd1284301abc71d"}, + {file = "tiktoken-0.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7b3134aa24319f42c27718c6967f3c1916a38a715a0fa73d33717ba121231307"}, + {file = "tiktoken-0.5.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6092e6e77730929c8c6a51bb0d7cfdf1b72b63c4d033d6258d1f2ee81052e9e5"}, + {file = "tiktoken-0.5.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72ad8ae2a747622efae75837abba59be6c15a8f31b4ac3c6156bc56ec7a8e631"}, + {file = "tiktoken-0.5.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:51cba7c8711afa0b885445f0637f0fcc366740798c40b981f08c5f984e02c9d1"}, + {file = "tiktoken-0.5.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3d8c7d2c9313f8e92e987d585ee2ba0f7c40a0de84f4805b093b634f792124f5"}, + {file = "tiktoken-0.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:692eca18c5fd8d1e0dde767f895c17686faaa102f37640e884eecb6854e7cca7"}, + {file = "tiktoken-0.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:138d173abbf1ec75863ad68ca289d4da30caa3245f3c8d4bfb274c4d629a2f77"}, + {file = "tiktoken-0.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7388fdd684690973fdc450b47dfd24d7f0cbe658f58a576169baef5ae4658607"}, + {file = "tiktoken-0.5.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a114391790113bcff670c70c24e166a841f7ea8f47ee2fe0e71e08b49d0bf2d4"}, + {file = "tiktoken-0.5.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca96f001e69f6859dd52926d950cfcc610480e920e576183497ab954e645e6ac"}, + {file = "tiktoken-0.5.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:15fed1dd88e30dfadcdd8e53a8927f04e1f6f81ad08a5ca824858a593ab476c7"}, + {file = "tiktoken-0.5.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:93f8e692db5756f7ea8cb0cfca34638316dcf0841fb8469de8ed7f6a015ba0b0"}, + {file = "tiktoken-0.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:bcae1c4c92df2ffc4fe9f475bf8148dbb0ee2404743168bbeb9dcc4b79dc1fdd"}, + {file = "tiktoken-0.5.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b76a1e17d4eb4357d00f0622d9a48ffbb23401dcf36f9716d9bd9c8e79d421aa"}, + {file = "tiktoken-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:01d8b171bb5df4035580bc26d4f5339a6fd58d06f069091899d4a798ea279d3e"}, + {file = "tiktoken-0.5.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42adf7d4fb1ed8de6e0ff2e794a6a15005f056a0d83d22d1d6755a39bffd9e7f"}, + {file = "tiktoken-0.5.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c3f894dbe0adb44609f3d532b8ea10820d61fdcb288b325a458dfc60fefb7db"}, + {file = "tiktoken-0.5.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:58ccfddb4e62f0df974e8f7e34a667981d9bb553a811256e617731bf1d007d19"}, + {file = "tiktoken-0.5.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58902a8bad2de4268c2a701f1c844d22bfa3cbcc485b10e8e3e28a050179330b"}, + {file = "tiktoken-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:5e39257826d0647fcac403d8fa0a474b30d02ec8ffc012cfaf13083e9b5e82c5"}, + {file = "tiktoken-0.5.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8bde3b0fbf09a23072d39c1ede0e0821f759b4fa254a5f00078909158e90ae1f"}, + {file = "tiktoken-0.5.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2ddee082dcf1231ccf3a591d234935e6acf3e82ee28521fe99af9630bc8d2a60"}, + {file = "tiktoken-0.5.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35c057a6a4e777b5966a7540481a75a31429fc1cb4c9da87b71c8b75b5143037"}, + {file = "tiktoken-0.5.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c4a049b87e28f1dc60509f8eb7790bc8d11f9a70d99b9dd18dfdd81a084ffe6"}, + {file = "tiktoken-0.5.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5bf5ce759089f4f6521ea6ed89d8f988f7b396e9f4afb503b945f5c949c6bec2"}, + {file = "tiktoken-0.5.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0c964f554af1a96884e01188f480dad3fc224c4bbcf7af75d4b74c4b74ae0125"}, + {file = "tiktoken-0.5.2-cp38-cp38-win_amd64.whl", hash = "sha256:368dd5726d2e8788e47ea04f32e20f72a2012a8a67af5b0b003d1e059f1d30a3"}, + {file = "tiktoken-0.5.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a2deef9115b8cd55536c0a02c0203512f8deb2447f41585e6d929a0b878a0dd2"}, + {file = "tiktoken-0.5.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2ed7d380195affbf886e2f8b92b14edfe13f4768ff5fc8de315adba5b773815e"}, + {file = "tiktoken-0.5.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c76fce01309c8140ffe15eb34ded2bb94789614b7d1d09e206838fc173776a18"}, + {file = "tiktoken-0.5.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60a5654d6a2e2d152637dd9a880b4482267dfc8a86ccf3ab1cec31a8c76bfae8"}, + {file = "tiktoken-0.5.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:41d4d3228e051b779245a8ddd21d4336f8975563e92375662f42d05a19bdff41"}, + {file = "tiktoken-0.5.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a5c1cdec2c92fcde8c17a50814b525ae6a88e8e5b02030dc120b76e11db93f13"}, + {file = "tiktoken-0.5.2-cp39-cp39-win_amd64.whl", hash = "sha256:84ddb36faedb448a50b246e13d1b6ee3437f60b7169b723a4b2abad75e914f3e"}, + {file = "tiktoken-0.5.2.tar.gz", hash = "sha256:f54c581f134a8ea96ce2023ab221d4d4d81ab614efa0b2fbce926387deb56c80"}, +] + +[package.dependencies] +regex = ">=2022.1.18" +requests = ">=2.26.0" + +[package.extras] +blobfile = ["blobfile (>=2)"] + [[package]] name = "tomlkit" version = "0.12.3" @@ -1758,4 +1929,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = ">=3.8.1,<4.0" -content-hash = "3f7a7f25c788600b31875592b8eff1e6e61cf80fe7c2de4e72e95bbcf6e8059a" +content-hash = "3c41ca809b411be94a980dfc4aa899622efabf5956dfca5a5b6fe63ec0db6ed7" diff --git a/templates/retrieval-agent/pyproject.toml b/templates/retrieval-agent/pyproject.toml index bc0ccff045538..931f743b5d52f 100644 --- a/templates/retrieval-agent/pyproject.toml +++ b/templates/retrieval-agent/pyproject.toml @@ -10,9 +10,10 @@ python = ">=3.8.1,<4.0" langchain = "^0.1" openai = "<2" arxiv = "^2.0.0" +langchain-openai = "^0.0.2.post1" [tool.poetry.group.dev.dependencies] -langchain-cli = ">=0.0.4" +langchain-cli = ">=0.0.20" fastapi = "^0.104.0" sse-starlette = "^1.6.5" diff --git a/templates/retrieval-agent/retrieval_agent/chain.py b/templates/retrieval-agent/retrieval_agent/chain.py index 8b4d215373797..2f774f6bfe30a 100644 --- a/templates/retrieval-agent/retrieval_agent/chain.py +++ b/templates/retrieval-agent/retrieval_agent/chain.py @@ -7,12 +7,12 @@ from langchain.callbacks.manager import CallbackManagerForRetrieverRun from langchain.schema import BaseRetriever, Document from langchain.tools.retriever import create_retriever_tool -from langchain_community.chat_models import AzureChatOpenAI from langchain_community.tools.convert_to_openai import format_tool_to_openai_function from langchain_community.utilities.arxiv import ArxivAPIWrapper from langchain_core.messages import AIMessage, HumanMessage from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder from langchain_core.pydantic_v1 import BaseModel, Field +from langchain_openai import AzureChatOpenAI class ArxivRetriever(BaseRetriever, ArxivAPIWrapper): @@ -67,10 +67,9 @@ def _get_relevant_documents( tools = [arxiv_tool] llm = AzureChatOpenAI( temperature=0, - deployment_name=os.environ["AZURE_OPENAI_DEPLOYMENT_NAME"], - openai_api_base=os.environ["AZURE_OPENAI_API_BASE"], - openai_api_version=os.environ["AZURE_OPENAI_API_VERSION"], - openai_api_key=os.environ["AZURE_OPENAI_API_KEY"], + azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"), + api_key=os.getenv("AZURE_OPENAI_API_KEY"), + api_version=os.getenv("AZURE_OPENAI_API_VERSION"), ) assistant_system_message = """You are a helpful research assistant. \ Lookup relevant information as needed."""