mirror of
https://github.com/langchain-ai/langserve.git
synced 2026-07-25 13:15:44 -04:00
Compare commits
37 Commits
v0.1.0
..
v0.3.0dev2
| Author | SHA1 | Date | |
|---|---|---|---|
| 59b3c81189 | |||
| 36e9919c17 | |||
| ff94f96dc8 | |||
| 8c852935e5 | |||
| 04236b0cf2 | |||
| 54eee64faf | |||
| 72c200ff81 | |||
| 21c2e3da2a | |||
| 6e7a9ee3f5 | |||
| 81c0285af2 | |||
| b62b925825 | |||
| b528955b60 | |||
| 5aedbf7083 | |||
| 41a9d798aa | |||
| d4704c2b45 | |||
| c259ec3e4d | |||
| 62e648a2bf | |||
| a74e072486 | |||
| 1487bf1ce5 | |||
| 050a0cc674 | |||
| 3fc76eca05 | |||
| df3aa45ef8 | |||
| 577dcc779f | |||
| 1b389f0751 | |||
| 6ef3aca359 | |||
| 81a633e0f4 | |||
| bb74431183 | |||
| 4be04bda6d | |||
| bba7122986 | |||
| ee250d20f2 | |||
| 21bf92f80c | |||
| 37f41e54ce | |||
| 075bdd0cfc | |||
| 08d4bdd61a | |||
| 5fe83e33b8 | |||
| 153616bb99 | |||
| 109445b25e |
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"contributors": ["eyurtsev", "hwchase17", "nfcampos", "efriis", "jacoblee93", "dqbd", "kreneskyp", "adarsh-jha-dev", "harris", "baskaryan", "hinthornw", "bracesproul", "jakerachleff", "craigsdennis", "anhi", "169", "LarchLiu", "PaulLockett", "RCMatthias", "jwynia", "majiayu000", "mpskex", "shivachittamuru", "sinashaloudegi", "sowsan", "akira", "lucianotonet", "JGalego", "nat-n", "dirien", "donbr", "rahilvora", "WarrenTheRabbit", "StreetLamb"],
|
||||
"contributors": ["eyurtsev", "hwchase17", "nfcampos", "efriis", "jacoblee93", "dqbd", "kreneskyp", "adarsh-jha-dev", "harris", "baskaryan", "hinthornw", "bracesproul", "jakerachleff", "craigsdennis", "anhi", "169", "LarchLiu", "PaulLockett", "RCMatthias", "jwynia", "majiayu000", "mpskex", "shivachittamuru", "sinashaloudegi", "sowsan", "akira", "lucianotonet", "JGalego", "nat-n", "dirien", "donbr", "rahilvora", "WarrenTheRabbit", "StreetLamb", "ccurme", "dennisrall"],
|
||||
"message": "Thank you for your pull request and welcome to our community. We require contributors to sign our Contributor License Agreement, and we don't seem to have the username {{usersWithoutCLA}} on file. In order for us to review and merge your code, please complete the Individual Contributor License Agreement here https://forms.gle/AQFbtkWRoHXUgipM6 .\n\nThis process is done manually on our side, so after signing the form one of the maintainers will add you to the contributors list.\n\nFor more details about why we have a CLA and other contribution guidelines please see: https://github.com/langchain-ai/langserve/blob/main/CONTRIBUTING.md."
|
||||
}
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
name: pydantic v1/v2 compatibility
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
working-directory:
|
||||
required: true
|
||||
type: string
|
||||
description: "From which folder this pipeline executes"
|
||||
|
||||
env:
|
||||
POETRY_VERSION: "1.5.1"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
timeout-minutes: 10
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version:
|
||||
- "3.8"
|
||||
- "3.9"
|
||||
- "3.10"
|
||||
- "3.11"
|
||||
name: Pydantic v1/v2 compatibility - Python ${{ matrix.python-version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }} + Poetry ${{ env.POETRY_VERSION }}
|
||||
uses: "./.github/actions/poetry_setup"
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
poetry-version: ${{ env.POETRY_VERSION }}
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
cache-key: pydantic-cross-compat
|
||||
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: poetry install
|
||||
|
||||
- name: Install the opposite major version of pydantic
|
||||
# If normal tests use pydantic v1, here we'll use v2, and vice versa.
|
||||
shell: bash
|
||||
run: |
|
||||
# Determine the major part of pydantic version
|
||||
REGULAR_VERSION=$(poetry run python -c "import pydantic; print(pydantic.__version__)" | cut -d. -f1)
|
||||
|
||||
if [[ "$REGULAR_VERSION" == "1" ]]; then
|
||||
PYDANTIC_DEP=">=2.1,<3"
|
||||
TEST_WITH_VERSION="2"
|
||||
elif [[ "$REGULAR_VERSION" == "2" ]]; then
|
||||
PYDANTIC_DEP="<2"
|
||||
TEST_WITH_VERSION="1"
|
||||
else
|
||||
echo "Unexpected pydantic major version '$REGULAR_VERSION', cannot determine which version to use for cross-compatibility test."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Install via `pip` instead of `poetry add` to avoid changing lockfile,
|
||||
# which would prevent caching from working: the cache would get saved
|
||||
# to a different key than where it gets loaded from.
|
||||
poetry run pip install "pydantic${PYDANTIC_DEP}"
|
||||
|
||||
# Ensure that the correct pydantic is installed now.
|
||||
echo "Checking pydantic version... Expecting ${TEST_WITH_VERSION}"
|
||||
|
||||
# Determine the major part of pydantic version
|
||||
CURRENT_VERSION=$(poetry run python -c "import pydantic; print(pydantic.__version__)" | cut -d. -f1)
|
||||
|
||||
# Check that the major part of pydantic version is as expected, if not
|
||||
# raise an error
|
||||
if [[ "$CURRENT_VERSION" != "$TEST_WITH_VERSION" ]]; then
|
||||
echo "Error: expected pydantic version ${CURRENT_VERSION} to have been installed, but found: ${TEST_WITH_VERSION}"
|
||||
exit 1
|
||||
fi
|
||||
echo "Found pydantic version ${CURRENT_VERSION}, as expected"
|
||||
- name: Run pydantic compatibility tests
|
||||
shell: bash
|
||||
run: make test
|
||||
|
||||
- name: Ensure the tests did not create any additional files
|
||||
shell: bash
|
||||
run: |
|
||||
set -eu
|
||||
|
||||
STATUS="$(git status)"
|
||||
echo "$STATUS"
|
||||
|
||||
# grep will exit non-zero if the target message isn't found,
|
||||
# and `set -e` above will cause the step to fail.
|
||||
echo "$STATUS" | grep 'nothing to commit, working tree clean'
|
||||
@@ -39,13 +39,6 @@ jobs:
|
||||
with:
|
||||
working-directory: .
|
||||
secrets: inherit
|
||||
|
||||
pydantic-compatibility:
|
||||
uses:
|
||||
./.github/workflows/_pydantic_compatibility.yml
|
||||
with:
|
||||
working-directory: .
|
||||
secrets: inherit
|
||||
test:
|
||||
timeout-minutes: 10
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -10,4 +10,5 @@ jobs:
|
||||
./.github/workflows/_release.yml
|
||||
with:
|
||||
working-directory: .
|
||||
permissions: write-all
|
||||
secrets: inherit
|
||||
|
||||
@@ -5,10 +5,6 @@
|
||||
[](https://github.com/langchain-ai/langserve/issues)
|
||||
[](https://discord.com/channels/1038097195422978059/1170024642245832774)
|
||||
|
||||
🚩 We will be releasing a hosted version of LangServe for one-click deployments of
|
||||
LangChain applications. [Sign up here](https://airtable.com/apppQ9p5XuujRl3wJ/shrABpHWdxry8Bacm)
|
||||
to get on the waitlist.
|
||||
|
||||
## Overview
|
||||
|
||||
[LangServe](https://github.com/langchain-ai/langserve) helps developers
|
||||
@@ -28,11 +24,11 @@ in [LangChain.js](https://js.langchain.com/docs/ecosystem/langserve).
|
||||
- Input and Output schemas automatically inferred from your LangChain object, and
|
||||
enforced on every API call, with rich error messages
|
||||
- API docs page with JSONSchema and Swagger (insert example link)
|
||||
- Efficient `/invoke/`, `/batch/` and `/stream/` endpoints with support for many
|
||||
- Efficient `/invoke`, `/batch` and `/stream` endpoints with support for many
|
||||
concurrent requests on a single server
|
||||
- `/stream_log/` endpoint for streaming all (or some) intermediate steps from your
|
||||
- `/stream_log` endpoint for streaming all (or some) intermediate steps from your
|
||||
chain/agent
|
||||
- **new** as of 0.0.40, supports `astream_events` to make it easier to stream without needing to parse the output of `stream_log`.
|
||||
- **new** as of 0.0.40, supports `/stream_events` to make it easier to stream without needing to parse the output of `/stream_log`.
|
||||
- Playground page at `/playground/` with streaming output and intermediate steps
|
||||
- Built-in (optional) tracing to [LangSmith](https://www.langchain.com/langsmith), just
|
||||
add your API key (see [Instructions](https://docs.smith.langchain.com/))
|
||||
@@ -42,6 +38,13 @@ in [LangChain.js](https://js.langchain.com/docs/ecosystem/langserve).
|
||||
locally (or call the HTTP API directly)
|
||||
- [LangServe Hub](https://github.com/langchain-ai/langchain/blob/master/templates/README.md)
|
||||
|
||||
## ⚠️ LangGraph Compatibility
|
||||
|
||||
LangServe is designed to primarily deploy simple Runnables and wok with well-known primitives in langchain-core.
|
||||
|
||||
If you need a deployment option for LangGraph, you should instead be looking at [LangGraph Cloud (beta)](https://langchain-ai.github.io/langgraph/cloud/) which will
|
||||
be better suited for deploying LangGraph applications.
|
||||
|
||||
## Limitations
|
||||
|
||||
- Client callbacks are not yet supported for events that originate on the server
|
||||
@@ -49,16 +52,9 @@ in [LangChain.js](https://js.langchain.com/docs/ecosystem/langserve).
|
||||
support [mixing pydantic v1 and v2 namespaces](https://github.com/tiangolo/fastapi/issues/10360).
|
||||
See section below for more details.
|
||||
|
||||
## Hosted LangServe
|
||||
|
||||
We will be releasing a hosted version of LangServe for one-click deployments of
|
||||
LangChain
|
||||
applications. [Sign up here](https://airtable.com/apppQ9p5XuujRl3wJ/shrABpHWdxry8Bacm)
|
||||
to get on the waitlist.
|
||||
|
||||
## Security
|
||||
|
||||
* Vulnerability in Versions 0.0.13 - 0.0.15 -- playground endpoint allows accessing
|
||||
- Vulnerability in Versions 0.0.13 - 0.0.15 -- playground endpoint allows accessing
|
||||
arbitrary files on
|
||||
server. [Resolved in 0.0.16](https://github.com/langchain-ai/langserve/pull/98).
|
||||
|
||||
@@ -79,35 +75,41 @@ Use the `LangChain` CLI to bootstrap a `LangServe` project quickly.
|
||||
|
||||
To use the langchain CLI make sure that you have a recent version of `langchain-cli`
|
||||
installed. You can install it with `pip install -U langchain-cli`.
|
||||
|
||||
## Setup
|
||||
|
||||
**Note**: We use `poetry` for dependency management. Please follow poetry [doc](https://python-poetry.org/docs/) to learn more about it.
|
||||
|
||||
### 1. Create new app using langchain cli command
|
||||
|
||||
```sh
|
||||
langchain app new my-app
|
||||
```
|
||||
|
||||
### 2. Define the runnable in add_routes. Go to server.py and edit
|
||||
|
||||
```sh
|
||||
add_routes(app. NotImplemented)
|
||||
```
|
||||
|
||||
### 3. Use `poetry` to add 3rd party packages (e.g., langchain-openai, langchain-anthropic, langchain-mistral etc).
|
||||
|
||||
```sh
|
||||
poetry add [package-name] // e.g `poetry add langchain-openai`
|
||||
```
|
||||
|
||||
### 4. Set up relevant env variables. For example,
|
||||
|
||||
```sh
|
||||
export OPENAI_API_KEY="sk-..."
|
||||
```
|
||||
|
||||
### 5. Serve your app
|
||||
|
||||
```sh
|
||||
poetry run langchain serve --port=8100
|
||||
```
|
||||
|
||||
|
||||
## Examples
|
||||
|
||||
Get your LangServe instance started quickly with
|
||||
@@ -119,24 +121,24 @@ or the [examples](https://github.com/langchain-ai/langserve/tree/main/examples)
|
||||
directory.
|
||||
|
||||
| Description | Links |
|
||||
|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **LLMs** Minimal example that reserves OpenAI and Anthropic chat models. Uses async, supports batching and streaming. | [server](https://github.com/langchain-ai/langserve/tree/main/examples/llm/server.py), [client](https://github.com/langchain-ai/langserve/blob/main/examples/llm/client.ipynb) |
|
||||
| **Retriever** Simple server that exposes a retriever as a runnable. | [server](https://github.com/langchain-ai/langserve/tree/main/examples/retrieval/server.py), [client](https://github.com/langchain-ai/langserve/tree/main/examples/retrieval/client.ipynb) |
|
||||
| **Conversational Retriever** A [Conversational Retriever](https://python.langchain.com/docs/expression_language/cookbook/retrieval#conversational-retrieval-chain) exposed via LangServe | [server](https://github.com/langchain-ai/langserve/tree/main/examples/conversational_retrieval_chain/server.py), [client](https://github.com/langchain-ai/langserve/tree/main/examples/conversational_retrieval_chain/client.ipynb) |
|
||||
| **Agent** without **conversation history** based on [OpenAI tools](https://python.langchain.com/docs/modules/agents/agent_types/openai_functions_agent) | [server](https://github.com/langchain-ai/langserve/tree/main/examples/agent/server.py), [client](https://github.com/langchain-ai/langserve/tree/main/examples/agent/client.ipynb) |
|
||||
| **Agent** with **conversation history** based on [OpenAI tools](https://python.langchain.com/docs/modules/agents/agent_types/openai_functions_agent) | [server](https://github.com/langchain-ai/langserve/blob/main/examples/agent_with_history/server.py), [client](https://github.com/langchain-ai/langserve/blob/main/examples/agent_with_history/client.ipynb) |
|
||||
| :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **LLMs** Minimal example that reserves OpenAI and Anthropic chat models. Uses async, supports batching and streaming. | [server](https://github.com/langchain-ai/langserve/tree/main/examples/llm/server.py), [client](https://github.com/langchain-ai/langserve/blob/main/examples/llm/client.ipynb) |
|
||||
| **Retriever** Simple server that exposes a retriever as a runnable. | [server](https://github.com/langchain-ai/langserve/tree/main/examples/retrieval/server.py), [client](https://github.com/langchain-ai/langserve/tree/main/examples/retrieval/client.ipynb) |
|
||||
| **Conversational Retriever** A [Conversational Retriever](https://python.langchain.com/docs/expression_language/cookbook/retrieval#conversational-retrieval-chain) exposed via LangServe | [server](https://github.com/langchain-ai/langserve/tree/main/examples/conversational_retrieval_chain/server.py), [client](https://github.com/langchain-ai/langserve/tree/main/examples/conversational_retrieval_chain/client.ipynb) |
|
||||
| **Agent** without **conversation history** based on [OpenAI tools](https://python.langchain.com/docs/modules/agents/agent_types/openai_functions_agent) | [server](https://github.com/langchain-ai/langserve/tree/main/examples/agent/server.py), [client](https://github.com/langchain-ai/langserve/tree/main/examples/agent/client.ipynb) |
|
||||
| **Agent** with **conversation history** based on [OpenAI tools](https://python.langchain.com/docs/modules/agents/agent_types/openai_functions_agent) | [server](https://github.com/langchain-ai/langserve/blob/main/examples/agent_with_history/server.py), [client](https://github.com/langchain-ai/langserve/blob/main/examples/agent_with_history/client.ipynb) |
|
||||
| [RunnableWithMessageHistory](https://python.langchain.com/docs/expression_language/how_to/message_history) to implement chat persisted on backend, keyed off a `session_id` supplied by client. | [server](https://github.com/langchain-ai/langserve/tree/main/examples/chat_with_persistence/server.py), [client](https://github.com/langchain-ai/langserve/tree/main/examples/chat_with_persistence/client.ipynb) |
|
||||
| [RunnableWithMessageHistory](https://python.langchain.com/docs/expression_language/how_to/message_history) to implement chat persisted on backend, keyed off a `conversation_id` supplied by client, and `user_id` (see Auth for implementing `user_id` properly). | [server](https://github.com/langchain-ai/langserve/tree/main/examples/chat_with_persistence_and_user/server.py), [client](https://github.com/langchain-ai/langserve/tree/main/examples/chat_with_persistence_and_user/client.ipynb) |
|
||||
| [Configurable Runnable](https://python.langchain.com/docs/expression_language/how_to/configure) to create a retriever that supports run time configuration of the index name. | [server](https://github.com/langchain-ai/langserve/tree/main/examples/configurable_retrieval/server.py), [client](https://github.com/langchain-ai/langserve/tree/main/examples/configurable_retrieval/client.ipynb) |
|
||||
| [Configurable Runnable](https://python.langchain.com/docs/expression_language/how_to/configure) that shows configurable fields and configurable alternatives. | [server](https://github.com/langchain-ai/langserve/tree/main/examples/configurable_chain/server.py), [client](https://github.com/langchain-ai/langserve/tree/main/examples/configurable_chain/client.ipynb) |
|
||||
| [RunnableWithMessageHistory](https://python.langchain.com/docs/expression_language/how_to/message_history) to implement chat persisted on backend, keyed off a `conversation_id` supplied by client, and `user_id` (see Auth for implementing `user_id` properly). | [server](https://github.com/langchain-ai/langserve/tree/main/examples/chat_with_persistence_and_user/server.py), [client](https://github.com/langchain-ai/langserve/tree/main/examples/chat_with_persistence_and_user/client.ipynb) |
|
||||
| [Configurable Runnable](https://python.langchain.com/docs/expression_language/how_to/configure) to create a retriever that supports run time configuration of the index name. | [server](https://github.com/langchain-ai/langserve/tree/main/examples/configurable_retrieval/server.py), [client](https://github.com/langchain-ai/langserve/tree/main/examples/configurable_retrieval/client.ipynb) |
|
||||
| [Configurable Runnable](https://python.langchain.com/docs/expression_language/how_to/configure) that shows configurable fields and configurable alternatives. | [server](https://github.com/langchain-ai/langserve/tree/main/examples/configurable_chain/server.py), [client](https://github.com/langchain-ai/langserve/tree/main/examples/configurable_chain/client.ipynb) |
|
||||
| **APIHandler** Shows how to use `APIHandler` instead of `add_routes`. This provides more flexibility for developers to define endpoints. Works well with all FastAPI patterns, but takes a bit more effort. | [server](https://github.com/langchain-ai/langserve/tree/main/examples/api_handler_examples/server.py) |
|
||||
| **LCEL Example** Example that uses LCEL to manipulate a dictionary input. | [server](https://github.com/langchain-ai/langserve/tree/main/examples/passthrough_dict/server.py), [client](https://github.com/langchain-ai/langserve/tree/main/examples/passthrough_dict/client.ipynb) |
|
||||
| **Auth** with `add_routes`: Simple authentication that can be applied across all endpoints associated with app. (Not useful on its own for implementing per user logic.) | [server](https://github.com/langchain-ai/langserve/tree/main/examples/auth/global_deps/server.py) |
|
||||
| **Auth** with `add_routes`: Simple authentication mechanism based on path dependencies. (No useful on its own for implementing per user logic.) | [server](https://github.com/langchain-ai/langserve/tree/main/examples/auth/path_dependencies/server.py) |
|
||||
| **Auth** with `add_routes`: Implement per user logic and auth for endpoints that use per request config modifier. (**Note**: At the moment, does not integrate with OpenAPI docs.) | [server](https://github.com/langchain-ai/langserve/tree/main/examples/auth/per_req_config_modifier/server.py), [client](https://github.com/langchain-ai/langserve/tree/main/examples/auth/per_req_config_modifier/client.ipynb) |
|
||||
| **Auth** with `APIHandler`: Implement per user logic and auth that shows how to search only within user owned documents. | [server](https://github.com/langchain-ai/langserve/tree/main/examples/auth/api_handler/server.py), [client](https://github.com/langchain-ai/langserve/tree/main/examples/auth/api_handler/client.ipynb) |
|
||||
| **Widgets** Different widgets that can be used with playground (file upload and chat) | [server](https://github.com/langchain-ai/langserve/tree/main/examples/widgets/chat/tuples/server.py) |
|
||||
| **Widgets** File upload widget used for LangServe playground. | [server](https://github.com/langchain-ai/langserve/tree/main/examples/file_processing/server.py), [client](https://github.com/langchain-ai/langserve/tree/main/examples/file_processing/client.ipynb) |
|
||||
| **LCEL Example** Example that uses LCEL to manipulate a dictionary input. | [server](https://github.com/langchain-ai/langserve/tree/main/examples/passthrough_dict/server.py), [client](https://github.com/langchain-ai/langserve/tree/main/examples/passthrough_dict/client.ipynb) |
|
||||
| **Auth** with `add_routes`: Simple authentication that can be applied across all endpoints associated with app. (Not useful on its own for implementing per user logic.) | [server](https://github.com/langchain-ai/langserve/tree/main/examples/auth/global_deps/server.py) |
|
||||
| **Auth** with `add_routes`: Simple authentication mechanism based on path dependencies. (No useful on its own for implementing per user logic.) | [server](https://github.com/langchain-ai/langserve/tree/main/examples/auth/path_dependencies/server.py) |
|
||||
| **Auth** with `add_routes`: Implement per user logic and auth for endpoints that use per request config modifier. (**Note**: At the moment, does not integrate with OpenAPI docs.) | [server](https://github.com/langchain-ai/langserve/tree/main/examples/auth/per_req_config_modifier/server.py), [client](https://github.com/langchain-ai/langserve/tree/main/examples/auth/per_req_config_modifier/client.ipynb) |
|
||||
| **Auth** with `APIHandler`: Implement per user logic and auth that shows how to search only within user owned documents. | [server](https://github.com/langchain-ai/langserve/tree/main/examples/auth/api_handler/server.py), [client](https://github.com/langchain-ai/langserve/tree/main/examples/auth/api_handler/client.ipynb) |
|
||||
| **Widgets** Different widgets that can be used with playground (file upload and chat) | [server](https://github.com/langchain-ai/langserve/tree/main/examples/widgets/chat/tuples/server.py) |
|
||||
| **Widgets** File upload widget used for LangServe playground. | [server](https://github.com/langchain-ai/langserve/tree/main/examples/file_processing/server.py), [client](https://github.com/langchain-ai/langserve/tree/main/examples/file_processing/client.ipynb) |
|
||||
|
||||
## Sample Application
|
||||
|
||||
@@ -161,17 +163,17 @@ app = FastAPI(
|
||||
|
||||
add_routes(
|
||||
app,
|
||||
ChatOpenAI(),
|
||||
ChatOpenAI(model="gpt-3.5-turbo-0125"),
|
||||
path="/openai",
|
||||
)
|
||||
|
||||
add_routes(
|
||||
app,
|
||||
ChatAnthropic(),
|
||||
ChatAnthropic(model="claude-3-haiku-20240307"),
|
||||
path="/anthropic",
|
||||
)
|
||||
|
||||
model = ChatAnthropic()
|
||||
model = ChatAnthropic(model="claude-3-haiku-20240307")
|
||||
prompt = ChatPromptTemplate.from_template("tell me a joke about {topic}")
|
||||
add_routes(
|
||||
app,
|
||||
@@ -206,8 +208,8 @@ app.add_middleware(
|
||||
|
||||
If you've deployed the server above, you can view the generated OpenAPI docs using:
|
||||
|
||||
> ⚠️ If using pydantic v2, docs will not be generated for *invoke*, *batch*, *stream*,
|
||||
*stream_log*. See [Pydantic](#pydantic) section below for more details.
|
||||
> ⚠️ If using pydantic v2, docs will not be generated for _invoke_, _batch_, _stream_,
|
||||
> _stream_log_. See [Pydantic](#pydantic) section below for more details.
|
||||
|
||||
```sh
|
||||
curl localhost:8000/docs
|
||||
@@ -267,10 +269,10 @@ In TypeScript (requires LangChain.js version 0.0.166 or later):
|
||||
import { RemoteRunnable } from "@langchain/core/runnables/remote";
|
||||
|
||||
const chain = new RemoteRunnable({
|
||||
url: `http://localhost:8000/joke/`,
|
||||
url: `http://localhost:8000/joke/`,
|
||||
});
|
||||
const result = await chain.invoke({
|
||||
topic: "cats",
|
||||
topic: "cats",
|
||||
});
|
||||
```
|
||||
|
||||
@@ -319,7 +321,7 @@ adds of these endpoints to the server:
|
||||
- `POST /my_runnable/stream_log` - invoke on a single input and stream the output,
|
||||
including output of intermediate steps as it's generated
|
||||
- `POST /my_runnable/astream_events` - invoke on a single input and stream events as they are generated,
|
||||
including from intermediate steps.
|
||||
including from intermediate steps.
|
||||
- `GET /my_runnable/input_schema` - json schema for input to the runnable
|
||||
- `GET /my_runnable/output_schema` - json schema for output of the runnable
|
||||
- `GET /my_runnable/config_schema` - json schema for config of the runnable
|
||||
@@ -378,7 +380,7 @@ prompt = ChatPromptTemplate.from_messages(
|
||||
]
|
||||
)
|
||||
|
||||
chain = prompt | ChatAnthropic(model="claude-2")
|
||||
chain = prompt | ChatAnthropic(model="claude-2.1")
|
||||
|
||||
|
||||
class InputChat(BaseModel):
|
||||
@@ -460,27 +462,6 @@ You can deploy to GCP Cloud Run using the following command:
|
||||
gcloud run deploy [your-service-name] --source . --port 8001 --allow-unauthenticated --region us-central1 --set-env-vars=OPENAI_API_KEY=your_key
|
||||
```
|
||||
|
||||
### Deploy using Infrastructure as Code
|
||||
|
||||
#### Pulumi
|
||||
|
||||
You can deploy your LangServe server with [Pulumi](https://www.pulumi.com/) using your preferred general purpose language. Below are some quickstart
|
||||
examples for deploying LangServe to different cloud providers.
|
||||
|
||||
These examples are a good starting point for your own infrastructure as code (IaC) projects. You can easily modify them to suit your needs.
|
||||
|
||||
|
||||
| Cloud | Language | Repository | Quickstart |
|
||||
|-------|------------|-----------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| AWS | dotnet | https://github.com/pulumi/examples/aws-cs-langserve | [](https://app.pulumi.com/new?template=https://github.com/pulumi/examples/aws-cs-langserve) |
|
||||
| AWS | golang | https://github.com/pulumi/examples/aws-go-langserve | [](https://app.pulumi.com/new?template=https://github.com/pulumi/examples/aws-go-langserve) |
|
||||
| AWS | python | https://github.com/pulumi/examples/aws-py-langserve | [](https://app.pulumi.com/new?template=https://github.com/pulumi/examples/aws-py-langserve) |
|
||||
| AWS | typescript | https://github.com/pulumi/examples/aws-ts-langserve | [](https://app.pulumi.com/new?template=https://github.com/pulumi/examples/aws-ts-langserve) |
|
||||
| AWS | javascript | https://github.com/pulumi/examples/aws-js-langserve | [](https://app.pulumi.com/new?template=https://github.com/pulumi/examples/aws-js-langserve) |
|
||||
|
||||
|
||||
|
||||
|
||||
### Community Contributed
|
||||
|
||||
#### Deploy to Railway
|
||||
@@ -494,7 +475,7 @@ These examples are a good starting point for your own infrastructure as code (Ia
|
||||
LangServe provides support for Pydantic 2 with some limitations.
|
||||
|
||||
1. OpenAPI docs will not be generated for invoke/batch/stream/stream_log when using
|
||||
Pydantic V2. Fast API does not support [mixing pydantic v1 and v2 namespaces].
|
||||
Pydantic V2. Fast API does not support [mixing pydantic v1 and v2 namespaces]. To fix this, use `pip install pydantic==1.10.17`.
|
||||
2. LangChain uses the v1 namespace in Pydantic v2. Please read
|
||||
the [following guidelines to ensure compatibility with LangChain](https://github.com/langchain-ai/langchain/discussions/9337)
|
||||
|
||||
@@ -511,7 +492,7 @@ and [security](https://fastapi.tiangolo.com/tutorial/security/).
|
||||
|
||||
The below examples show how to wire up authentication logic LangServe endpoints using FastAPI primitives.
|
||||
|
||||
You are responsible for providing the actual authentication logic, the users table etc.
|
||||
You are responsible for providing the actual authentication logic, the users table etc.
|
||||
|
||||
If you're not sure what you're doing, you could try using an existing solution [Auth0](https://auth0.com/).
|
||||
|
||||
@@ -520,11 +501,11 @@ If you're not sure what you're doing, you could try using an existing solution [
|
||||
If you're using `add_routes`, see
|
||||
examples [here](https://github.com/langchain-ai/langserve/tree/main/examples/auth).
|
||||
|
||||
| Description | Links |
|
||||
|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **Auth** with `add_routes`: Simple authentication that can be applied across all endpoints associated with app. (Not useful on its own for implementing per user logic.) | [server](https://github.com/langchain-ai/langserve/tree/main/examples/auth/global_deps/server.py) |
|
||||
| **Auth** with `add_routes`: Simple authentication mechanism based on path dependencies. (No useful on its own for implementing per user logic.) | [server](https://github.com/langchain-ai/langserve/tree/main/examples/auth/path_dependencies/server.py) |
|
||||
| **Auth** with `add_routes`: Implement per user logic and auth for endpoints that use per request config modifier. (**Note**: At the moment, does not integrate with OpenAPI docs.) | [server](https://github.com/langchain-ai/langserve/tree/main/examples/auth/per_req_config_modifier/server.py), [client](https://github.com/langchain-ai/langserve/tree/main/examples/auth/per_req_config_modifier/client.ipynb) |
|
||||
| Description | Links |
|
||||
| :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Auth** with `add_routes`: Simple authentication that can be applied across all endpoints associated with app. (Not useful on its own for implementing per user logic.) | [server](https://github.com/langchain-ai/langserve/tree/main/examples/auth/global_deps/server.py) |
|
||||
| **Auth** with `add_routes`: Simple authentication mechanism based on path dependencies. (No useful on its own for implementing per user logic.) | [server](https://github.com/langchain-ai/langserve/tree/main/examples/auth/path_dependencies/server.py) |
|
||||
| **Auth** with `add_routes`: Implement per user logic and auth for endpoints that use per request config modifier. (**Note**: At the moment, does not integrate with OpenAPI docs.) | [server](https://github.com/langchain-ai/langserve/tree/main/examples/auth/per_req_config_modifier/server.py), [client](https://github.com/langchain-ai/langserve/tree/main/examples/auth/per_req_config_modifier/client.ipynb) |
|
||||
|
||||
Alternatively, you can use FastAPI's [middleware](https://fastapi.tiangolo.com/tutorial/middleware/).
|
||||
|
||||
@@ -544,10 +525,10 @@ authorization purposes.
|
||||
|
||||
If you feel comfortable with FastAPI and python, you can use LangServe's [APIHandler](https://github.com/langchain-ai/langserve/blob/main/examples/api_handler_examples/server.py).
|
||||
|
||||
| Description | Links |
|
||||
|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **Auth** with `APIHandler`: Implement per user logic and auth that shows how to search only within user owned documents. | [server](https://github.com/langchain-ai/langserve/tree/main/examples/auth/api_handler/server.py), [client](https://github.com/langchain-ai/langserve/tree/main/examples/auth/api_handler/client.ipynb) |
|
||||
| **APIHandler** Shows how to use `APIHandler` instead of `add_routes`. This provides more flexibility for developers to define endpoints. Works well with all FastAPI patterns, but takes a bit more effort. | [server](https://github.com/langchain-ai/langserve/tree/main/examples/api_handler_examples/server.py), [client](https://github.com/langchain-ai/langserve/tree/main/examples/api_handler_examples/client.ipynb) |
|
||||
| Description | Links |
|
||||
| :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Auth** with `APIHandler`: Implement per user logic and auth that shows how to search only within user owned documents. | [server](https://github.com/langchain-ai/langserve/tree/main/examples/auth/api_handler/server.py), [client](https://github.com/langchain-ai/langserve/tree/main/examples/auth/api_handler/client.ipynb) |
|
||||
| **APIHandler** Shows how to use `APIHandler` instead of `add_routes`. This provides more flexibility for developers to define endpoints. Works well with all FastAPI patterns, but takes a bit more effort. | [server](https://github.com/langchain-ai/langserve/tree/main/examples/api_handler_examples/server.py), [client](https://github.com/langchain-ai/langserve/tree/main/examples/api_handler_examples/client.ipynb) |
|
||||
|
||||
It's a bit more work, but gives you complete control over the endpoint definitions, so
|
||||
you can do whatever custom logic you need for auth.
|
||||
@@ -616,8 +597,8 @@ add_routes(app, runnable)
|
||||
Inherit from `CustomUserType` if you want the data to de-serialize into a
|
||||
pydantic model rather than the equivalent dict representation.
|
||||
|
||||
At the moment, this type only works *server* side and is used
|
||||
to specify desired *decoding* behavior. If inheriting from this type
|
||||
At the moment, this type only works _server_ side and is used
|
||||
to specify desired _decoding_ behavior. If inheriting from this type
|
||||
the server will keep the decoded type as a pydantic model instead
|
||||
of converting it into a dict.
|
||||
|
||||
@@ -656,10 +637,10 @@ The playground allows you to define custom widgets for your runnable from the ba
|
||||
|
||||
Here are a few examples:
|
||||
|
||||
| Description | Links |
|
||||
|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **Widgets** Different widgets that can be used with playground (file upload and chat) | [server](https://github.com/langchain-ai/langserve/tree/main/examples/widgets/chat/tuples/server.py), [client](https://github.com/langchain-ai/langserve/tree/main/examples/widgets/client.ipynb) |
|
||||
| **Widgets** File upload widget used for LangServe playground. | [server](https://github.com/langchain-ai/langserve/tree/main/examples/file_processing/server.py), [client](https://github.com/langchain-ai/langserve/tree/main/examples/file_processing/client.ipynb) |
|
||||
| Description | Links |
|
||||
| :------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Widgets** Different widgets that can be used with playground (file upload and chat) | [server](https://github.com/langchain-ai/langserve/tree/main/examples/widgets/chat/tuples/server.py), [client](https://github.com/langchain-ai/langserve/tree/main/examples/widgets/client.ipynb) |
|
||||
| **Widgets** File upload widget used for LangServe playground. | [server](https://github.com/langchain-ai/langserve/tree/main/examples/file_processing/server.py), [client](https://github.com/langchain-ai/langserve/tree/main/examples/file_processing/client.ipynb) |
|
||||
|
||||
#### Schema
|
||||
|
||||
@@ -675,8 +656,8 @@ type NameSpacedPath = { title: string; path: JsonPath }; // Using title to mimic
|
||||
type OneOfPath = { oneOf: JsonPath[] };
|
||||
|
||||
type Widget = {
|
||||
type: string // Some well known type (e.g., base64file, chat etc.)
|
||||
[key: string]: JsonPath | NameSpacedPath | OneOfPath;
|
||||
type: string; // Some well known type (e.g., base64file, chat etc.)
|
||||
[key: string]: JsonPath | NameSpacedPath | OneOfPath;
|
||||
};
|
||||
```
|
||||
|
||||
@@ -734,9 +715,9 @@ at the [widget example](https://github.com/langchain-ai/langserve/tree/main/exam
|
||||
|
||||
To define a chat widget, make sure that you pass "type": "chat".
|
||||
|
||||
* "input" is JSONPath to the field in the *Request* that has the new input message.
|
||||
* "output" is JSONPath to the field in the *Response* that has new output message(s).
|
||||
* Don't specify these fields if the entire input or output should be used as they are (
|
||||
- "input" is JSONPath to the field in the _Request_ that has the new input message.
|
||||
- "output" is JSONPath to the field in the _Response_ that has new output message(s).
|
||||
- Don't specify these fields if the entire input or output should be used as they are (
|
||||
e.g., if the output is a list of chat messages.)
|
||||
|
||||
Here's a snippet:
|
||||
@@ -776,6 +757,7 @@ add_routes(
|
||||
```
|
||||
|
||||
Example widget:
|
||||
|
||||
<p align="center">
|
||||
<img src="https://github.com/langchain-ai/langserve/assets/3205522/a71ff37b-a6a9-4857-a376-cf27c41d3ca4" width="50%"/>
|
||||
</p>
|
||||
@@ -790,7 +772,7 @@ prompt = ChatPromptTemplate.from_messages(
|
||||
]
|
||||
)
|
||||
|
||||
chain = prompt | ChatAnthropic(model="claude-2")
|
||||
chain = prompt | ChatAnthropic(model="claude-2.1")
|
||||
|
||||
|
||||
class MessageListInput(BaseModel):
|
||||
|
||||
@@ -20,15 +20,15 @@ Relevant LangChain documentation:
|
||||
from typing import Any
|
||||
|
||||
from fastapi import FastAPI
|
||||
from langchain.agents import AgentExecutor, tool
|
||||
from langchain.agents import AgentExecutor
|
||||
from langchain.agents.format_scratchpad import format_to_openai_functions
|
||||
from langchain.agents.output_parsers import OpenAIFunctionsAgentOutputParser
|
||||
from langchain.chat_models import ChatOpenAI
|
||||
from langchain.embeddings import OpenAIEmbeddings
|
||||
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||
from langchain.pydantic_v1 import BaseModel
|
||||
from langchain.tools.render import format_tool_to_openai_function
|
||||
from langchain.vectorstores import FAISS
|
||||
from langchain_community.vectorstores import FAISS
|
||||
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||
from langchain_core.tools import tool
|
||||
from langchain_core.utils.function_calling import format_tool_to_openai_function
|
||||
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
|
||||
from pydantic import BaseModel
|
||||
|
||||
from langserve import add_routes
|
||||
|
||||
|
||||
@@ -47,19 +47,19 @@ Relevant LangChain documentation:
|
||||
from typing import Any, AsyncIterator, List, Literal
|
||||
|
||||
from fastapi import FastAPI
|
||||
from langchain.agents import AgentExecutor, tool
|
||||
from langchain.agents import AgentExecutor
|
||||
from langchain.agents.format_scratchpad.openai_tools import (
|
||||
format_to_openai_tool_messages,
|
||||
)
|
||||
from langchain.agents.output_parsers.openai_tools import OpenAIToolsAgentOutputParser
|
||||
from langchain.prompts import MessagesPlaceholder
|
||||
from langchain_community.tools.convert_to_openai import format_tool_to_openai_tool
|
||||
from langchain_core.prompts import ChatPromptTemplate
|
||||
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||
from langchain_core.runnables import RunnableLambda
|
||||
from langchain_core.tools import tool
|
||||
from langchain_core.utils.function_calling import format_tool_to_openai_tool
|
||||
from langchain_openai import ChatOpenAI
|
||||
from pydantic import BaseModel
|
||||
|
||||
from langserve import add_routes
|
||||
from langserve.pydantic_v1 import BaseModel
|
||||
|
||||
prompt = ChatPromptTemplate.from_messages(
|
||||
[
|
||||
|
||||
@@ -26,19 +26,19 @@ Relevant LangChain documentation:
|
||||
from typing import Any, List, Union
|
||||
|
||||
from fastapi import FastAPI
|
||||
from langchain.agents import AgentExecutor, tool
|
||||
from langchain.agents import AgentExecutor
|
||||
from langchain.agents.format_scratchpad.openai_tools import (
|
||||
format_to_openai_tool_messages,
|
||||
)
|
||||
from langchain.agents.output_parsers.openai_tools import OpenAIToolsAgentOutputParser
|
||||
from langchain.prompts import MessagesPlaceholder
|
||||
from langchain_community.tools.convert_to_openai import format_tool_to_openai_tool
|
||||
from langchain_core.messages import AIMessage, FunctionMessage, HumanMessage
|
||||
from langchain_core.prompts import ChatPromptTemplate
|
||||
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||
from langchain_core.tools import tool
|
||||
from langchain_core.utils.function_calling import format_tool_to_openai_tool
|
||||
from langchain_openai import ChatOpenAI
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from langserve import add_routes
|
||||
from langserve.pydantic_v1 import BaseModel, Field
|
||||
|
||||
prompt = ChatPromptTemplate.from_messages(
|
||||
[
|
||||
|
||||
@@ -35,7 +35,6 @@ from typing import Any, List, Optional, Union
|
||||
|
||||
from fastapi import Depends, FastAPI, HTTPException, Request, Response, status
|
||||
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
||||
from langchain_community.embeddings.openai import OpenAIEmbeddings
|
||||
from langchain_community.vectorstores.chroma import Chroma
|
||||
from langchain_core.documents import Document
|
||||
from langchain_core.runnables import (
|
||||
@@ -44,10 +43,11 @@ from langchain_core.runnables import (
|
||||
RunnableSerializable,
|
||||
)
|
||||
from langchain_core.vectorstores import VectorStore
|
||||
from langchain_openai import OpenAIEmbeddings
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from typing_extensions import Annotated
|
||||
|
||||
from langserve import APIHandler
|
||||
from langserve.pydantic_v1 import BaseModel
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
@@ -150,10 +150,9 @@ class PerUserVectorstore(RunnableSerializable):
|
||||
user_id: Optional[str]
|
||||
vectorstore: VectorStore
|
||||
|
||||
class Config:
|
||||
# Allow arbitrary types since VectorStore is an abstract interface
|
||||
# and not a pydantic model
|
||||
arbitrary_types_allowed = True
|
||||
model_config = ConfigDict(
|
||||
arbitrary_types_allowed=True,
|
||||
)
|
||||
|
||||
def _invoke(
|
||||
self, input: str, config: Optional[RunnableConfig] = None, **kwargs: Any
|
||||
|
||||
@@ -36,7 +36,6 @@ from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
from fastapi import Depends, FastAPI, HTTPException, Request, status
|
||||
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
||||
from langchain_community.embeddings.openai import OpenAIEmbeddings
|
||||
from langchain_community.vectorstores.chroma import Chroma
|
||||
from langchain_core.documents import Document
|
||||
from langchain_core.runnables import (
|
||||
@@ -45,10 +44,11 @@ from langchain_core.runnables import (
|
||||
RunnableSerializable,
|
||||
)
|
||||
from langchain_core.vectorstores import VectorStore
|
||||
from langchain_openai import OpenAIEmbeddings
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from typing_extensions import Annotated
|
||||
|
||||
from langserve import add_routes
|
||||
from langserve.pydantic_v1 import BaseModel
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
@@ -147,10 +147,9 @@ class PerUserVectorstore(RunnableSerializable):
|
||||
user_id: Optional[str]
|
||||
vectorstore: VectorStore
|
||||
|
||||
class Config:
|
||||
# Allow arbitrary types since VectorStore is an abstract interface
|
||||
# and not a pydantic model
|
||||
arbitrary_types_allowed = True
|
||||
model_config = ConfigDict(
|
||||
arbitrary_types_allowed=True,
|
||||
)
|
||||
|
||||
def _invoke(
|
||||
self, input: str, config: Optional[RunnableConfig] = None, **kwargs: Any
|
||||
|
||||
@@ -5,12 +5,12 @@ state back and forth between server and client.
|
||||
from typing import List, Union
|
||||
|
||||
from fastapi import FastAPI
|
||||
from langchain.chat_models import ChatAnthropic
|
||||
from langchain_anthropic import ChatAnthropic
|
||||
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
|
||||
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from langserve import add_routes
|
||||
from langserve.pydantic_v1 import BaseModel, Field
|
||||
|
||||
app = FastAPI(
|
||||
title="LangChain Server",
|
||||
@@ -28,7 +28,7 @@ prompt = ChatPromptTemplate.from_messages(
|
||||
]
|
||||
)
|
||||
|
||||
chain = prompt | ChatAnthropic(model="claude-2")
|
||||
chain = prompt | ChatAnthropic(model="claude-2.1")
|
||||
|
||||
|
||||
class InputChat(BaseModel):
|
||||
|
||||
@@ -8,9 +8,9 @@ from fastapi import FastAPI
|
||||
from langchain_anthropic.chat_models import ChatAnthropic
|
||||
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
|
||||
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from langserve import add_routes
|
||||
from langserve.pydantic_v1 import BaseModel, Field
|
||||
|
||||
app = FastAPI(
|
||||
title="LangChain Server",
|
||||
|
||||
@@ -13,14 +13,14 @@ from pathlib import Path
|
||||
from typing import Callable, Union
|
||||
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from langchain.chat_models import ChatAnthropic
|
||||
from langchain.memory import FileChatMessageHistory
|
||||
from langchain_anthropic import ChatAnthropic
|
||||
from langchain_community.chat_message_histories import FileChatMessageHistory
|
||||
from langchain_core.chat_history import BaseChatMessageHistory
|
||||
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||
from langchain_core.runnables.history import RunnableWithMessageHistory
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from langserve import add_routes
|
||||
from langserve.pydantic_v1 import BaseModel, Field
|
||||
|
||||
|
||||
def _is_valid_identifier(value: str) -> bool:
|
||||
@@ -76,7 +76,7 @@ prompt = ChatPromptTemplate.from_messages(
|
||||
]
|
||||
)
|
||||
|
||||
chain = prompt | ChatAnthropic(model="claude-2")
|
||||
chain = prompt | ChatAnthropic(model="claude-2.1")
|
||||
|
||||
|
||||
class InputChat(BaseModel):
|
||||
|
||||
@@ -13,13 +13,13 @@ from pathlib import Path
|
||||
from typing import Any, Callable, Dict, Union
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Request
|
||||
from langchain.chat_models import ChatOpenAI
|
||||
from langchain.memory import FileChatMessageHistory
|
||||
from langchain.schema.runnable.utils import ConfigurableFieldSpec
|
||||
from langchain_community.chat_message_histories import FileChatMessageHistory
|
||||
from langchain_core import __version__
|
||||
from langchain_core.chat_history import BaseChatMessageHistory
|
||||
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||
from langchain_core.runnables import ConfigurableFieldSpec
|
||||
from langchain_core.runnables.history import RunnableWithMessageHistory
|
||||
from langchain_openai import ChatOpenAI
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langserve import add_routes
|
||||
|
||||
@@ -17,15 +17,11 @@ on the LLM and use the stream_log endpoint rather than stream endpoint.
|
||||
from typing import Any, AsyncIterator, Dict, List, Optional, cast
|
||||
|
||||
from fastapi import FastAPI
|
||||
from langchain.agents import AgentExecutor, tool
|
||||
from langchain.agents import AgentExecutor
|
||||
from langchain.agents.format_scratchpad import format_to_openai_functions
|
||||
from langchain.agents.output_parsers import OpenAIFunctionsAgentOutputParser
|
||||
from langchain.chat_models import ChatOpenAI
|
||||
from langchain.embeddings import OpenAIEmbeddings
|
||||
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||
from langchain.pydantic_v1 import BaseModel
|
||||
from langchain.tools.render import format_tool_to_openai_function
|
||||
from langchain.vectorstores import FAISS
|
||||
from langchain_community.vectorstores import FAISS
|
||||
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||
from langchain_core.runnables import (
|
||||
ConfigurableField,
|
||||
ConfigurableFieldSpec,
|
||||
@@ -33,6 +29,10 @@ from langchain_core.runnables import (
|
||||
RunnableConfig,
|
||||
)
|
||||
from langchain_core.runnables.utils import Input, Output
|
||||
from langchain_core.tools import tool
|
||||
from langchain_core.utils.function_calling import format_tool_to_openai_function
|
||||
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
|
||||
from pydantic import BaseModel
|
||||
|
||||
from langserve import add_routes
|
||||
|
||||
|
||||
@@ -10,10 +10,10 @@ from typing import Any, Dict
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from langchain.chat_models import ChatOpenAI
|
||||
from langchain.prompts import PromptTemplate
|
||||
from langchain.schema.output_parser import StrOutputParser
|
||||
from langchain.schema.runnable import ConfigurableField
|
||||
from langchain_core.output_parsers import StrOutputParser
|
||||
from langchain_core.prompts import PromptTemplate
|
||||
from langchain_core.runnables import ConfigurableField
|
||||
from langchain_openai import ChatOpenAI
|
||||
|
||||
from langserve import add_routes
|
||||
|
||||
|
||||
@@ -3,20 +3,21 @@
|
||||
from typing import Any, Iterable, List, Optional, Type
|
||||
|
||||
from fastapi import FastAPI
|
||||
from langchain.embeddings import OpenAIEmbeddings
|
||||
from langchain.schema import Document
|
||||
from langchain.schema.embeddings import Embeddings
|
||||
from langchain.schema.retriever import BaseRetriever
|
||||
from langchain.schema.runnable import (
|
||||
from langchain.schema.vectorstore import VST
|
||||
from langchain_community.vectorstores import FAISS
|
||||
from langchain_core.documents import Document
|
||||
from langchain_core.embeddings import Embeddings
|
||||
from langchain_core.retrievers import BaseRetriever
|
||||
from langchain_core.runnables import (
|
||||
ConfigurableFieldSingleOption,
|
||||
RunnableConfig,
|
||||
RunnableSerializable,
|
||||
)
|
||||
from langchain.schema.vectorstore import VST
|
||||
from langchain.vectorstores import FAISS, VectorStore
|
||||
from langchain_core.vectorstores import VectorStore
|
||||
from langchain_openai import OpenAIEmbeddings
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from langserve import add_routes
|
||||
from langserve.pydantic_v1 import BaseModel, Field
|
||||
|
||||
vectorstore1 = FAISS.from_texts(
|
||||
["cats like fish", "dogs like sticks"], embedding=OpenAIEmbeddings()
|
||||
|
||||
@@ -13,17 +13,14 @@ from operator import itemgetter
|
||||
from typing import List, Tuple
|
||||
|
||||
from fastapi import FastAPI
|
||||
from langchain.chat_models import ChatOpenAI
|
||||
from langchain.embeddings import OpenAIEmbeddings
|
||||
from langchain.prompts import ChatPromptTemplate
|
||||
from langchain.prompts.prompt import PromptTemplate
|
||||
from langchain.schema import format_document
|
||||
from langchain.schema.output_parser import StrOutputParser
|
||||
from langchain.schema.runnable import RunnableMap, RunnablePassthrough
|
||||
from langchain.vectorstores import FAISS
|
||||
from langchain_community.vectorstores import FAISS
|
||||
from langchain_core.output_parsers import StrOutputParser
|
||||
from langchain_core.prompts import ChatPromptTemplate, PromptTemplate, format_document
|
||||
from langchain_core.runnables import RunnableMap, RunnablePassthrough
|
||||
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from langserve import add_routes
|
||||
from langserve.pydantic_v1 import BaseModel, Field
|
||||
|
||||
_TEMPLATE = """Given the following conversation and a follow up question, rephrase the
|
||||
follow up question to be a standalone question, in its original language.
|
||||
|
||||
@@ -15,10 +15,10 @@ allowing one to upload a binary file using the langserve playground UI.
|
||||
import base64
|
||||
|
||||
from fastapi import FastAPI
|
||||
from langchain.document_loaders.blob_loaders import Blob
|
||||
from langchain.document_loaders.parsers.pdf import PDFMinerParser
|
||||
from langchain.pydantic_v1 import Field
|
||||
from langchain.schema.runnable import RunnableLambda
|
||||
from langchain_community.document_loaders.parsers.pdf import PDFMinerParser
|
||||
from langchain_core.document_loaders import Blob
|
||||
from langchain_core.runnables import RunnableLambda
|
||||
from pydantic import Field
|
||||
|
||||
from langserve import CustomUserType, add_routes
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
"""Example LangChain server exposes multiple runnables (LLMs in this case)."""
|
||||
|
||||
from fastapi import FastAPI
|
||||
from langchain.chat_models import ChatAnthropic, ChatOpenAI
|
||||
from langchain_anthropic import ChatAnthropic
|
||||
from langchain_openai import ChatOpenAI
|
||||
|
||||
from langserve import add_routes
|
||||
|
||||
@@ -14,12 +15,12 @@ app = FastAPI(
|
||||
|
||||
add_routes(
|
||||
app,
|
||||
ChatOpenAI(),
|
||||
ChatOpenAI(model="gpt-3.5-turbo-0125"),
|
||||
path="/openai",
|
||||
)
|
||||
add_routes(
|
||||
app,
|
||||
ChatAnthropic(),
|
||||
ChatAnthropic(model="claude-3-haiku-20240307"),
|
||||
path="/anthropic",
|
||||
)
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
from typing import Any, Callable, Dict, List, Optional, TypedDict
|
||||
|
||||
from fastapi import FastAPI
|
||||
from langchain.chat_models import ChatOpenAI
|
||||
from langchain.prompts import ChatPromptTemplate
|
||||
from langchain.schema.runnable import RunnableMap, RunnablePassthrough
|
||||
from langchain_core.prompts import ChatPromptTemplate
|
||||
from langchain_core.runnables import RunnableParallel, RunnablePassthrough
|
||||
from langchain_openai import ChatOpenAI
|
||||
|
||||
from langserve import add_routes
|
||||
|
||||
@@ -43,7 +43,7 @@ model = ChatOpenAI()
|
||||
|
||||
underlying_chain = prompt | model
|
||||
|
||||
wrapped_chain = RunnableMap(
|
||||
wrapped_chain = RunnableParallel(
|
||||
{
|
||||
"output": _create_projection(exclude_keys=["info"]) | underlying_chain,
|
||||
"info": _create_projection(include_keys=["info"]),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env python
|
||||
"""Example LangChain server exposes a retriever."""
|
||||
from fastapi import FastAPI
|
||||
from langchain.embeddings import OpenAIEmbeddings
|
||||
from langchain.vectorstores import FAISS
|
||||
from langchain_community.vectorstores import FAISS
|
||||
from langchain_openai import OpenAIEmbeddings
|
||||
|
||||
from langserve import add_routes
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@ See more documentation at:
|
||||
https://fastapi.tiangolo.com/tutorial/bigger-applications/
|
||||
"""
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from langchain.chat_models import ChatAnthropic, ChatOpenAI
|
||||
from langchain_anthropic import ChatAnthropic
|
||||
from langchain_openai import ChatOpenAI
|
||||
|
||||
from langserve import add_routes
|
||||
|
||||
@@ -20,13 +21,13 @@ router = APIRouter(prefix="/models")
|
||||
# Invocations to this router will appear in trace logs as /models/openai
|
||||
add_routes(
|
||||
router,
|
||||
ChatOpenAI(),
|
||||
ChatOpenAI(model="gpt-3.5-turbo-0125"),
|
||||
path="/openai",
|
||||
)
|
||||
# Invocations to this router will appear in trace logs as /models/anthropic
|
||||
add_routes(
|
||||
router,
|
||||
ChatAnthropic(),
|
||||
ChatAnthropic(model="claude-3-haiku-20240307"),
|
||||
path="/anthropic",
|
||||
)
|
||||
|
||||
|
||||
@@ -6,13 +6,13 @@ from typing import List, Union
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from langchain.chat_models import ChatAnthropic
|
||||
from langchain_anthropic import ChatAnthropic
|
||||
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
|
||||
from langchain_core.output_parsers import StrOutputParser
|
||||
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from langserve import add_routes
|
||||
from langserve.pydantic_v1 import BaseModel, Field
|
||||
|
||||
app = FastAPI(
|
||||
title="LangChain Server",
|
||||
@@ -40,7 +40,7 @@ prompt = ChatPromptTemplate.from_messages(
|
||||
]
|
||||
)
|
||||
|
||||
chain = prompt | ChatAnthropic(model="claude-2") | StrOutputParser()
|
||||
chain = prompt | ChatAnthropic(model="claude-2.1") | StrOutputParser()
|
||||
|
||||
|
||||
class InputChat(BaseModel):
|
||||
|
||||
@@ -6,18 +6,17 @@ from typing import Any, Dict, List, Tuple
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from langchain.chat_models.openai import ChatOpenAI
|
||||
from langchain.document_loaders.blob_loaders import Blob
|
||||
from langchain.document_loaders.parsers.pdf import PDFMinerParser
|
||||
from langchain.pydantic_v1 import BaseModel, Field
|
||||
from langchain.schema.messages import (
|
||||
from langchain_community.document_loaders.parsers.pdf import PDFMinerParser
|
||||
from langchain_core.document_loaders import Blob
|
||||
from langchain_core.messages import (
|
||||
AIMessage,
|
||||
BaseMessage,
|
||||
FunctionMessage,
|
||||
HumanMessage,
|
||||
)
|
||||
from langchain.schema.runnable import RunnableLambda
|
||||
from langchain_core.runnables import RunnableParallel
|
||||
from langchain_core.runnables import RunnableLambda, RunnableParallel
|
||||
from langchain_openai import ChatOpenAI
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from langserve import CustomUserType
|
||||
from langserve.server import add_routes
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
from typing import Any, Dict, Type, cast
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, RootModel
|
||||
from pydantic.json_schema import (
|
||||
DEFAULT_REF_TEMPLATE,
|
||||
GenerateJsonSchema,
|
||||
JsonSchemaMode,
|
||||
)
|
||||
|
||||
|
||||
def _create_root_model(name: str, type_: Any) -> Type[RootModel]:
|
||||
"""Create a base class."""
|
||||
|
||||
def schema(
|
||||
cls: Type[BaseModel],
|
||||
by_alias: bool = True,
|
||||
ref_template: str = DEFAULT_REF_TEMPLATE,
|
||||
) -> Dict[str, Any]:
|
||||
# Complains about schema not being defined in superclass
|
||||
schema_ = super(cls, cls).schema( # type: ignore[misc]
|
||||
by_alias=by_alias, ref_template=ref_template
|
||||
)
|
||||
schema_["title"] = name
|
||||
return schema_
|
||||
|
||||
def model_json_schema(
|
||||
cls: Type[BaseModel],
|
||||
by_alias: bool = True,
|
||||
ref_template: str = DEFAULT_REF_TEMPLATE,
|
||||
schema_generator: type[GenerateJsonSchema] = GenerateJsonSchema,
|
||||
mode: JsonSchemaMode = "validation",
|
||||
) -> Dict[str, Any]:
|
||||
# Complains about model_json_schema not being defined in superclass
|
||||
schema_ = super(cls, cls).model_json_schema( # type: ignore[misc]
|
||||
by_alias=by_alias,
|
||||
ref_template=ref_template,
|
||||
schema_generator=schema_generator,
|
||||
mode=mode,
|
||||
)
|
||||
schema_["title"] = name
|
||||
return schema_
|
||||
|
||||
base_class_attributes = {
|
||||
"__annotations__": {"root": type_},
|
||||
"model_config": ConfigDict(arbitrary_types_allowed=True),
|
||||
"schema": classmethod(schema),
|
||||
"model_json_schema": classmethod(model_json_schema),
|
||||
# Should replace __module__ with caller based on stack frame.
|
||||
"__module__": "langserve._pydantic",
|
||||
}
|
||||
|
||||
custom_root_type = type(name, (RootModel,), base_class_attributes)
|
||||
return cast(Type[RootModel], custom_root_type)
|
||||
+56
-48
@@ -29,6 +29,7 @@ from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from langchain_core._api.beta_decorator import warn_beta
|
||||
from langchain_core.callbacks.base import AsyncCallbackHandler
|
||||
from langchain_core.callbacks.manager import BaseCallbackManager
|
||||
from langchain_core.load.serializable import Serializable
|
||||
from langchain_core.runnables import Runnable, RunnableConfig
|
||||
from langchain_core.runnables.config import (
|
||||
@@ -40,14 +41,15 @@ from langchain_core.tracers import RunLogPatch
|
||||
from langsmith import client as ls_client
|
||||
from langsmith.schemas import FeedbackIngestToken
|
||||
from langsmith.utils import tracing_is_enabled
|
||||
from pydantic import BaseModel, Field, RootModel, ValidationError, create_model
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import JSONResponse, Response
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langserve._pydantic import _create_root_model
|
||||
from langserve.callbacks import AsyncEventAggregatorCallback, CallbackEventDict
|
||||
from langserve.lzstring import LZString
|
||||
from langserve.playground import serve_playground
|
||||
from langserve.pydantic_v1 import BaseModel, Field, ValidationError, create_model
|
||||
from langserve.schema import (
|
||||
BatchResponseMetadata,
|
||||
CustomUserType,
|
||||
@@ -184,11 +186,11 @@ async def _unpack_request_config(
|
||||
config_dicts = []
|
||||
for config in client_sent_configs:
|
||||
if isinstance(config, str):
|
||||
config_dicts.append(model(**_config_from_hash(config)).dict())
|
||||
config_dicts.append(model(**_config_from_hash(config)).model_dump())
|
||||
elif isinstance(config, BaseModel):
|
||||
config_dicts.append(config.dict())
|
||||
config_dicts.append(config.model_dump())
|
||||
elif isinstance(config, Mapping):
|
||||
config_dicts.append(model(**config).dict())
|
||||
config_dicts.append(model(**config).model_dump())
|
||||
else:
|
||||
raise TypeError(f"Expected a string, dict or BaseModel got {type(config)}")
|
||||
config = merge_configs(*config_dicts)
|
||||
@@ -255,10 +257,12 @@ def _update_config_with_defaults(
|
||||
}
|
||||
metadata.update(hosted_metadata)
|
||||
|
||||
non_overridable_default_config = RunnableConfig(
|
||||
run_name=run_name,
|
||||
metadata=metadata,
|
||||
)
|
||||
non_overridable_default_config: RunnableConfig = {
|
||||
"metadata": metadata,
|
||||
}
|
||||
|
||||
if run_name:
|
||||
non_overridable_default_config["run_name"] = run_name
|
||||
|
||||
# merge_configs is last-writer-wins, so we specifically pass in the
|
||||
# overridable configs first, then the user provided configs, then
|
||||
@@ -279,8 +283,8 @@ def _update_config_with_defaults(
|
||||
|
||||
def _unpack_input(validated_model: BaseModel) -> Any:
|
||||
"""Unpack the decoded input from the validated model."""
|
||||
if hasattr(validated_model, "__root__"):
|
||||
model = validated_model.__root__
|
||||
if isinstance(validated_model, RootModel):
|
||||
model = validated_model.root
|
||||
else:
|
||||
model = validated_model
|
||||
|
||||
@@ -294,7 +298,7 @@ def _unpack_input(validated_model: BaseModel) -> Any:
|
||||
# This logic should be applied recursively to nested models.
|
||||
return {
|
||||
fieldname: _unpack_input(getattr(model, fieldname))
|
||||
for fieldname in model.__fields__.keys()
|
||||
for fieldname in model.model_fields.keys()
|
||||
}
|
||||
|
||||
return model
|
||||
@@ -304,7 +308,7 @@ def _rename_pydantic_model(model: Type[BaseModel], prefix: str) -> Type[BaseMode
|
||||
"""Rename the given pydantic model to the given name."""
|
||||
return create_model(
|
||||
prefix + model.__name__,
|
||||
__config__=model.__config__,
|
||||
__config__=model.model_config,
|
||||
**{
|
||||
fieldname: (
|
||||
_rename_pydantic_model(field.annotation, prefix)
|
||||
@@ -313,10 +317,10 @@ def _rename_pydantic_model(model: Type[BaseModel], prefix: str) -> Type[BaseMode
|
||||
Field(
|
||||
field.default,
|
||||
title=fieldname,
|
||||
description=field.field_info.description,
|
||||
description=field.description,
|
||||
),
|
||||
)
|
||||
for fieldname, field in model.__fields__.items()
|
||||
for fieldname, field in model.model_fields.items()
|
||||
},
|
||||
)
|
||||
|
||||
@@ -326,6 +330,11 @@ def _replace_non_alphanumeric_with_underscores(s: str) -> str:
|
||||
return re.sub(r"[^a-zA-Z0-9]", "_", s)
|
||||
|
||||
|
||||
def _schema_json(model: Type[BaseModel]) -> str:
|
||||
"""Return the JSON representation of the model schema."""
|
||||
return json.dumps(model.model_json_schema(), sort_keys=True, indent=False)
|
||||
|
||||
|
||||
def _resolve_model(
|
||||
type_: Union[Type, BaseModel], default_name: str, namespace: str
|
||||
) -> Type[BaseModel]:
|
||||
@@ -333,15 +342,15 @@ def _resolve_model(
|
||||
if isclass(type_) and issubclass(type_, BaseModel):
|
||||
model = type_
|
||||
else:
|
||||
model = create_model(default_name, __root__=(type_, ...))
|
||||
model = _create_root_model(default_name, type_)
|
||||
|
||||
hash_ = model.schema_json()
|
||||
hash_ = _schema_json(model)
|
||||
|
||||
if model.__name__ in _SEEN_NAMES and hash_ not in _MODEL_REGISTRY:
|
||||
# If the model name has been seen before, but the model itself is different
|
||||
# generate a new name for the model.
|
||||
model_to_use = _rename_pydantic_model(model, namespace)
|
||||
hash_ = model_to_use.schema_json()
|
||||
hash_ = _schema_json(model_to_use)
|
||||
else:
|
||||
model_to_use = model
|
||||
|
||||
@@ -366,11 +375,7 @@ def _add_namespace_to_model(namespace: str, model: Type[BaseModel]) -> Type[Base
|
||||
A new model with name prepended with the given namespace.
|
||||
"""
|
||||
model_with_unique_name = _rename_pydantic_model(model, namespace)
|
||||
if "run_id" in model_with_unique_name.__annotations__:
|
||||
# Help resolve reference by providing namespace references
|
||||
model_with_unique_name.update_forward_refs(uuid=uuid)
|
||||
else:
|
||||
model_with_unique_name.update_forward_refs()
|
||||
model_with_unique_name.model_rebuild()
|
||||
return model_with_unique_name
|
||||
|
||||
|
||||
@@ -403,7 +408,7 @@ def _with_validation_error_translation() -> Generator[None, None, None]:
|
||||
try:
|
||||
yield
|
||||
except ValidationError as e:
|
||||
raise RequestValidationError(e.errors(), body=e.model)
|
||||
raise RequestValidationError(e.errors())
|
||||
|
||||
|
||||
def _json_encode_response(model: BaseModel) -> JSONResponse:
|
||||
@@ -423,27 +428,27 @@ def _json_encode_response(model: BaseModel) -> JSONResponse:
|
||||
|
||||
if isinstance(model, InvokeBaseResponse):
|
||||
# Invoke Response
|
||||
# Collapse '__root__' from output field if it exists. This is done
|
||||
# Collapse 'root' from output field if it exists. This is done
|
||||
# automatically by fastapi when annotating request and response with
|
||||
# We need to do this manually since we're using vanilla JSONResponse
|
||||
if isinstance(obj["output"], dict) and "__root__" in obj["output"]:
|
||||
obj["output"] = obj["output"]["__root__"]
|
||||
if isinstance(obj["output"], dict) and "root" in obj["output"]:
|
||||
obj["output"] = obj["output"]["root"]
|
||||
|
||||
if "callback_events" in obj:
|
||||
for idx, callback_event in enumerate(obj["callback_events"]):
|
||||
if isinstance(callback_event, dict) and "__root__" in callback_event:
|
||||
obj["callback_events"][idx] = callback_event["__root__"]
|
||||
if isinstance(callback_event, dict) and "root" in callback_event:
|
||||
obj["callback_events"][idx] = callback_event["root"]
|
||||
elif isinstance(model, BatchBaseResponse):
|
||||
if not isinstance(obj["output"], list):
|
||||
raise AssertionError("Expected output to be a list")
|
||||
|
||||
# Collapse '__root__' from output field if it exists. This is done
|
||||
# Collapse 'root' from output field if it exists. This is done
|
||||
# automatically by fastapi when annotating request and response with
|
||||
# We need to do this manually since we're using vanilla JSONResponse
|
||||
outputs = obj["output"]
|
||||
for idx, output in enumerate(outputs):
|
||||
if isinstance(output, dict) and "__root__" in output:
|
||||
outputs[idx] = output["__root__"]
|
||||
if isinstance(output, dict) and "root" in output:
|
||||
outputs[idx] = output["root"]
|
||||
|
||||
if "callback_events" in obj:
|
||||
if not isinstance(obj["callback_events"], list):
|
||||
@@ -451,11 +456,8 @@ def _json_encode_response(model: BaseModel) -> JSONResponse:
|
||||
|
||||
for callback_events in obj["callback_events"]:
|
||||
for idx, callback_event in enumerate(callback_events):
|
||||
if (
|
||||
isinstance(callback_event, dict)
|
||||
and "__root__" in callback_event
|
||||
):
|
||||
callback_events[idx] = callback_event["__root__"]
|
||||
if isinstance(callback_event, dict) and "root" in callback_event:
|
||||
callback_events[idx] = callback_event["root"]
|
||||
else:
|
||||
raise AssertionError(
|
||||
f"Expected a InvokeBaseResponse or BatchBaseResponse got: {type(model)}"
|
||||
@@ -470,7 +472,12 @@ def _add_callbacks(
|
||||
"""Add the callback aggregator to the config."""
|
||||
if "callbacks" not in config:
|
||||
config["callbacks"] = []
|
||||
config["callbacks"].extend(callbacks)
|
||||
if "callbacks" in config:
|
||||
if isinstance(config["callbacks"], list):
|
||||
config["callbacks"].extend(callbacks)
|
||||
elif isinstance(config["callbacks"], BaseCallbackManager):
|
||||
for callback in callbacks:
|
||||
config["callbacks"].add_handler(callback, inherit=True)
|
||||
|
||||
|
||||
_MODEL_REGISTRY = {}
|
||||
@@ -753,7 +760,7 @@ class APIHandler:
|
||||
except json.JSONDecodeError:
|
||||
raise RequestValidationError(errors=["Invalid JSON body"])
|
||||
try:
|
||||
body = InvokeRequestShallowValidator.validate(body)
|
||||
body = InvokeRequestShallowValidator.model_validate(body)
|
||||
|
||||
# Merge the config from the path with the config from the body.
|
||||
user_provided_config = await _unpack_request_config(
|
||||
@@ -775,7 +782,7 @@ class APIHandler:
|
||||
# This takes into account changes in the input type when
|
||||
# using configuration.
|
||||
schema = self._runnable.with_config(config).input_schema
|
||||
input_ = schema.validate(body.input)
|
||||
input_ = schema.model_validate(body.input)
|
||||
return config, _unpack_input(input_)
|
||||
except ValidationError as e:
|
||||
raise RequestValidationError(e.errors(), body=body)
|
||||
@@ -852,7 +859,7 @@ class APIHandler:
|
||||
feedback_tokens=[
|
||||
FeedbackToken(
|
||||
key=feedback_key,
|
||||
url=feedback_token.url,
|
||||
token_url=feedback_token.url,
|
||||
expires_at=feedback_token.expires_at.isoformat(),
|
||||
)
|
||||
]
|
||||
@@ -885,7 +892,7 @@ class APIHandler:
|
||||
raise RequestValidationError(errors=["Invalid JSON body"])
|
||||
|
||||
with _with_validation_error_translation():
|
||||
body = BatchRequestShallowValidator.validate(body)
|
||||
body = BatchRequestShallowValidator.model_validate(body)
|
||||
config = body.config
|
||||
|
||||
# First unpack the config
|
||||
@@ -936,7 +943,7 @@ class APIHandler:
|
||||
|
||||
inputs = [
|
||||
_unpack_input(
|
||||
self._runnable.with_config(config_).input_schema.validate(input_)
|
||||
self._runnable.with_config(config_).input_schema.model_validate(input_)
|
||||
)
|
||||
for config_, input_ in zip(configs_, inputs_)
|
||||
]
|
||||
@@ -999,7 +1006,7 @@ class APIHandler:
|
||||
feedback_tokens=[
|
||||
FeedbackToken(
|
||||
key=feedback_key,
|
||||
url=feedback_token.url,
|
||||
token_url=feedback_token.url,
|
||||
expires_at=feedback_token.expires_at.isoformat(),
|
||||
)
|
||||
],
|
||||
@@ -1245,7 +1252,7 @@ class APIHandler:
|
||||
}
|
||||
|
||||
# Send a metadata event as soon as possible
|
||||
if not has_sent_metadata and self._enable_feedback_endpoint:
|
||||
if not has_sent_metadata and self._token_feedback_enabled:
|
||||
if task is None:
|
||||
raise AssertionError("Feedback token task was not created.")
|
||||
if not task.done():
|
||||
@@ -1354,7 +1361,7 @@ class APIHandler:
|
||||
}
|
||||
|
||||
# Send a metadata event as soon as possible
|
||||
if not has_sent_metadata and self._enable_feedback_endpoint:
|
||||
if not has_sent_metadata and self._token_feedback_enabled:
|
||||
if task is None:
|
||||
raise AssertionError("Feedback token task was not created.")
|
||||
if not task.done():
|
||||
@@ -1405,7 +1412,7 @@ class APIHandler:
|
||||
self._run_name, user_provided_config, request
|
||||
)
|
||||
|
||||
return self._runnable.get_input_schema(config).schema()
|
||||
return self._runnable.get_input_schema(config).model_json_schema()
|
||||
|
||||
async def output_schema(
|
||||
self,
|
||||
@@ -1432,7 +1439,7 @@ class APIHandler:
|
||||
config = _update_config_with_defaults(
|
||||
self._run_name, user_provided_config, request
|
||||
)
|
||||
return self._runnable.get_output_schema(config).schema()
|
||||
return self._runnable.get_output_schema(config).model_json_schema()
|
||||
|
||||
async def config_schema(
|
||||
self,
|
||||
@@ -1462,7 +1469,7 @@ class APIHandler:
|
||||
return (
|
||||
self._runnable.with_config(config)
|
||||
.config_schema(include=self._config_keys)
|
||||
.schema()
|
||||
.model_json_schema()
|
||||
)
|
||||
|
||||
async def playground(
|
||||
@@ -1590,6 +1597,7 @@ class APIHandler:
|
||||
score=create_request.score,
|
||||
value=create_request.value,
|
||||
comment=create_request.comment,
|
||||
correction=create_request.correction,
|
||||
metadata=metadata,
|
||||
)
|
||||
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -5,7 +5,7 @@
|
||||
<link rel="icon" href="/____LANGSERVE_BASE_URL/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Chat Playground</title>
|
||||
<script type="module" crossorigin src="/____LANGSERVE_BASE_URL/assets/index-86d4d9c0.js"></script>
|
||||
<script type="module" crossorigin src="/____LANGSERVE_BASE_URL/assets/index-53ad47d4.js"></script>
|
||||
<link rel="stylesheet" href="/____LANGSERVE_BASE_URL/assets/index-434ff580.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -30,6 +30,7 @@ export function App() {
|
||||
);
|
||||
const outputSchemaSupported = (
|
||||
outputDataSchema?.anyOf?.find((option) => option.properties?.type?.enum?.includes("ai")) ||
|
||||
outputDataSchema?.oneOf?.find((option) => option.properties?.type?.enum?.includes("ai")) ||
|
||||
outputDataSchema?.type === "string"
|
||||
);
|
||||
const isSupported = isLoading || (inputSchemaSupported && outputSchemaSupported);
|
||||
|
||||
+6
-2
@@ -431,11 +431,15 @@ class RemoteRunnable(Runnable[Input, Output]):
|
||||
self,
|
||||
inputs: List[Input],
|
||||
config: Optional[RunnableConfig] = None,
|
||||
*,
|
||||
return_exceptions: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> List[Output]:
|
||||
if kwargs:
|
||||
raise NotImplementedError("kwargs not implemented yet.")
|
||||
return self._batch_with_config(self._batch, inputs, config)
|
||||
raise NotImplementedError(f"kwargs not implemented yet. Got {kwargs}")
|
||||
return self._batch_with_config(
|
||||
self._batch, inputs, config, return_exceptions=return_exceptions
|
||||
)
|
||||
|
||||
async def _abatch(
|
||||
self,
|
||||
|
||||
@@ -6,8 +6,7 @@ from typing import Literal, Sequence, Type
|
||||
|
||||
from fastapi.responses import Response
|
||||
from langchain_core.runnables import Runnable
|
||||
|
||||
from langserve.pydantic_v1 import BaseModel
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class PlaygroundTemplate(Template):
|
||||
@@ -90,10 +89,12 @@ async def serve_playground(
|
||||
if base_url.startswith("/")
|
||||
else base_url,
|
||||
LANGSERVE_CONFIG_SCHEMA=json.dumps(
|
||||
runnable.config_schema(include=config_keys).schema()
|
||||
runnable.config_schema(include=config_keys).model_json_schema()
|
||||
),
|
||||
LANGSERVE_INPUT_SCHEMA=json.dumps(input_schema.model_json_schema()),
|
||||
LANGSERVE_OUTPUT_SCHEMA=json.dumps(
|
||||
output_schema.model_json_schema()
|
||||
),
|
||||
LANGSERVE_INPUT_SCHEMA=json.dumps(input_schema.schema()),
|
||||
LANGSERVE_OUTPUT_SCHEMA=json.dumps(output_schema.schema()),
|
||||
LANGSERVE_FEEDBACK_ENABLED=json.dumps(
|
||||
"true" if feedback_enabled else "false"
|
||||
),
|
||||
|
||||
+47
-47
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -5,7 +5,7 @@
|
||||
<link rel="icon" href="/____LANGSERVE_BASE_URL/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Playground</title>
|
||||
<script type="module" crossorigin src="/____LANGSERVE_BASE_URL/assets/index-dbc96538.js"></script>
|
||||
<script type="module" crossorigin src="/____LANGSERVE_BASE_URL/assets/index-400979f0.js"></script>
|
||||
<link rel="stylesheet" href="/____LANGSERVE_BASE_URL/assets/index-52e8ab2f.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -6,12 +6,30 @@ import {
|
||||
schemaMatches,
|
||||
Paths,
|
||||
isControl,
|
||||
JsonSchema,
|
||||
} from "@jsonforms/core";
|
||||
import { useStreamCallback } from "../useStreamCallback";
|
||||
import { isJsonSchemaExtra } from "../utils/schema";
|
||||
import { MessageFields, ChatMessageInput } from "./ChatMessageInput";
|
||||
import { useEffect } from "react";
|
||||
|
||||
function checkItemSchema(schema: JsonSchema) {
|
||||
const isObjectMessage =
|
||||
schema.type === "object" &&
|
||||
(schema.title?.endsWith("Message") ||
|
||||
schema.title?.endsWith("MessageChunk"));
|
||||
|
||||
const isTupleMessage =
|
||||
schema.type === "array" &&
|
||||
schema.minItems === 2 &&
|
||||
schema.maxItems === 2 &&
|
||||
Array.isArray(schema.items) &&
|
||||
schema.items.length === 2 &&
|
||||
schema.items.every((schema) => schema.type === "string");
|
||||
|
||||
return isObjectMessage || isTupleMessage;
|
||||
}
|
||||
|
||||
export const chatMessagesTester = rankWith(
|
||||
12,
|
||||
and(
|
||||
@@ -34,22 +52,11 @@ export const chatMessagesTester = rankWith(
|
||||
}
|
||||
|
||||
if ("anyOf" in schema.items && schema.items.anyOf != null) {
|
||||
return schema.items.anyOf.every((schema) => {
|
||||
const isObjectMessage =
|
||||
schema.type === "object" &&
|
||||
(schema.title?.endsWith("Message") ||
|
||||
schema.title?.endsWith("MessageChunk"));
|
||||
return schema.items.anyOf.every(checkItemSchema);
|
||||
}
|
||||
|
||||
const isTupleMessage =
|
||||
schema.type === "array" &&
|
||||
schema.minItems === 2 &&
|
||||
schema.maxItems === 2 &&
|
||||
Array.isArray(schema.items) &&
|
||||
schema.items.length === 2 &&
|
||||
schema.items.every((schema) => schema.type === "string");
|
||||
|
||||
return isObjectMessage || isTupleMessage;
|
||||
});
|
||||
if ("oneOf" in schema.items && schema.items.oneOf != null) {
|
||||
return schema.items.oneOf.every(checkItemSchema);
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -64,10 +71,14 @@ export const ChatMessagesControlRenderer = withJsonFormsControlProps(
|
||||
useEffect(() => {
|
||||
if (!isJsonSchemaExtra(props.schema)) return;
|
||||
if (props.schema.extra.widget.type !== "chat") return;
|
||||
setTimeout(() => props.handleChange(props.path, [
|
||||
...data,
|
||||
{ content: "", type: "human" },
|
||||
]), 10);
|
||||
setTimeout(
|
||||
() =>
|
||||
props.handleChange(props.path, [
|
||||
...data,
|
||||
{ content: "", type: "human" },
|
||||
]),
|
||||
10
|
||||
);
|
||||
}, []);
|
||||
|
||||
useStreamCallback("onStart", () => {
|
||||
@@ -81,7 +92,10 @@ export const ChatMessagesControlRenderer = withJsonFormsControlProps(
|
||||
if (props.schema.extra.widget.type !== "chat") return;
|
||||
if (aggregatedState?.final_output !== undefined) {
|
||||
const msgPath = Paths.compose(props.path, `${data.length - 1}`);
|
||||
if ((aggregatedState.final_output as MessageFields)?.type === "AIMessageChunk") {
|
||||
if (
|
||||
(aggregatedState.final_output as MessageFields)?.type ===
|
||||
"AIMessageChunk"
|
||||
) {
|
||||
props.handleChange(
|
||||
Paths.compose(msgPath, "content"),
|
||||
(aggregatedState.final_output as MessageFields)?.content
|
||||
@@ -140,7 +154,7 @@ export const ChatMessagesControlRenderer = withJsonFormsControlProps(
|
||||
props.path,
|
||||
data.filter((_, i) => i !== index)
|
||||
);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<ChatMessageInput
|
||||
message={message}
|
||||
@@ -148,7 +162,7 @@ export const ChatMessagesControlRenderer = withJsonFormsControlProps(
|
||||
handleRemoval={handleChatMessageRemoval}
|
||||
path={props.path}
|
||||
key={index}
|
||||
></ChatMessageInput>
|
||||
></ChatMessageInput>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
from importlib import metadata
|
||||
|
||||
## Create namespaces for pydantic v1 and v2.
|
||||
# This code must stay at the top of the file before other modules may
|
||||
# attempt to import pydantic since it adds pydantic_v1 and pydantic_v2 to sys.modules.
|
||||
#
|
||||
# This hack is done for the following reasons:
|
||||
# * Langchain will attempt to remain compatible with both pydantic v1 and v2 since
|
||||
# both dependencies and dependents may be stuck on either version of v1 or v2.
|
||||
# * Creating namespaces for pydantic v1 and v2 should allow us to write code that
|
||||
# unambiguously uses either v1 or v2 API.
|
||||
# * This change is easier to roll out and roll back.
|
||||
|
||||
try:
|
||||
# F401: imported but unused
|
||||
from pydantic.v1 import ( # noqa: F401
|
||||
BaseModel,
|
||||
Field,
|
||||
ValidationError,
|
||||
create_model,
|
||||
)
|
||||
except ImportError:
|
||||
from pydantic import BaseModel, Field, ValidationError, create_model # noqa: F401
|
||||
|
||||
|
||||
# This is not a pydantic v1 thing, but it feels too small to create a new module for.
|
||||
|
||||
PYDANTIC_VERSION = metadata.version("pydantic")
|
||||
|
||||
try:
|
||||
_PYDANTIC_MAJOR_VERSION: int = int(PYDANTIC_VERSION.split(".")[0])
|
||||
except metadata.PackageNotFoundError:
|
||||
_PYDANTIC_MAJOR_VERSION = -1
|
||||
+5
-4
@@ -2,10 +2,11 @@ from datetime import datetime
|
||||
from typing import Dict, List, Optional, Union
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel # Floats between v1 and v2
|
||||
|
||||
from langserve.pydantic_v1 import BaseModel as BaseModelV1
|
||||
from langserve.pydantic_v1 import Field
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
Field,
|
||||
)
|
||||
from pydantic import BaseModel as BaseModelV1
|
||||
|
||||
|
||||
class CustomUserType(BaseModelV1):
|
||||
|
||||
+57
-41
@@ -13,7 +13,7 @@ sensitive information from the server to the client.
|
||||
import abc
|
||||
import logging
|
||||
from functools import lru_cache
|
||||
from typing import Any, Dict, List, Union
|
||||
from typing import Annotated, Any, Dict, List, Union
|
||||
|
||||
import orjson
|
||||
from langchain_core.agents import AgentAction, AgentActionMessageLog, AgentFinish
|
||||
@@ -29,6 +29,8 @@ from langchain_core.messages import (
|
||||
HumanMessageChunk,
|
||||
SystemMessage,
|
||||
SystemMessageChunk,
|
||||
ToolMessage,
|
||||
ToolMessageChunk,
|
||||
)
|
||||
from langchain_core.outputs import (
|
||||
ChatGeneration,
|
||||
@@ -38,8 +40,8 @@ from langchain_core.outputs import (
|
||||
)
|
||||
from langchain_core.prompt_values import ChatPromptValueConcrete
|
||||
from langchain_core.prompts.base import StringPromptValue
|
||||
from pydantic import BaseModel, Discriminator, Field, RootModel, Tag, ValidationError
|
||||
|
||||
from langserve.pydantic_v1 import BaseModel, ValidationError
|
||||
from langserve.validation import CallbackEvent
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -51,42 +53,58 @@ def _log_error_message_once(error_message: str) -> None:
|
||||
logger.error(error_message)
|
||||
|
||||
|
||||
class WellKnownLCObject(BaseModel):
|
||||
"""A well known LangChain object.
|
||||
def _get_type(v: Any) -> str:
|
||||
"""Get the type associated with the object for serialization purposes."""
|
||||
if isinstance(v, dict) and "type" in v:
|
||||
return v["type"]
|
||||
elif hasattr(v, "type"):
|
||||
return v.type
|
||||
else:
|
||||
raise TypeError(
|
||||
f"Expected either a dictionary with a 'type' key or an object "
|
||||
f"with a 'type' attribute. Instead got type {type(v)}."
|
||||
)
|
||||
|
||||
A pydantic model that defines what constitutes a well known LangChain object.
|
||||
|
||||
All well-known objects are allowed to be serialized and de-serialized.
|
||||
"""
|
||||
# A well known LangChain object.
|
||||
# A pydantic model that defines what constitutes a well known LangChain object.
|
||||
# All well-known objects are allowed to be serialized and de-serialized.
|
||||
|
||||
__root__: Union[
|
||||
Document,
|
||||
HumanMessage,
|
||||
SystemMessage,
|
||||
ChatMessage,
|
||||
FunctionMessage,
|
||||
AIMessage,
|
||||
HumanMessageChunk,
|
||||
SystemMessageChunk,
|
||||
ChatMessageChunk,
|
||||
FunctionMessageChunk,
|
||||
AIMessageChunk,
|
||||
StringPromptValue,
|
||||
ChatPromptValueConcrete,
|
||||
AgentAction,
|
||||
AgentFinish,
|
||||
AgentActionMessageLog,
|
||||
LLMResult,
|
||||
ChatGeneration,
|
||||
Generation,
|
||||
ChatGenerationChunk,
|
||||
WellKnownLCObject = RootModel[
|
||||
Annotated[
|
||||
Union[
|
||||
Annotated[AIMessage, Tag(tag="ai")],
|
||||
Annotated[HumanMessage, Tag(tag="human")],
|
||||
Annotated[ChatMessage, Tag(tag="chat")],
|
||||
Annotated[SystemMessage, Tag(tag="system")],
|
||||
Annotated[FunctionMessage, Tag(tag="function")],
|
||||
Annotated[ToolMessage, Tag(tag="tool")],
|
||||
Annotated[AIMessageChunk, Tag(tag="AIMessageChunk")],
|
||||
Annotated[HumanMessageChunk, Tag(tag="HumanMessageChunk")],
|
||||
Annotated[ChatMessageChunk, Tag(tag="ChatMessageChunk")],
|
||||
Annotated[SystemMessageChunk, Tag(tag="SystemMessageChunk")],
|
||||
Annotated[FunctionMessageChunk, Tag(tag="FunctionMessageChunk")],
|
||||
Annotated[ToolMessageChunk, Tag(tag="ToolMessageChunk")],
|
||||
Annotated[Document, Tag(tag="Document")],
|
||||
Annotated[StringPromptValue, Tag(tag="StringPromptValue")],
|
||||
Annotated[ChatPromptValueConcrete, Tag(tag="ChatPromptValueConcrete")],
|
||||
Annotated[AgentAction, Tag(tag="AgentAction")],
|
||||
Annotated[AgentFinish, Tag(tag="AgentFinish")],
|
||||
Annotated[AgentActionMessageLog, Tag(tag="AgentActionMessageLog")],
|
||||
Annotated[ChatGeneration, Tag(tag="ChatGeneration")],
|
||||
Annotated[Generation, Tag(tag="Generation")],
|
||||
Annotated[ChatGenerationChunk, Tag(tag="ChatGenerationChunk")],
|
||||
Annotated[LLMResult, Tag(tag="LLMResult")],
|
||||
],
|
||||
Field(discriminator=Discriminator(_get_type)),
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
def default(obj) -> Any:
|
||||
"""Default serialization for well known objects."""
|
||||
if isinstance(obj, BaseModel):
|
||||
return obj.dict()
|
||||
return obj.model_dump()
|
||||
return super().default(obj)
|
||||
|
||||
|
||||
@@ -96,12 +114,10 @@ def _decode_lc_objects(value: Any) -> Any:
|
||||
v = {key: _decode_lc_objects(v) for key, v in value.items()}
|
||||
|
||||
try:
|
||||
obj = WellKnownLCObject.parse_obj(v)
|
||||
parsed = obj.__root__
|
||||
if set(parsed.dict()) != set(value):
|
||||
raise ValueError("Invalid object")
|
||||
obj = WellKnownLCObject.model_validate(v)
|
||||
parsed = obj.root
|
||||
return parsed
|
||||
except (ValidationError, ValueError):
|
||||
except (ValidationError, ValueError, TypeError):
|
||||
return v
|
||||
elif isinstance(value, list):
|
||||
return [_decode_lc_objects(item) for item in value]
|
||||
@@ -123,12 +139,12 @@ def _decode_event_data(value: Any) -> Any:
|
||||
"""Decode the event data from a JSON object representation."""
|
||||
if isinstance(value, dict):
|
||||
try:
|
||||
obj = CallbackEvent.parse_obj(value)
|
||||
return obj.__root__
|
||||
obj = CallbackEvent.model_validate(value)
|
||||
return obj.root
|
||||
except ValidationError:
|
||||
try:
|
||||
obj = WellKnownLCObject.parse_obj(value)
|
||||
return obj.__root__
|
||||
obj = WellKnownLCObject.model_validate(value)
|
||||
return obj.root
|
||||
except ValidationError:
|
||||
return {key: _decode_event_data(v) for key, v in value.items()}
|
||||
elif isinstance(value, list):
|
||||
@@ -178,7 +194,7 @@ class WellKnownLCSerializer(Serializer):
|
||||
|
||||
def _project_top_level(model: BaseModel) -> Dict[str, Any]:
|
||||
"""Project the top level of the model as dict."""
|
||||
return {key: getattr(model, key) for key in model.__fields__}
|
||||
return {key: getattr(model, key) for key in model.model_fields}
|
||||
|
||||
|
||||
def load_events(events: Any) -> List[Dict[str, Any]]:
|
||||
@@ -209,7 +225,7 @@ def load_events(events: Any) -> List[Dict[str, Any]]:
|
||||
|
||||
# Then validate the event
|
||||
try:
|
||||
full_event = CallbackEvent.parse_obj(decoded_event_data)
|
||||
full_event = CallbackEvent.model_validate(decoded_event_data)
|
||||
except ValidationError as e:
|
||||
msg = f"Encountered an invalid event: {e}"
|
||||
if "type" in decoded_event_data:
|
||||
@@ -217,7 +233,7 @@ def load_events(events: Any) -> List[Dict[str, Any]]:
|
||||
_log_error_message_once(msg)
|
||||
continue
|
||||
|
||||
decoded_event_data = _project_top_level(full_event.__root__)
|
||||
decoded_event_data = _project_top_level(full_event.root)
|
||||
|
||||
if decoded_event_data["type"].endswith("_error"):
|
||||
# Data is validated by this point, so we can assume that the shape
|
||||
|
||||
+296
-336
@@ -5,6 +5,7 @@ This code contains integration for langchain runnables with FastAPI.
|
||||
The main entry point is the `add_routes` function which adds the routes to an existing
|
||||
FastAPI app or APIRouter.
|
||||
"""
|
||||
import warnings
|
||||
import weakref
|
||||
from typing import (
|
||||
Any,
|
||||
@@ -16,6 +17,7 @@ from typing import (
|
||||
)
|
||||
|
||||
from langchain_core.runnables import Runnable
|
||||
from pydantic import BaseModel
|
||||
from typing_extensions import Annotated
|
||||
|
||||
from langserve.api_handler import (
|
||||
@@ -24,11 +26,6 @@ from langserve.api_handler import (
|
||||
TokenFeedbackConfig,
|
||||
_is_hosted,
|
||||
)
|
||||
from langserve.pydantic_v1 import (
|
||||
_PYDANTIC_MAJOR_VERSION,
|
||||
PYDANTIC_VERSION,
|
||||
BaseModel,
|
||||
)
|
||||
|
||||
try:
|
||||
from fastapi import APIRouter, Depends, FastAPI, Request, Response
|
||||
@@ -205,51 +202,43 @@ def _register_path_for_app(
|
||||
def _setup_global_app_handlers(
|
||||
app: Union[FastAPI, APIRouter], endpoint_configuration: _EndpointConfiguration
|
||||
) -> None:
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
LANGSERVE = r"""
|
||||
__ ___ .__ __. _______ _______. _______ .______ ____ ____ _______
|
||||
| | / \ | \ | | / _____| / || ____|| _ \ \ \ / / | ____|
|
||||
| | / ^ \ | \| | | | __ | (----`| |__ | |_) | \ \/ / | |__
|
||||
| | / /_\ \ | . ` | | | |_ | \ \ | __| | / \ / | __|
|
||||
| `----./ _____ \ | |\ | | |__| | .----) | | |____ | |\ \----. \ / | |____
|
||||
|_______/__/ \__\ |__| \__| \______| |_______/ |_______|| _| `._____| \__/ |_______|
|
||||
""" # noqa: E501
|
||||
with warnings.catch_warnings():
|
||||
# We are using deprecated functionality here.
|
||||
# We should re-write to use lifetime events at some point, and yielding
|
||||
# an APIRouter instance to the caller.
|
||||
warnings.filterwarnings(
|
||||
"ignore",
|
||||
"[\\s.]*on_event is deprecated[\\s.]*",
|
||||
category=DeprecationWarning,
|
||||
)
|
||||
|
||||
def green(text: str) -> str:
|
||||
"""Return the given text in green."""
|
||||
return "\x1b[1;32;40m" + text + "\x1b[0m"
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
LANGSERVE = r"""
|
||||
__ ___ .__ __. _______ _______. _______ .______ ____ ____ _______
|
||||
| | / \ | \ | | / _____| / || ____|| _ \ \ \ / / | ____|
|
||||
| | / ^ \ | \| | | | __ | (----`| |__ | |_) | \ \/ / | |__
|
||||
| | / /_\ \ | . ` | | | |_ | \ \ | __| | / \ / | __|
|
||||
| `----./ _____ \ | |\ | | |__| | .----) | | |____ | |\ \----. \ / | |____
|
||||
|_______/__/ \__\ |__| \__| \______| |_______/ |_______|| _| `._____| \__/ |_______|
|
||||
""" # noqa: E501
|
||||
|
||||
def orange(text: str) -> str:
|
||||
"""Return the given text in orange."""
|
||||
return "\x1b[1;31;40m" + text + "\x1b[0m"
|
||||
def green(text: str) -> str:
|
||||
"""Return the given text in green."""
|
||||
return "\x1b[1;32;40m" + text + "\x1b[0m"
|
||||
|
||||
paths = _APP_TO_PATHS[app]
|
||||
print(LANGSERVE)
|
||||
for path in paths:
|
||||
if endpoint_configuration.is_playground_enabled:
|
||||
print(
|
||||
f'{green("LANGSERVE:")} Playground for chain "{path or ""}/" is '
|
||||
f"live at:"
|
||||
)
|
||||
print(f'{green("LANGSERVE:")} │')
|
||||
print(f'{green("LANGSERVE:")} └──> {path}/playground/')
|
||||
print(f'{green("LANGSERVE:")}')
|
||||
print(f'{green("LANGSERVE:")} See all available routes at {app.docs_url}/')
|
||||
|
||||
if _PYDANTIC_MAJOR_VERSION == 2:
|
||||
print()
|
||||
print(f'{orange("LANGSERVE:")} ', end="")
|
||||
print(
|
||||
f"⚠️ Using pydantic {PYDANTIC_VERSION}. "
|
||||
f"OpenAPI docs for invoke, batch, stream, stream_log "
|
||||
f"endpoints will not be generated. API endpoints and playground "
|
||||
f"should work as expected. "
|
||||
f"If you need to see the docs, you can downgrade to pydantic 1. "
|
||||
"For example, `pip install pydantic==1.10.13`. "
|
||||
f"See https://github.com/tiangolo/fastapi/issues/10360 for details."
|
||||
)
|
||||
print()
|
||||
paths = _APP_TO_PATHS[app]
|
||||
print(LANGSERVE)
|
||||
for path in paths:
|
||||
if endpoint_configuration.is_playground_enabled:
|
||||
print(
|
||||
f'{green("LANGSERVE:")} Playground for chain "{path or ""}/" '
|
||||
f'is live at:'
|
||||
)
|
||||
print(f'{green("LANGSERVE:")} │')
|
||||
print(f'{green("LANGSERVE:")} └──> {path}/playground/')
|
||||
print(f'{green("LANGSERVE:")}')
|
||||
print(f'{green("LANGSERVE:")} See all available routes at {app.docs_url}/')
|
||||
|
||||
|
||||
# PUBLIC API
|
||||
@@ -473,35 +462,9 @@ def add_routes(
|
||||
if hasattr(app, "openapi_tags") and (path or (app not in _APP_SEEN)):
|
||||
if not path:
|
||||
_APP_SEEN.add(app)
|
||||
|
||||
if _PYDANTIC_MAJOR_VERSION == 1:
|
||||
# Documentation for the default endpoints
|
||||
default_endpoint_tags = {
|
||||
"name": route_tags[0] if route_tags else "default",
|
||||
}
|
||||
elif _PYDANTIC_MAJOR_VERSION == 2:
|
||||
# When using pydantic v2, we cannot generate openapi docs for
|
||||
# the invoke/batch/stream/stream_log endpoints since the underlying
|
||||
# models are from the pydantic.v1 namespace and cannot be supported
|
||||
# by FastAPI's.
|
||||
# https://github.com/tiangolo/fastapi/issues/10360
|
||||
default_endpoint_tags = {
|
||||
"name": route_tags[0] if route_tags else "default",
|
||||
"description": (
|
||||
f"⚠️ Using pydantic {PYDANTIC_VERSION}. "
|
||||
f"OpenAPI docs for `invoke`, `batch`, `stream`, `stream_log` "
|
||||
f"endpoints will not be generated. API endpoints and playground "
|
||||
f"should work as expected. "
|
||||
f"If you need to see the docs, you can downgrade to pydantic 1. "
|
||||
"For example, `pip install pydantic==1.10.13`"
|
||||
f"See https://github.com/tiangolo/fastapi/issues/10360 for details."
|
||||
),
|
||||
}
|
||||
else:
|
||||
raise AssertionError(
|
||||
f"Expected pydantic major version 1 or 2, got {_PYDANTIC_MAJOR_VERSION}"
|
||||
)
|
||||
|
||||
default_endpoint_tags = {
|
||||
"name": route_tags[0] if route_tags else "default",
|
||||
}
|
||||
if endpoint_configuration.is_config_hash_enabled:
|
||||
app.openapi_tags = [
|
||||
*(getattr(app, "openapi_tags", []) or []),
|
||||
@@ -778,329 +741,326 @@ def add_routes(
|
||||
# Documentation variants of end points.
|
||||
#######################################
|
||||
# At the moment, we only support pydantic 1.x for documentation
|
||||
if _PYDANTIC_MAJOR_VERSION == 1:
|
||||
InvokeRequest = api_handler.InvokeRequest
|
||||
InvokeResponse = api_handler.InvokeResponse
|
||||
BatchRequest = api_handler.BatchRequest
|
||||
BatchResponse = api_handler.BatchResponse
|
||||
StreamRequest = api_handler.StreamRequest
|
||||
StreamLogRequest = api_handler.StreamLogRequest
|
||||
StreamEventsRequest = api_handler.StreamEventsRequest
|
||||
InvokeRequest = api_handler.InvokeRequest
|
||||
InvokeResponse = api_handler.InvokeResponse
|
||||
BatchRequest = api_handler.BatchRequest
|
||||
BatchResponse = api_handler.BatchResponse
|
||||
StreamRequest = api_handler.StreamRequest
|
||||
StreamLogRequest = api_handler.StreamLogRequest
|
||||
StreamEventsRequest = api_handler.StreamEventsRequest
|
||||
|
||||
if endpoint_configuration.is_invoke_enabled:
|
||||
if endpoint_configuration.is_invoke_enabled:
|
||||
|
||||
async def _invoke_docs(
|
||||
invoke_request: Annotated[InvokeRequest, InvokeRequest],
|
||||
config_hash: str = "",
|
||||
) -> InvokeResponse:
|
||||
"""Invoke the runnable with the given input and config."""
|
||||
raise AssertionError("This endpoint should not be reachable.")
|
||||
async def _invoke_docs(
|
||||
invoke_request: Annotated[InvokeRequest, InvokeRequest],
|
||||
config_hash: str = "",
|
||||
) -> InvokeResponse:
|
||||
"""Invoke the runnable with the given input and config."""
|
||||
raise AssertionError("This endpoint should not be reachable.")
|
||||
|
||||
invoke_docs = app.post(
|
||||
f"{namespace}/invoke",
|
||||
invoke_docs = app.post(
|
||||
f"{namespace}/invoke",
|
||||
response_model=api_handler.InvokeResponse,
|
||||
tags=route_tags,
|
||||
name=_route_name("invoke"),
|
||||
dependencies=dependencies,
|
||||
)(_invoke_docs)
|
||||
|
||||
if endpoint_configuration.is_config_hash_enabled:
|
||||
app.post(
|
||||
namespace + "/c/{config_hash}/invoke",
|
||||
response_model=api_handler.InvokeResponse,
|
||||
tags=route_tags,
|
||||
name=_route_name("invoke"),
|
||||
tags=route_tags_with_config,
|
||||
name=_route_name_with_config("invoke"),
|
||||
dependencies=dependencies,
|
||||
)(_invoke_docs)
|
||||
description=(
|
||||
"This endpoint is to be used with share links generated by the "
|
||||
"LangServe playground. "
|
||||
"The hash is an LZString compressed JSON string. "
|
||||
"For regular use cases, use the /invoke endpoint without "
|
||||
"the `c/{config_hash}` path parameter."
|
||||
),
|
||||
)(invoke_docs)
|
||||
|
||||
if endpoint_configuration.is_config_hash_enabled:
|
||||
app.post(
|
||||
namespace + "/c/{config_hash}/invoke",
|
||||
response_model=api_handler.InvokeResponse,
|
||||
tags=route_tags_with_config,
|
||||
name=_route_name_with_config("invoke"),
|
||||
dependencies=dependencies,
|
||||
description=(
|
||||
"This endpoint is to be used with share links generated by the "
|
||||
"LangServe playground. "
|
||||
"The hash is an LZString compressed JSON string. "
|
||||
"For regular use cases, use the /invoke endpoint without "
|
||||
"the `c/{config_hash}` path parameter."
|
||||
),
|
||||
)(invoke_docs)
|
||||
if endpoint_configuration.is_batch_enabled:
|
||||
|
||||
if endpoint_configuration.is_batch_enabled:
|
||||
async def _batch_docs(
|
||||
batch_request: Annotated[BatchRequest, BatchRequest],
|
||||
config_hash: str = "",
|
||||
) -> BatchResponse:
|
||||
"""Batch invoke the runnable with the given inputs and config."""
|
||||
raise AssertionError("This endpoint should not be reachable.")
|
||||
|
||||
async def _batch_docs(
|
||||
batch_request: Annotated[BatchRequest, BatchRequest],
|
||||
config_hash: str = "",
|
||||
) -> BatchResponse:
|
||||
"""Batch invoke the runnable with the given inputs and config."""
|
||||
raise AssertionError("This endpoint should not be reachable.")
|
||||
batch_docs = app.post(
|
||||
f"{namespace}/batch",
|
||||
response_model=BatchResponse,
|
||||
tags=route_tags,
|
||||
name=_route_name("batch"),
|
||||
dependencies=dependencies,
|
||||
)(_batch_docs)
|
||||
|
||||
batch_docs = app.post(
|
||||
f"{namespace}/batch",
|
||||
if endpoint_configuration.is_config_hash_enabled:
|
||||
app.post(
|
||||
namespace + "/c/{config_hash}/batch",
|
||||
response_model=BatchResponse,
|
||||
tags=route_tags,
|
||||
name=_route_name("batch"),
|
||||
tags=route_tags_with_config,
|
||||
name=_route_name_with_config("batch"),
|
||||
dependencies=dependencies,
|
||||
)(_batch_docs)
|
||||
description=(
|
||||
"This endpoint is to be used with share links generated by the "
|
||||
"LangServe playground. "
|
||||
"The hash is an LZString compressed JSON string. "
|
||||
"For regular use cases, use the /batch endpoint without "
|
||||
"the `c/{config_hash}` path parameter."
|
||||
),
|
||||
)(batch_docs)
|
||||
|
||||
if endpoint_configuration.is_config_hash_enabled:
|
||||
app.post(
|
||||
namespace + "/c/{config_hash}/batch",
|
||||
response_model=BatchResponse,
|
||||
tags=route_tags_with_config,
|
||||
name=_route_name_with_config("batch"),
|
||||
dependencies=dependencies,
|
||||
description=(
|
||||
"This endpoint is to be used with share links generated by the "
|
||||
"LangServe playground. "
|
||||
"The hash is an LZString compressed JSON string. "
|
||||
"For regular use cases, use the /batch endpoint without "
|
||||
"the `c/{config_hash}` path parameter."
|
||||
),
|
||||
)(batch_docs)
|
||||
if endpoint_configuration.is_stream_enabled:
|
||||
|
||||
if endpoint_configuration.is_stream_enabled:
|
||||
async def _stream_docs(
|
||||
stream_request: Annotated[StreamRequest, StreamRequest],
|
||||
config_hash: str = "",
|
||||
) -> EventSourceResponse:
|
||||
"""Invoke the runnable stream the output.
|
||||
|
||||
async def _stream_docs(
|
||||
stream_request: Annotated[StreamRequest, StreamRequest],
|
||||
config_hash: str = "",
|
||||
) -> EventSourceResponse:
|
||||
"""Invoke the runnable stream the output.
|
||||
This endpoint allows to stream the output of the runnable.
|
||||
|
||||
This endpoint allows to stream the output of the runnable.
|
||||
The endpoint uses a server sent event stream to stream the output.
|
||||
|
||||
The endpoint uses a server sent event stream to stream the output.
|
||||
https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events
|
||||
|
||||
https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events
|
||||
Important: Set the "text/event-stream" media type for request headers if
|
||||
not using an existing SDK.
|
||||
|
||||
Important: Set the "text/event-stream" media type for request headers if
|
||||
not using an existing SDK.
|
||||
The events that the endpoint uses are the following:
|
||||
* "data" -- used for streaming the output of the runnale
|
||||
* "error" -- signaling an error while streaming and ends the stream.
|
||||
* "end" -- used for signaling the end of the stream
|
||||
* "metadata" -- used for sending metadata about the run; e.g., run id.
|
||||
|
||||
The events that the endpoint uses are the following:
|
||||
* "data" -- used for streaming the output of the runnale
|
||||
* "error" -- signaling an error while streaming and ends the stream.
|
||||
* "end" -- used for signaling the end of the stream
|
||||
* "metadata" -- used for sending metadata about the run; e.g., run id.
|
||||
|
||||
The event type is in the "event" field of the event.
|
||||
The payload associated with the event is in the "data" field
|
||||
of the event, and it is JSON encoded.
|
||||
The event type is in the "event" field of the event.
|
||||
The payload associated with the event is in the "data" field
|
||||
of the event, and it is JSON encoded.
|
||||
|
||||
|
||||
Here are some examples of events that the endpoint can send:
|
||||
Here are some examples of events that the endpoint can send:
|
||||
|
||||
Regular streaming event:
|
||||
{
|
||||
"event": "data",
|
||||
"data": {
|
||||
...
|
||||
}
|
||||
}
|
||||
|
||||
Internal server error:
|
||||
{
|
||||
"event": "error",
|
||||
"data": {
|
||||
"status_code": 500,
|
||||
"message": "Internal Server Error"
|
||||
}
|
||||
}
|
||||
|
||||
Streaming ended so client should stop listening for events:
|
||||
{
|
||||
"event": "end",
|
||||
}
|
||||
"""
|
||||
raise AssertionError("This endpoint should not be reachable.")
|
||||
|
||||
stream_docs = app.post(
|
||||
f"{namespace}/stream",
|
||||
include_in_schema=True,
|
||||
tags=route_tags,
|
||||
name=_route_name("stream"),
|
||||
dependencies=dependencies,
|
||||
description=(
|
||||
"This endpoint allows to stream the output of the runnable. "
|
||||
"The endpoint uses a server sent event stream to stream the "
|
||||
"output. "
|
||||
"https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events"
|
||||
),
|
||||
)(_stream_docs)
|
||||
|
||||
if endpoint_configuration.is_config_hash_enabled:
|
||||
app.post(
|
||||
namespace + "/c/{config_hash}/stream",
|
||||
include_in_schema=True,
|
||||
tags=route_tags_with_config,
|
||||
name=_route_name_with_config("stream"),
|
||||
dependencies=dependencies,
|
||||
description=(
|
||||
"This endpoint is to be used with share links generated by the "
|
||||
"LangServe playground. "
|
||||
"The hash is an LZString compressed JSON string. "
|
||||
"For regular use cases, use the /stream endpoint without "
|
||||
"the `c/{config_hash}` path parameter."
|
||||
),
|
||||
)(stream_docs)
|
||||
|
||||
if endpoint_configuration.is_stream_log_enabled:
|
||||
|
||||
async def _stream_log_docs(
|
||||
stream_log_request: Annotated[StreamLogRequest, StreamLogRequest],
|
||||
config_hash: str = "",
|
||||
) -> EventSourceResponse:
|
||||
"""Invoke the runnable stream_log the output.
|
||||
|
||||
This endpoint allows to stream the output of the runnable, including
|
||||
the output of all intermediate steps.
|
||||
|
||||
The endpoint uses a server sent event stream to stream the output.
|
||||
|
||||
https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events
|
||||
|
||||
Important: Set the "text/event-stream" media type for request headers if
|
||||
not using an existing SDK.
|
||||
|
||||
This endpoint uses two different types of events:
|
||||
|
||||
* data - for streaming the output of the runnable
|
||||
|
||||
Regular streaming event:
|
||||
{
|
||||
"event": "data",
|
||||
"data": {
|
||||
...
|
||||
...
|
||||
}
|
||||
}
|
||||
|
||||
Internal server error:
|
||||
{
|
||||
"event": "error",
|
||||
"data": {
|
||||
"status_code": 500,
|
||||
"message": "Internal Server Error"
|
||||
}
|
||||
}
|
||||
* error - for signaling an error in the stream, also ends the stream.
|
||||
|
||||
{
|
||||
"event": "error",
|
||||
"data": {
|
||||
"status_code": 500,
|
||||
"message": "Internal Server Error"
|
||||
}
|
||||
}
|
||||
|
||||
* end - for signaling the end of the stream.
|
||||
|
||||
This helps the client to know when to stop listening for events and
|
||||
know that the streaming has ended successfully.
|
||||
|
||||
Streaming ended so client should stop listening for events:
|
||||
{
|
||||
"event": "end",
|
||||
}
|
||||
"""
|
||||
raise AssertionError("This endpoint should not be reachable.")
|
||||
"""
|
||||
raise AssertionError("This endpoint should not be reachable.")
|
||||
|
||||
stream_docs = app.post(
|
||||
f"{namespace}/stream",
|
||||
include_in_schema=True,
|
||||
tags=route_tags,
|
||||
name=_route_name("stream"),
|
||||
dependencies=dependencies,
|
||||
description=(
|
||||
"This endpoint allows to stream the output of the runnable. "
|
||||
"The endpoint uses a server sent event stream to stream the "
|
||||
"output. "
|
||||
"https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events"
|
||||
),
|
||||
)(_stream_docs)
|
||||
|
||||
if endpoint_configuration.is_config_hash_enabled:
|
||||
app.post(
|
||||
namespace + "/c/{config_hash}/stream",
|
||||
include_in_schema=True,
|
||||
tags=route_tags_with_config,
|
||||
name=_route_name_with_config("stream"),
|
||||
dependencies=dependencies,
|
||||
description=(
|
||||
"This endpoint is to be used with share links generated by the "
|
||||
"LangServe playground. "
|
||||
"The hash is an LZString compressed JSON string. "
|
||||
"For regular use cases, use the /stream endpoint without "
|
||||
"the `c/{config_hash}` path parameter."
|
||||
),
|
||||
)(stream_docs)
|
||||
|
||||
if endpoint_configuration.is_stream_log_enabled:
|
||||
|
||||
async def _stream_log_docs(
|
||||
stream_log_request: Annotated[StreamLogRequest, StreamLogRequest],
|
||||
config_hash: str = "",
|
||||
) -> EventSourceResponse:
|
||||
"""Invoke the runnable stream_log the output.
|
||||
|
||||
This endpoint allows to stream the output of the runnable, including
|
||||
the output of all intermediate steps.
|
||||
|
||||
The endpoint uses a server sent event stream to stream the output.
|
||||
|
||||
https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events
|
||||
|
||||
Important: Set the "text/event-stream" media type for request headers if
|
||||
not using an existing SDK.
|
||||
|
||||
This endpoint uses two different types of events:
|
||||
|
||||
* data - for streaming the output of the runnable
|
||||
|
||||
{
|
||||
"event": "data",
|
||||
"data": {
|
||||
...
|
||||
}
|
||||
}
|
||||
|
||||
* error - for signaling an error in the stream, also ends the stream.
|
||||
|
||||
{
|
||||
"event": "error",
|
||||
"data": {
|
||||
"status_code": 500,
|
||||
"message": "Internal Server Error"
|
||||
}
|
||||
}
|
||||
|
||||
* end - for signaling the end of the stream.
|
||||
|
||||
This helps the client to know when to stop listening for events and
|
||||
know that the streaming has ended successfully.
|
||||
|
||||
{
|
||||
"event": "end",
|
||||
}
|
||||
"""
|
||||
raise AssertionError("This endpoint should not be reachable.")
|
||||
app.post(
|
||||
f"{namespace}/stream_log",
|
||||
include_in_schema=True,
|
||||
tags=route_tags,
|
||||
name=_route_name("stream_log"),
|
||||
dependencies=dependencies,
|
||||
)(_stream_log_docs)
|
||||
|
||||
if endpoint_configuration.is_config_hash_enabled:
|
||||
app.post(
|
||||
f"{namespace}/stream_log",
|
||||
namespace + "/c/{config_hash}/stream_log",
|
||||
include_in_schema=True,
|
||||
tags=route_tags,
|
||||
name=_route_name("stream_log"),
|
||||
tags=route_tags_with_config,
|
||||
name=_route_name_with_config("stream_log"),
|
||||
description=(
|
||||
"This endpoint is to be used with share links generated by the "
|
||||
"LangServe playground. "
|
||||
"The hash is an LZString compressed JSON string. "
|
||||
"For regular use cases, use the /stream_log endpoint without "
|
||||
"the `c/{config_hash}` path parameter."
|
||||
),
|
||||
dependencies=dependencies,
|
||||
)(_stream_log_docs)
|
||||
|
||||
if endpoint_configuration.is_config_hash_enabled:
|
||||
app.post(
|
||||
namespace + "/c/{config_hash}/stream_log",
|
||||
include_in_schema=True,
|
||||
tags=route_tags_with_config,
|
||||
name=_route_name_with_config("stream_log"),
|
||||
description=(
|
||||
"This endpoint is to be used with share links generated by the "
|
||||
"LangServe playground. "
|
||||
"The hash is an LZString compressed JSON string. "
|
||||
"For regular use cases, use the /stream_log endpoint without "
|
||||
"the `c/{config_hash}` path parameter."
|
||||
),
|
||||
dependencies=dependencies,
|
||||
)(_stream_log_docs)
|
||||
if has_astream_events and endpoint_configuration.is_stream_events_enabled:
|
||||
|
||||
if has_astream_events and endpoint_configuration.is_stream_events_enabled:
|
||||
async def _stream_events_docs(
|
||||
stream_events_request: Annotated[StreamEventsRequest, StreamEventsRequest],
|
||||
config_hash: str = "",
|
||||
) -> EventSourceResponse:
|
||||
"""Stream events from the given runnable.
|
||||
|
||||
async def _stream_events_docs(
|
||||
stream_events_request: Annotated[
|
||||
StreamEventsRequest, StreamEventsRequest
|
||||
],
|
||||
config_hash: str = "",
|
||||
) -> EventSourceResponse:
|
||||
"""Stream events from the given runnable.
|
||||
This endpoint allows to stream events from the runnable, including
|
||||
events from all intermediate steps.
|
||||
|
||||
This endpoint allows to stream events from the runnable, including
|
||||
events from all intermediate steps.
|
||||
**Attention**
|
||||
|
||||
**Attention**
|
||||
This is a new endpoint that only works for langchain-core >= 0.1.14.
|
||||
|
||||
This is a new endpoint that only works for langchain-core >= 0.1.14.
|
||||
It belongs to a Beta API that may change in the future.
|
||||
|
||||
It belongs to a Beta API that may change in the future.
|
||||
**Important**
|
||||
Specify filters to the events you want to receive by setting
|
||||
the appropriate filters in the request body.
|
||||
|
||||
**Important**
|
||||
Specify filters to the events you want to receive by setting
|
||||
the appropriate filters in the request body.
|
||||
This will help avoid sending too much data over the network.
|
||||
|
||||
This will help avoid sending too much data over the network.
|
||||
It will also prevent serialization issues with
|
||||
any unsupported types since it won't need to serialize events
|
||||
that aren't transmitted.
|
||||
|
||||
It will also prevent serialization issues with
|
||||
any unsupported types since it won't need to serialize events
|
||||
that aren't transmitted.
|
||||
The endpoint uses a server sent event stream to stream the output.
|
||||
|
||||
The endpoint uses a server sent event stream to stream the output.
|
||||
https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events
|
||||
|
||||
https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events
|
||||
The encoding of events follows the following format:
|
||||
|
||||
The encoding of events follows the following format:
|
||||
|
||||
* data - for streaming the output of the runnable
|
||||
|
||||
{
|
||||
"event": "data",
|
||||
"data": {
|
||||
...
|
||||
}
|
||||
}
|
||||
|
||||
* error - for signaling an error in the stream, also ends the stream.
|
||||
* data - for streaming the output of the runnable
|
||||
|
||||
{
|
||||
"event": "error",
|
||||
"event": "data",
|
||||
"data": {
|
||||
"status_code": 500,
|
||||
"message": "Internal Server Error"
|
||||
...
|
||||
}
|
||||
}
|
||||
|
||||
* end - for signaling the end of the stream.
|
||||
* error - for signaling an error in the stream, also ends the stream.
|
||||
|
||||
This helps the client to know when to stop listening for events and
|
||||
know that the streaming has ended successfully.
|
||||
{
|
||||
"event": "error",
|
||||
"data": {
|
||||
"status_code": 500,
|
||||
"message": "Internal Server Error"
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"event": "end",
|
||||
}
|
||||
* end - for signaling the end of the stream.
|
||||
|
||||
`data` for the `data` event is a JSON object that corresponds
|
||||
to a serialized representation of a StreamEvent.
|
||||
This helps the client to know when to stop listening for events and
|
||||
know that the streaming has ended successfully.
|
||||
|
||||
See LangChain documentation for more information about astream_events.
|
||||
"""
|
||||
raise AssertionError("This endpoint should not be reachable.")
|
||||
{
|
||||
"event": "end",
|
||||
}
|
||||
|
||||
`data` for the `data` event is a JSON object that corresponds
|
||||
to a serialized representation of a StreamEvent.
|
||||
|
||||
See LangChain documentation for more information about astream_events.
|
||||
"""
|
||||
raise AssertionError("This endpoint should not be reachable.")
|
||||
|
||||
app.post(
|
||||
f"{namespace}/stream_events",
|
||||
include_in_schema=True,
|
||||
tags=route_tags,
|
||||
name=_route_name("stream_events"),
|
||||
dependencies=dependencies,
|
||||
)(_stream_events_docs)
|
||||
|
||||
if endpoint_configuration.is_config_hash_enabled:
|
||||
app.post(
|
||||
f"{namespace}/stream_events",
|
||||
namespace + "/c/{config_hash}/stream_events",
|
||||
include_in_schema=True,
|
||||
tags=route_tags,
|
||||
name=_route_name("stream_events"),
|
||||
tags=route_tags_with_config,
|
||||
name=_route_name_with_config("stream_events"),
|
||||
description=(
|
||||
"This endpoint is to be used with share links generated by the "
|
||||
"LangServe playground. "
|
||||
"The hash is an LZString compressed JSON string. "
|
||||
"For regular use cases, use the /stream_events endpoint "
|
||||
"without the `c/{config_hash}` path parameter."
|
||||
),
|
||||
dependencies=dependencies,
|
||||
)(_stream_events_docs)
|
||||
|
||||
if endpoint_configuration.is_config_hash_enabled:
|
||||
app.post(
|
||||
namespace + "/c/{config_hash}/stream_events",
|
||||
include_in_schema=True,
|
||||
tags=route_tags_with_config,
|
||||
name=_route_name_with_config("stream_events"),
|
||||
description=(
|
||||
"This endpoint is to be used with share links generated by the "
|
||||
"LangServe playground. "
|
||||
"The hash is an LZString compressed JSON string. "
|
||||
"For regular use cases, use the /stream_events endpoint "
|
||||
"without the `c/{config_hash}` path parameter."
|
||||
),
|
||||
dependencies=dependencies,
|
||||
)(_stream_events_docs)
|
||||
|
||||
+11
-15
@@ -22,16 +22,11 @@ from uuid import UUID
|
||||
from langchain_core.documents import Document
|
||||
from langchain_core.messages import BaseMessage
|
||||
from langchain_core.outputs import ChatGeneration, Generation, RunInfo
|
||||
from pydantic import BaseModel, Field, RootModel, create_model
|
||||
from typing_extensions import Type
|
||||
|
||||
from langserve.schema import BatchResponseMetadata, InvokeResponseMetadata
|
||||
|
||||
try:
|
||||
from pydantic.v1 import BaseModel, Field, create_model
|
||||
except ImportError:
|
||||
from pydantic import BaseModel, Field, create_model
|
||||
|
||||
|
||||
# Type that is either a python annotation or a pydantic model that can be
|
||||
# used to validate the input or output of a runnable.
|
||||
Validator = Union[Type[BaseModel], type]
|
||||
@@ -66,7 +61,7 @@ def create_invoke_request_model(
|
||||
),
|
||||
),
|
||||
)
|
||||
invoke_request_type.update_forward_refs()
|
||||
invoke_request_type.model_rebuild()
|
||||
return invoke_request_type
|
||||
|
||||
|
||||
@@ -97,7 +92,7 @@ def create_stream_request_model(
|
||||
),
|
||||
),
|
||||
)
|
||||
stream_request_model.update_forward_refs()
|
||||
stream_request_model.model_rebuild()
|
||||
return stream_request_model
|
||||
|
||||
|
||||
@@ -129,7 +124,7 @@ def create_batch_request_model(
|
||||
),
|
||||
),
|
||||
)
|
||||
batch_request_type.update_forward_refs()
|
||||
batch_request_type.model_rebuild()
|
||||
return batch_request_type
|
||||
|
||||
|
||||
@@ -187,7 +182,7 @@ def create_stream_log_request_model(
|
||||
),
|
||||
kwargs=(dict, Field(default_factory=dict)),
|
||||
)
|
||||
stream_log_request.update_forward_refs()
|
||||
stream_log_request.model_rebuild()
|
||||
return stream_log_request
|
||||
|
||||
|
||||
@@ -245,7 +240,7 @@ def create_stream_events_request_model(
|
||||
),
|
||||
kwargs=(dict, Field(default_factory=dict)),
|
||||
)
|
||||
stream_events_request.update_forward_refs()
|
||||
stream_events_request.model_rebuild()
|
||||
return stream_events_request
|
||||
|
||||
|
||||
@@ -297,7 +292,7 @@ def create_invoke_response_model(
|
||||
__base__=InvokeBaseResponse,
|
||||
**fields,
|
||||
)
|
||||
invoke_response_type.update_forward_refs()
|
||||
invoke_response_type.model_rebuild()
|
||||
return invoke_response_type
|
||||
|
||||
|
||||
@@ -347,7 +342,7 @@ def create_batch_response_model(
|
||||
__base__=BatchBaseResponse,
|
||||
**fields,
|
||||
)
|
||||
batch_response_type.update_forward_refs()
|
||||
batch_response_type.model_rebuild()
|
||||
return batch_response_type
|
||||
|
||||
|
||||
@@ -566,8 +561,8 @@ class OnRetrieverEnd(BaseModel):
|
||||
type: Literal["on_retriever_end"] = "on_retriever_end"
|
||||
|
||||
|
||||
class CallbackEvent(BaseModel):
|
||||
__root__: Union[
|
||||
CallbackEvent = RootModel[
|
||||
Union[
|
||||
OnChainStart,
|
||||
OnChainEnd,
|
||||
OnChainError,
|
||||
@@ -581,3 +576,4 @@ class CallbackEvent(BaseModel):
|
||||
OnRetrieverEnd,
|
||||
OnRetrieverError,
|
||||
]
|
||||
]
|
||||
|
||||
Generated
+1462
-1331
File diff suppressed because it is too large
Load Diff
+10
-5
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langserve"
|
||||
version = "0.1.0"
|
||||
version = "0.3.0dev2"
|
||||
description = ""
|
||||
readme = "README.md"
|
||||
authors = ["LangChain"]
|
||||
@@ -10,13 +10,14 @@ exclude = ["langserve/playground,langserve/chat_playground"]
|
||||
include = ["langserve/playground/dist/**/*", "langserve/chat_playground/dist/**/*"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.8.1"
|
||||
python = "^3.9"
|
||||
httpx = ">=0.23.0" # May be able to decrease this version
|
||||
fastapi = {version = ">=0.90.1,<1", optional = true}
|
||||
sse-starlette = {version = "^1.3.0", optional = true}
|
||||
pydantic = ">=1"
|
||||
langchain-core = "^0.1.0"
|
||||
langchain-core = "0.3.0dev5"
|
||||
orjson = ">=2"
|
||||
pyproject-toml = "^0.0.10"
|
||||
pydantic = "^2.7"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
jupyterlab = "^3.6.1"
|
||||
@@ -46,7 +47,7 @@ sse-starlette = "^1.3.0"
|
||||
|
||||
[tool.poetry.extras]
|
||||
# Extras that are used for client
|
||||
client = []
|
||||
client = ["fastapi"]
|
||||
# Extras that are used for server
|
||||
server = ["sse-starlette", "fastapi"]
|
||||
# All
|
||||
@@ -94,3 +95,7 @@ addopts = "--strict-markers --strict-config --durations=5 -vv"
|
||||
# take more than 5 seconds
|
||||
timeout = 5
|
||||
asyncio_mode = "auto"
|
||||
filterwarnings = [
|
||||
"ignore::langchain_core._api.beta_decorator.LangChainBetaWarning",
|
||||
]
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Test the playground API."""
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from httpx import AsyncClient
|
||||
from langchain_core.runnables import RunnableLambda
|
||||
@@ -15,7 +16,9 @@ async def test_serve_playground() -> None:
|
||||
RunnableLambda(lambda foo: "hello"),
|
||||
)
|
||||
|
||||
async with AsyncClient(app=app, base_url="http://localhost:9999") as client:
|
||||
async with AsyncClient(
|
||||
base_url="http://localhost:9999", transport=httpx.ASGITransport(app=app)
|
||||
) as client:
|
||||
response = await client.get("/playground/index.html")
|
||||
assert response.status_code == 200
|
||||
# Test that we can't access files that do not exist.
|
||||
@@ -42,7 +45,9 @@ async def test_serve_playground_with_api_router() -> None:
|
||||
|
||||
app.include_router(router)
|
||||
|
||||
async with AsyncClient(app=app, base_url="http://localhost:9999") as client:
|
||||
async with AsyncClient(
|
||||
base_url="http://localhost:9999", transport=httpx.ASGITransport(app=app)
|
||||
) as client:
|
||||
response = await client.get("/langserve_runnables/chat/playground/index.html")
|
||||
assert response.status_code == 200
|
||||
|
||||
@@ -64,7 +69,9 @@ async def test_serve_playground_with_api_router_in_api_router() -> None:
|
||||
# Now add parent router to the app
|
||||
app.include_router(parent_router)
|
||||
|
||||
async with AsyncClient(app=app, base_url="http://localhost:9999") as client:
|
||||
async with AsyncClient(
|
||||
base_url="http://localhost:9999", transport=httpx.ASGITransport(app=app)
|
||||
) as client:
|
||||
response = await client.get("/parent/bar/foo/playground/index.html")
|
||||
assert response.status_code == 200
|
||||
|
||||
@@ -88,7 +95,9 @@ async def test_root_path_on_playground() -> None:
|
||||
)
|
||||
app.include_router(router)
|
||||
|
||||
async_client = AsyncClient(app=app, base_url="http://localhost:9999")
|
||||
async_client = AsyncClient(
|
||||
base_url="http://localhost:9999", transport=httpx.ASGITransport(app=app)
|
||||
)
|
||||
|
||||
response = await async_client.get("/chat/playground/index.html")
|
||||
assert response.status_code == 200
|
||||
|
||||
@@ -4,15 +4,23 @@ from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from langchain_core.documents.base import Document
|
||||
from langchain_core.messages import HumanMessage, HumanMessageChunk, SystemMessage
|
||||
from langchain_core.outputs import ChatGeneration
|
||||
from pydantic import BaseModel
|
||||
|
||||
try:
|
||||
from pydantic.v1 import BaseModel
|
||||
except ImportError:
|
||||
from pydantic import BaseModel
|
||||
from langserve.serialization import (
|
||||
WellKnownLCObject,
|
||||
WellKnownLCSerializer,
|
||||
load_events,
|
||||
)
|
||||
|
||||
from langserve.serialization import WellKnownLCSerializer, load_events
|
||||
|
||||
def test_document_serialization() -> None:
|
||||
"""Simple test. Exhaustive tests follow below."""
|
||||
doc = Document(page_content="hello")
|
||||
d = doc.model_dump()
|
||||
WellKnownLCObject.model_validate(d)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -23,6 +31,8 @@ from langserve.serialization import WellKnownLCSerializer, load_events
|
||||
[],
|
||||
{},
|
||||
{"a": 1},
|
||||
Document(page_content="Hello"),
|
||||
[Document(page_content="Hello")],
|
||||
{"output": [HumanMessage(content="hello")]},
|
||||
# Test with a single message (HumanMessage)
|
||||
HumanMessage(content="Hello"),
|
||||
@@ -77,7 +87,7 @@ def _get_full_representation(data: Any) -> Any:
|
||||
elif isinstance(data, list):
|
||||
return [_get_full_representation(value) for value in data]
|
||||
elif isinstance(data, BaseModel):
|
||||
return data.schema()
|
||||
return data.model_json_schema()
|
||||
else:
|
||||
return data
|
||||
|
||||
@@ -179,3 +189,9 @@ def test_encoding_of_well_known_types(obj: Any, expected: str) -> None:
|
||||
"""
|
||||
lc_serializer = WellKnownLCSerializer()
|
||||
assert lc_serializer.dumpd(obj) == expected
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason="0.3")
|
||||
def test_fail_03() -> None:
|
||||
"""This test will fail on purposes. It contains a TODO list for 0.3 release."""
|
||||
assert "CHatGeneration_Deserialized correct" == "UNcomment test above"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,14 +5,9 @@ import pytest
|
||||
from fastapi import Request
|
||||
from langchain_core.prompts import PromptTemplate
|
||||
from langchain_core.runnables import ConfigurableField
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
from langserve.api_handler import _unpack_request_config
|
||||
|
||||
try:
|
||||
from pydantic.v1 import BaseModel, ValidationError
|
||||
except ImportError:
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
from langserve.validation import (
|
||||
create_batch_request_model,
|
||||
create_invoke_request_model,
|
||||
@@ -175,11 +170,11 @@ async def test_invoke_request_with_runnables() -> None:
|
||||
"configurable": {"template": "goodbye {name}"},
|
||||
},
|
||||
)
|
||||
assert request.input == {"name": "bob"}
|
||||
assert dict(request.input) == {"name": "bob"}
|
||||
assert request.config.tags == ["hello"]
|
||||
assert request.config.run_name == "run"
|
||||
assert isinstance(request.config.configurable, BaseModel)
|
||||
assert request.config.configurable.dict() == {
|
||||
assert request.config.configurable.model_dump() == {
|
||||
"template": "goodbye {name}",
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
from typing import Any
|
||||
|
||||
|
||||
def recursive_dump(obj: Any) -> Any:
|
||||
"""Recursively dump the object if encountering any pydantic models."""
|
||||
if isinstance(obj, dict):
|
||||
return {
|
||||
k: recursive_dump(v)
|
||||
for k, v in obj.items()
|
||||
if k != "id" # Remove the id field for testing purposes
|
||||
}
|
||||
if isinstance(obj, list):
|
||||
return [recursive_dump(v) for v in obj]
|
||||
if hasattr(obj, "model_dump"):
|
||||
# if the object contains an ID field, we'll remove it for testing purposes
|
||||
d = obj.model_dump()
|
||||
if "id" in d:
|
||||
d.pop("id")
|
||||
return recursive_dump(d)
|
||||
if hasattr(obj, "dict"):
|
||||
# if the object contains an ID field, we'll remove it for testing purposes
|
||||
if hasattr(obj, "id"):
|
||||
d = obj.dict()
|
||||
d.pop("id")
|
||||
return recursive_dump(d)
|
||||
return recursive_dump(obj.dict())
|
||||
return obj
|
||||
@@ -0,0 +1,22 @@
|
||||
from typing import Any
|
||||
|
||||
from langchain_core.messages import AIMessage, AIMessageChunk
|
||||
|
||||
|
||||
class AnyStr(str):
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return isinstance(other, str)
|
||||
|
||||
|
||||
def _AnyIdAIMessage(**kwargs: Any) -> AIMessage:
|
||||
"""Create ai message with an any id field."""
|
||||
message = AIMessage(**kwargs)
|
||||
message.id = AnyStr()
|
||||
return message
|
||||
|
||||
|
||||
def _AnyIdAIMessageChunk(**kwargs: Any) -> AIMessageChunk:
|
||||
"""Create ai message with an any id field."""
|
||||
message = AIMessageChunk(**kwargs)
|
||||
message.id = AnyStr()
|
||||
return message
|
||||
@@ -4,10 +4,11 @@ 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.messages import AIMessage, BaseMessage
|
||||
from langchain_core.outputs import ChatGenerationChunk, GenerationChunk
|
||||
|
||||
from tests.unit_tests.utils.llms import GenericFakeChatModel
|
||||
from tests.unit_tests.utils.stubs import _AnyIdAIMessage, _AnyIdAIMessageChunk
|
||||
|
||||
|
||||
def test_generic_fake_chat_model_invoke() -> None:
|
||||
@@ -15,11 +16,11 @@ def test_generic_fake_chat_model_invoke() -> None:
|
||||
infinite_cycle = cycle([AIMessage(content="hello"), AIMessage(content="goodbye")])
|
||||
model = GenericFakeChatModel(messages=infinite_cycle)
|
||||
response = model.invoke("meow")
|
||||
assert response == AIMessage(content="hello")
|
||||
assert response == _AnyIdAIMessage(content="hello")
|
||||
response = model.invoke("kitty")
|
||||
assert response == AIMessage(content="goodbye")
|
||||
assert response == _AnyIdAIMessage(content="goodbye")
|
||||
response = model.invoke("meow")
|
||||
assert response == AIMessage(content="hello")
|
||||
assert response == _AnyIdAIMessage(content="hello")
|
||||
|
||||
|
||||
async def test_generic_fake_chat_model_ainvoke() -> None:
|
||||
@@ -27,11 +28,11 @@ async def test_generic_fake_chat_model_ainvoke() -> None:
|
||||
infinite_cycle = cycle([AIMessage(content="hello"), AIMessage(content="goodbye")])
|
||||
model = GenericFakeChatModel(messages=infinite_cycle)
|
||||
response = await model.ainvoke("meow")
|
||||
assert response == AIMessage(content="hello")
|
||||
assert response == _AnyIdAIMessage(content="hello")
|
||||
response = await model.ainvoke("kitty")
|
||||
assert response == AIMessage(content="goodbye")
|
||||
assert response == _AnyIdAIMessage(content="goodbye")
|
||||
response = await model.ainvoke("meow")
|
||||
assert response == AIMessage(content="hello")
|
||||
assert response == _AnyIdAIMessage(content="hello")
|
||||
|
||||
|
||||
async def test_generic_fake_chat_model_stream() -> None:
|
||||
@@ -44,26 +45,26 @@ async def test_generic_fake_chat_model_stream() -> None:
|
||||
model = GenericFakeChatModel(messages=infinite_cycle)
|
||||
chunks = [chunk async for chunk in model.astream("meow")]
|
||||
assert chunks == [
|
||||
AIMessageChunk(content="hello"),
|
||||
AIMessageChunk(content=" "),
|
||||
AIMessageChunk(content="goodbye"),
|
||||
_AnyIdAIMessageChunk(content="hello"),
|
||||
_AnyIdAIMessageChunk(content=" "),
|
||||
_AnyIdAIMessageChunk(content="goodbye"),
|
||||
]
|
||||
|
||||
chunks = [chunk for chunk in model.stream("meow")]
|
||||
assert chunks == [
|
||||
AIMessageChunk(content="hello"),
|
||||
AIMessageChunk(content=" "),
|
||||
AIMessageChunk(content="goodbye"),
|
||||
_AnyIdAIMessageChunk(content="hello"),
|
||||
_AnyIdAIMessageChunk(content=" "),
|
||||
_AnyIdAIMessageChunk(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})
|
||||
message = AIMessage(content="", additional_kwargs={"foo": 42, "bar": 24}, id="1")
|
||||
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}),
|
||||
_AnyIdAIMessageChunk(content="", additional_kwargs={"foo": 42}),
|
||||
_AnyIdAIMessageChunk(content="", additional_kwargs={"bar": 24}),
|
||||
]
|
||||
|
||||
message = AIMessage(
|
||||
@@ -80,19 +81,21 @@ async def test_generic_fake_chat_model_stream() -> None:
|
||||
chunks = [chunk async for chunk in model.astream("meow")]
|
||||
|
||||
assert chunks == [
|
||||
AIMessageChunk(
|
||||
content="", additional_kwargs={"function_call": {"name": "move_file"}}
|
||||
_AnyIdAIMessageChunk(
|
||||
content="",
|
||||
additional_kwargs={"function_call": {"name": "move_file"}},
|
||||
),
|
||||
AIMessageChunk(
|
||||
_AnyIdAIMessageChunk(
|
||||
content="",
|
||||
additional_kwargs={
|
||||
"function_call": {"arguments": '{\n "source_path": "foo"'}
|
||||
},
|
||||
),
|
||||
AIMessageChunk(
|
||||
content="", additional_kwargs={"function_call": {"arguments": ","}}
|
||||
_AnyIdAIMessageChunk(
|
||||
content="",
|
||||
additional_kwargs={"function_call": {"arguments": ","}},
|
||||
),
|
||||
AIMessageChunk(
|
||||
_AnyIdAIMessageChunk(
|
||||
content="",
|
||||
additional_kwargs={
|
||||
"function_call": {"arguments": '\n "destination_path": "bar"\n}'}
|
||||
@@ -107,7 +110,7 @@ async def test_generic_fake_chat_model_stream() -> None:
|
||||
else:
|
||||
accumulate_chunks += chunk
|
||||
|
||||
assert accumulate_chunks == AIMessageChunk(
|
||||
assert accumulate_chunks == _AnyIdAIMessageChunk(
|
||||
content="",
|
||||
additional_kwargs={
|
||||
"function_call": {
|
||||
@@ -128,9 +131,9 @@ async def test_generic_fake_chat_model_astream_log() -> None:
|
||||
]
|
||||
final = log_patches[-1]
|
||||
assert final.state["streamed_output"] == [
|
||||
AIMessageChunk(content="hello"),
|
||||
AIMessageChunk(content=" "),
|
||||
AIMessageChunk(content="goodbye"),
|
||||
_AnyIdAIMessageChunk(content="hello"),
|
||||
_AnyIdAIMessageChunk(content=" "),
|
||||
_AnyIdAIMessageChunk(content="goodbye"),
|
||||
]
|
||||
|
||||
|
||||
@@ -178,8 +181,8 @@ async def test_callback_handlers() -> None:
|
||||
# New model
|
||||
results = list(model.stream("meow", {"callbacks": [MyCustomAsyncHandler(tokens)]}))
|
||||
assert results == [
|
||||
AIMessageChunk(content="hello"),
|
||||
AIMessageChunk(content=" "),
|
||||
AIMessageChunk(content="goodbye"),
|
||||
_AnyIdAIMessageChunk(content="hello"),
|
||||
_AnyIdAIMessageChunk(content=" "),
|
||||
_AnyIdAIMessageChunk(content="goodbye"),
|
||||
]
|
||||
assert tokens == ["hello", " ", "goodbye"]
|
||||
|
||||
Reference in New Issue
Block a user