Compare commits

..

1 Commits

Author SHA1 Message Date
Eugene Yurtsev c2bff4dfea Bump version 2024-03-08 16:31:34 -05:00
115 changed files with 3345 additions and 21372 deletions
+1 -1
View File
@@ -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", "ccurme", "dennisrall"],
"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"],
"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."
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

@@ -0,0 +1,94 @@
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'
+7
View File
@@ -39,6 +39,13 @@ 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
-1
View File
@@ -10,5 +10,4 @@ jobs:
./.github/workflows/_release.yml
with:
working-directory: .
permissions: write-all
secrets: inherit
-29
View File
@@ -49,32 +49,3 @@ To run linting for this project:
```sh
make lint
```
## Frontend Playground Development
Here are a few tips to keep in mind when developing the LangServe playgrounds:
### Setup
Switch directories to `langserve/playground` or `langserve/chat_playground`, then run `yarn` to install required
dependencies. `yarn dev` will start the playground at `http://localhost:5173/____LANGSERVE_BASE_URL/` in dev mode.
You can run one of the chains in the `examples/` repo using `poetry run python path/to/file.py`.
### Setting CORS
You may need to add the following to an example route when developing the playground in dev mode to handle CORS:
```python
from fastapi.middleware.cors import CORSMiddleware
# Set all CORS enabled origins
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
expose_headers=["*"],
)
```
+83 -153
View File
@@ -5,6 +5,10 @@
[![Open Issues](https://img.shields.io/github/issues-raw/langchain-ai/langserve)](https://github.com/langchain-ai/langserve/issues)
[![](https://dcbadge.vercel.app/api/server/6adMQxSpJS?compact=true&style=flat)](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/app0hN6sd93QcKubv/shrAjst60xXa6quV2)
to get on the waitlist.
## Overview
[LangServe](https://github.com/langchain-ai/langserve) helps developers
@@ -24,11 +28,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 `/stream_events` to make it easier to stream without needing to parse the output of `/stream_log`.
- **new** as of 0.0.40, supports `astream_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/))
@@ -38,13 +42,6 @@ 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
@@ -52,9 +49,16 @@ be better suited for deploying LangGraph applications.
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).
@@ -76,38 +80,8 @@ 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
langchain app new ../path/to/directory
```
## Examples
@@ -121,24 +95,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/client.ipynb), [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
@@ -163,17 +137,17 @@ app = FastAPI(
add_routes(
app,
ChatOpenAI(model="gpt-3.5-turbo-0125"),
ChatOpenAI(),
path="/openai",
)
add_routes(
app,
ChatAnthropic(model="claude-3-haiku-20240307"),
ChatAnthropic(),
path="/anthropic",
)
model = ChatAnthropic(model="claude-3-haiku-20240307")
model = ChatAnthropic()
prompt = ChatPromptTemplate.from_template("tell me a joke about {topic}")
add_routes(
app,
@@ -208,8 +182,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
@@ -269,10 +243,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",
});
```
@@ -321,7 +295,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
@@ -356,70 +330,6 @@ runnable and share a link with the configuration:
<img src="https://github.com/langchain-ai/langserve/assets/3205522/86ce9c59-f8e4-4d08-9fa3-62030e0f521d" width="50%"/>
</p>
## Chat playground
LangServe also supports a chat-focused playground that opt into and use under `/my_runnable/playground/`.
Unlike the general playground, only certain types of runnables are supported - the runnable's input schema must
be a `dict` with either:
- a single key, and that key's value must be a list of chat messages.
- two keys, one whose value is a list of messages, and the other representing the most recent message.
We recommend you use the first format.
The runnable must also return either an `AIMessage` or a string.
To enable it, you must set `playground_type="chat",` when adding your route. Here's an example:
```python
# Declare a chain
prompt = ChatPromptTemplate.from_messages(
[
("system", "You are a helpful, professional assistant named Cob."),
MessagesPlaceholder(variable_name="messages"),
]
)
chain = prompt | ChatAnthropic(model="claude-2")
class InputChat(BaseModel):
"""Input for the chat endpoint."""
messages: List[Union[HumanMessage, AIMessage, SystemMessage]] = Field(
...,
description="The chat messages representing the current conversation.",
)
add_routes(
app,
chain.with_types(input_type=InputChat),
enable_feedback_endpoint=True,
enable_public_trace_link_endpoint=True,
playground_type="chat",
)
```
If you are using LangSmith, you can also set `enable_feedback_endpoint=True` on your route to enable thumbs-up/thumbs-down buttons
after each message, and `enable_public_trace_link_endpoint=True` to add a button that creates a public traces for runs.
Note that you will also need to set the following environment variables:
```bash
export LANGCHAIN_TRACING_V2="true"
export LANGCHAIN_PROJECT="YOUR_PROJECT_NAME"
export LANGCHAIN_API_KEY="YOUR_API_KEY"
```
Here's an example with the above two options turned on:
<p align="center">
<img src="./.github/img/chat_playground.png" width="50%"/>
</p>
Note: If you enable public trace links, the internals of your chain will be exposed. We recommend only using this setting
for demos or testing.
## Legacy Chains
LangServe works with both Runnables (constructed
@@ -462,6 +372,27 @@ 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 | [![Deploy](https://get.pulumi.com/new/button.svg)](https://app.pulumi.com/new?template=https://github.com/pulumi/examples/aws-cs-langserve) |
| AWS | golang | https://github.com/pulumi/examples/aws-go-langserve | [![Deploy](https://get.pulumi.com/new/button.svg)](https://app.pulumi.com/new?template=https://github.com/pulumi/examples/aws-go-langserve) |
| AWS | python | https://github.com/pulumi/examples/aws-py-langserve | [![Deploy](https://get.pulumi.com/new/button.svg)](https://app.pulumi.com/new?template=https://github.com/pulumi/examples/aws-py-langserve) |
| AWS | typescript | https://github.com/pulumi/examples/aws-ts-langserve | [![Deploy](https://get.pulumi.com/new/button.svg)](https://app.pulumi.com/new?template=https://github.com/pulumi/examples/aws-ts-langserve) |
| AWS | javascript | https://github.com/pulumi/examples/aws-js-langserve | [![Deploy](https://get.pulumi.com/new/button.svg)](https://app.pulumi.com/new?template=https://github.com/pulumi/examples/aws-js-langserve) |
### Community Contributed
#### Deploy to Railway
@@ -475,7 +406,7 @@ gcloud run deploy [your-service-name] --source . --port 8001 --allow-unauthentic
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]. To fix this, use `pip install pydantic==1.10.17`.
Pydantic V2. Fast API does not support [mixing pydantic v1 and v2 namespaces].
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)
@@ -492,7 +423,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/).
@@ -501,11 +432,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/).
@@ -525,10 +456,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.
@@ -597,8 +528,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.
@@ -637,10 +568,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
@@ -656,8 +587,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;
};
```
@@ -715,9 +646,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:
@@ -757,7 +688,6 @@ 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>
+3 -58
View File
@@ -1,61 +1,6 @@
# Security Policy
## Reporting OSS Vulnerabilities
## Reporting a Vulnerability
LangChain is partnered with [huntr by Protect AI](https://huntr.com/) to provide
a bounty program for our open source projects.
Please report security vulnerabilities associated with the LangChain
open source projects by visiting the following link:
[https://huntr.com/bounties/disclose/](https://huntr.com/bounties/disclose/?target=https%3A%2F%2Fgithub.com%2Flangchain-ai%2Flangchain&validSearch=true)
Before reporting a vulnerability, please review:
1) In-Scope Targets and Out-of-Scope Targets below.
2) The [langchain-ai/langchain](https://python.langchain.com/docs/contributing/repo_structure) monorepo structure.
3) LangChain [security guidelines](https://python.langchain.com/docs/security) to
understand what we consider to be a security vulnerability vs. developer
responsibility.
### In-Scope Targets
The following packages and repositories are eligible for bug bounties:
- langchain-core
- langchain (see exceptions)
- langchain-community (see exceptions)
- langgraph
- langserve
### Out of Scope Targets
All out of scope targets defined by huntr as well as:
- **langchain-experimental**: This repository is for experimental code and is not
eligible for bug bounties, bug reports to it will be marked as interesting or waste of
time and published with no bounty attached.
- **tools**: Tools in either langchain or langchain-community are not eligible for bug
bounties. This includes the following directories
- langchain/tools
- langchain-community/tools
- Please review our [security guidelines](https://python.langchain.com/docs/security)
for more details, but generally tools interact with the real world. Developers are
expected to understand the security implications of their code and are responsible
for the security of their tools.
- Code documented with security notices. This will be decided done on a case by
case basis, but likely will not be eligible for a bounty as the code is already
documented with guidelines for developers that should be followed for making their
application secure.
- Any LangSmith related repositories or APIs see below.
## Reporting LangSmith Vulnerabilities
Please report security vulnerabilities associated with LangSmith by email to `security@langchain.dev`.
- LangSmith site: https://smith.langchain.com
- SDK client: https://github.com/langchain-ai/langsmith-sdk
### Other Security Concerns
For any other security concerns, please contact us at `security@langchain.dev`.
Please report security vulnerabilities by email to `security@langchain.dev`.
This email is an alias to a subset of our maintainers, and will ensure the issue is promptly triaged and acted upon as needed.
+7 -7
View File
@@ -20,15 +20,15 @@ Relevant LangChain documentation:
from typing import Any
from fastapi import FastAPI
from langchain.agents import AgentExecutor
from langchain.agents import AgentExecutor, tool
from langchain.agents.format_scratchpad import format_to_openai_functions
from langchain.agents.output_parsers import OpenAIFunctionsAgentOutputParser
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 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 langserve import add_routes
+5 -5
View File
@@ -47,19 +47,19 @@ Relevant LangChain documentation:
from typing import Any, AsyncIterator, List, Literal
from fastapi import FastAPI
from langchain.agents import AgentExecutor
from langchain.agents import AgentExecutor, tool
from langchain.agents.format_scratchpad.openai_tools import (
format_to_openai_tool_messages,
)
from langchain.agents.output_parsers.openai_tools import OpenAIToolsAgentOutputParser
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
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.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(
[
+5 -5
View File
@@ -26,19 +26,19 @@ Relevant LangChain documentation:
from typing import Any, List, Union
from fastapi import FastAPI
from langchain.agents import AgentExecutor
from langchain.agents import AgentExecutor, tool
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, MessagesPlaceholder
from langchain_core.tools import tool
from langchain_core.utils.function_calling import format_tool_to_openai_tool
from langchain_core.prompts import ChatPromptTemplate
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(
[
+6 -5
View File
@@ -35,6 +35,7 @@ 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 (
@@ -43,11 +44,10 @@ 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,9 +150,10 @@ class PerUserVectorstore(RunnableSerializable):
user_id: Optional[str]
vectorstore: VectorStore
model_config = ConfigDict(
arbitrary_types_allowed=True,
)
class Config:
# Allow arbitrary types since VectorStore is an abstract interface
# and not a pydantic model
arbitrary_types_allowed = True
def _invoke(
self, input: str, config: Optional[RunnableConfig] = None, **kwargs: Any
@@ -36,6 +36,7 @@ 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 (
@@ -44,11 +45,10 @@ 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,9 +147,10 @@ class PerUserVectorstore(RunnableSerializable):
user_id: Optional[str]
vectorstore: VectorStore
model_config = ConfigDict(
arbitrary_types_allowed=True,
)
class Config:
# Allow arbitrary types since VectorStore is an abstract interface
# and not a pydantic model
arbitrary_types_allowed = True
def _invoke(
self, input: str, config: Optional[RunnableConfig] = None, **kwargs: Any
@@ -1,56 +0,0 @@
#!/usr/bin/env python
"""Example of a simple chatbot that just passes current conversation
state back and forth between server and client.
"""
from typing import List, Union
from fastapi import FastAPI
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
app = FastAPI(
title="LangChain Server",
version="1.0",
description="Spin up a simple api server using Langchain's Runnable interfaces",
)
# Declare a chain
prompt = ChatPromptTemplate.from_messages(
[
("system", "You are a helpful, professional assistant named Cob."),
MessagesPlaceholder(variable_name="messages"),
("human", "{input}"),
]
)
chain = prompt | ChatAnthropic(model="claude-2")
class InputChat(BaseModel):
"""Input for the chat endpoint."""
messages: List[Union[HumanMessage, AIMessage, SystemMessage]] = Field(
...,
description="The chat messages representing the current conversation.",
)
input: str
add_routes(
app,
chain.with_types(input_type=InputChat),
enable_feedback_endpoint=True,
enable_public_trace_link_endpoint=True,
playground_type="chat",
)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="localhost", port=8000)
-53
View File
@@ -1,53 +0,0 @@
#!/usr/bin/env python
"""Example of a simple chatbot that just passes current conversation
state back and forth between server and client.
"""
from typing import List, Union
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
app = FastAPI(
title="LangChain Server",
version="1.0",
description="Spin up a simple api server using Langchain's Runnable interfaces",
)
# Declare a chain
prompt = ChatPromptTemplate.from_messages(
[
("system", "You are a helpful, professional assistant named Cob."),
MessagesPlaceholder(variable_name="messages"),
]
)
chain = prompt | ChatAnthropic(model_name="claude-3-sonnet-20240229")
class InputChat(BaseModel):
"""Input for the chat endpoint."""
messages: List[Union[HumanMessage, AIMessage, SystemMessage]] = Field(
...,
description="The chat messages representing the current conversation.",
)
add_routes(
app,
chain.with_types(input_type=InputChat),
enable_feedback_endpoint=True,
enable_public_trace_link_endpoint=True,
playground_type="chat",
)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="localhost", port=8000)
+3 -3
View File
@@ -13,14 +13,14 @@ from pathlib import Path
from typing import Callable, Union
from fastapi import FastAPI, HTTPException
from langchain_anthropic import ChatAnthropic
from langchain_community.chat_message_histories import FileChatMessageHistory
from langchain.chat_models import ChatAnthropic
from langchain.memory 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:
@@ -13,13 +13,13 @@ from pathlib import Path
from typing import Any, Callable, Dict, Union
from fastapi import FastAPI, HTTPException, Request
from langchain_community.chat_message_histories import FileChatMessageHistory
from langchain.chat_models import ChatOpenAI
from langchain.memory import FileChatMessageHistory
from langchain.schema.runnable.utils import ConfigurableFieldSpec
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,11 +17,15 @@ 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
from langchain.agents import AgentExecutor, tool
from langchain.agents.format_scratchpad import format_to_openai_functions
from langchain.agents.output_parsers import OpenAIFunctionsAgentOutputParser
from langchain_community.vectorstores import FAISS
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
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_core.runnables import (
ConfigurableField,
ConfigurableFieldSpec,
@@ -29,10 +33,6 @@ 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
+4 -4
View File
@@ -10,10 +10,10 @@ from typing import Any, Dict
from fastapi import FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
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 langchain.chat_models import ChatOpenAI
from langchain.prompts import PromptTemplate
from langchain.schema.output_parser import StrOutputParser
from langchain.schema.runnable import ConfigurableField
from langserve import add_routes
+8 -9
View File
@@ -3,21 +3,20 @@
from typing import Any, Iterable, List, Optional, Type
from fastapi import FastAPI
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 (
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 (
ConfigurableFieldSingleOption,
RunnableConfig,
RunnableSerializable,
)
from langchain_core.vectorstores import VectorStore
from langchain_openai import OpenAIEmbeddings
from pydantic import BaseModel, Field
from langchain.schema.vectorstore import VST
from langchain.vectorstores import FAISS, VectorStore
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,14 +13,17 @@ from operator import itemgetter
from typing import List, Tuple
from fastapi import FastAPI
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 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 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.
+4 -4
View File
@@ -15,10 +15,10 @@ allowing one to upload a binary file using the langserve playground UI.
import base64
from fastapi import FastAPI
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 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 langserve import CustomUserType, add_routes
+3 -4
View File
@@ -2,8 +2,7 @@
"""Example LangChain server exposes multiple runnables (LLMs in this case)."""
from fastapi import FastAPI
from langchain_anthropic import ChatAnthropic
from langchain_openai import ChatOpenAI
from langchain.chat_models import ChatAnthropic, ChatOpenAI
from langserve import add_routes
@@ -15,12 +14,12 @@ app = FastAPI(
add_routes(
app,
ChatOpenAI(model="gpt-3.5-turbo-0125"),
ChatOpenAI(),
path="/openai",
)
add_routes(
app,
ChatAnthropic(model="claude-3-haiku-20240307"),
ChatAnthropic(),
path="/anthropic",
)
+4 -4
View File
@@ -4,9 +4,9 @@
from typing import Any, Callable, Dict, List, Optional, TypedDict
from fastapi import FastAPI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableParallel, RunnablePassthrough
from langchain_openai import ChatOpenAI
from langchain.chat_models import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
from langchain.schema.runnable import RunnableMap, RunnablePassthrough
from langserve import add_routes
@@ -43,7 +43,7 @@ model = ChatOpenAI()
underlying_chain = prompt | model
wrapped_chain = RunnableParallel(
wrapped_chain = RunnableMap(
{
"output": _create_projection(exclude_keys=["info"]) | underlying_chain,
"info": _create_projection(include_keys=["info"]),
+2 -2
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env python
"""Example LangChain server exposes a retriever."""
from fastapi import FastAPI
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import FAISS
from langserve import add_routes
+3 -4
View File
@@ -9,8 +9,7 @@ See more documentation at:
https://fastapi.tiangolo.com/tutorial/bigger-applications/
"""
from fastapi import APIRouter, FastAPI
from langchain_anthropic import ChatAnthropic
from langchain_openai import ChatOpenAI
from langchain.chat_models import ChatAnthropic, ChatOpenAI
from langserve import add_routes
@@ -21,13 +20,13 @@ router = APIRouter(prefix="/models")
# Invocations to this router will appear in trace logs as /models/openai
add_routes(
router,
ChatOpenAI(model="gpt-3.5-turbo-0125"),
ChatOpenAI(),
path="/openai",
)
# Invocations to this router will appear in trace logs as /models/anthropic
add_routes(
router,
ChatAnthropic(model="claude-3-haiku-20240307"),
ChatAnthropic(),
path="/anthropic",
)
+2 -2
View File
@@ -6,13 +6,13 @@ from typing import List, Union
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from langchain_anthropic import ChatAnthropic
from langchain.chat_models 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",
+7 -6
View File
@@ -6,17 +6,18 @@ from typing import Any, Dict, List, Tuple
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from langchain_community.document_loaders.parsers.pdf import PDFMinerParser
from langchain_core.document_loaders import Blob
from langchain_core.messages import (
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 (
AIMessage,
BaseMessage,
FunctionMessage,
HumanMessage,
)
from langchain_core.runnables import RunnableLambda, RunnableParallel
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field
from langchain.schema.runnable import RunnableLambda
from langchain_core.runnables import RunnableParallel
from langserve import CustomUserType
from langserve.server import add_routes
-53
View File
@@ -1,53 +0,0 @@
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)
+185 -425
View File
@@ -1,11 +1,9 @@
import asyncio
import contextlib
import importlib
import inspect
import json
import os
import re
import uuid
from inspect import isclass
from typing import (
Any,
@@ -14,7 +12,6 @@ from typing import (
Callable,
Dict,
Generator,
List,
Literal,
Mapping,
Optional,
@@ -27,39 +24,26 @@ from typing import (
from fastapi import HTTPException
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 (
get_config_list,
merge_configs,
run_in_executor,
)
from langchain_core.tracers import RunLogPatch
from langchain.callbacks.base import AsyncCallbackHandler
from langchain.callbacks.tracers.log_stream import RunLogPatch
from langchain.load.serializable import Serializable
from langchain.schema.runnable import Runnable, RunnableConfig
from langchain.schema.runnable.config import get_config_list, merge_configs
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,
Feedback,
FeedbackCreateRequest,
FeedbackCreateRequestTokenBased,
FeedbackToken,
InvokeResponseMetadata,
PublicTraceLink,
PublicTraceLinkCreateRequest,
SingletonResponseMetadata,
)
from langserve.serialization import WellKnownLCSerializer
from langserve.validation import (
@@ -120,39 +104,6 @@ def _strip_internal_keys(metadata: Dict[str, Any]) -> Dict[str, Any]:
return {k: v for k, v in metadata.items() if not k.startswith("__")}
def _create_metadata_event(
run_id: Optional[uuid.UUID] = None,
feedback_key: Optional[str] = None,
feedback_ingest_token: Optional[FeedbackIngestToken] = None,
) -> Dict[str, Any]:
"""Create a metadata event with the given type and metadata."""
data = {
"run_id": str(run_id) if run_id else None,
}
if feedback_ingest_token:
if not feedback_key:
raise ValueError("Feedback key must be provided if feedback token is given")
if feedback_ingest_token.expires_at:
expires_at = feedback_ingest_token.expires_at.isoformat()
else:
expires_at = None
data["feedback_tokens"] = [
{
"key": feedback_key,
"token_url": feedback_ingest_token.url,
"expires_at": expires_at,
}
]
metadata = {
"event": "metadata",
"data": json.dumps(data),
}
return metadata
async def _unpack_request_config(
*client_sent_configs: Union[BaseModel, Mapping, str],
config_keys: Sequence[str],
@@ -257,34 +208,25 @@ def _update_config_with_defaults(
}
metadata.update(hosted_metadata)
non_overridable_default_config: RunnableConfig = {
"metadata": metadata,
}
if run_name:
non_overridable_default_config["run_name"] = run_name
non_overridable_default_config = RunnableConfig(
run_name=run_name,
metadata=metadata,
)
# merge_configs is last-writer-wins, so we specifically pass in the
# overridable configs first, then the user provided configs, then
# finally the non-overridable configs
config = merge_configs(
return merge_configs(
overridable_default_config,
incoming_config,
non_overridable_default_config,
)
# run_id may have been set by user (and accepted by server) or
# it may have been by the user on the server request path.
# If it's not set, we'll generate a new one.
if "run_id" not in config or config["run_id"] is None:
config["run_id"] = str(uuid.uuid4())
return config
def _unpack_input(validated_model: BaseModel) -> Any:
"""Unpack the decoded input from the validated model."""
if isinstance(validated_model, RootModel):
model = validated_model.root
if hasattr(validated_model, "__root__"):
model = validated_model.__root__
else:
model = validated_model
@@ -308,7 +250,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.model_config,
__config__=model.__config__,
**{
fieldname: (
_rename_pydantic_model(field.annotation, prefix)
@@ -317,10 +259,10 @@ def _rename_pydantic_model(model: Type[BaseModel], prefix: str) -> Type[BaseMode
Field(
field.default,
title=fieldname,
description=field.description,
description=field.field_info.description,
),
)
for fieldname, field in model.model_fields.items()
for fieldname, field in model.__fields__.items()
},
)
@@ -337,7 +279,7 @@ def _resolve_model(
if isclass(type_) and issubclass(type_, BaseModel):
model = type_
else:
model = _create_root_model(default_name, type_)
model = create_model(default_name, __root__=(type_, ...))
hash_ = model.schema_json()
@@ -370,7 +312,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)
model_with_unique_name.model_rebuild()
model_with_unique_name.update_forward_refs()
return model_with_unique_name
@@ -403,7 +345,24 @@ def _with_validation_error_translation() -> Generator[None, None, None]:
try:
yield
except ValidationError as e:
raise RequestValidationError(e.errors())
raise RequestValidationError(e.errors(), body=e.model)
def _get_base_run_id_as_str(
event_aggregator: AsyncEventAggregatorCallback,
) -> Optional[str]:
"""
Uses `event_aggregator` to determine the base run ID for a given run. Returns
the run_id as a string, or None if it does not exist.
"""
# The first run in the callback_events list corresponds to the
# overall trace for request
if event_aggregator.callback_events and event_aggregator.callback_events[0].get(
"run_id"
):
return str(event_aggregator.callback_events[0].get("run_id"))
else:
raise AssertionError("No run_id found for the given run")
def _json_encode_response(model: BaseModel) -> JSONResponse:
@@ -423,27 +382,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,8 +410,11 @@ 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)}"
@@ -467,35 +429,14 @@ def _add_callbacks(
"""Add the callback aggregator to the config."""
if "callbacks" not in config:
config["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)
config["callbacks"].extend(callbacks)
_MODEL_REGISTRY = {}
_SEEN_NAMES = set()
class PerKeyFeedbackConfig(TypedDict):
"""Per feedback configuration.
Use to configure the feedback token.
"""
key: str
# PUBLIC API
class TokenFeedbackConfig(TypedDict):
"""Token feedback configuration.
This is used to configure the feedback tokens.
"""
key_configs: List[PerKeyFeedbackConfig]
class APIHandler:
@@ -521,14 +462,8 @@ class APIHandler:
config_keys: Sequence[str] = ("configurable",),
include_callback_events: bool = False,
enable_feedback_endpoint: bool = False,
# token feedback config configures **token** based feedback which
# is different from the feedback endpoint.
# Read the documentation for more details
token_feedback_config: Optional[TokenFeedbackConfig] = None,
enable_public_trace_link_endpoint: bool = False,
per_req_config_modifier: Optional[PerRequestConfigModifier] = None,
stream_log_name_allow_list: Optional[Sequence[str]] = None,
playground_type: Literal["default", "chat"] = "default",
) -> None:
"""Create an API handler for the given runnable.
@@ -567,18 +502,6 @@ class APIHandler:
to LangSmith. Disabled by default. If this flag is disabled or LangSmith
tracing is not enabled for the runnable, then 4xx errors will be thrown
when accessing the feedback endpoint
**Attention** this is distinct from `token_feedback_config`.
token_feedback_config: optional configuration for token based feedback.
**Attention** this is distinct from `enable_feedback_endpoint`.
When provided, feedback tokens will be included in the response
metadata that can be used to provide feedback on the run.
enable_public_trace_link_endpoint: Whether to enable an endpoint for
end-users to publicly view LangSmith traces of your chain runs.
WARNING: THIS WILL EXPOSE THE INTERNAL STATE OF YOUR RUN AND CHAIN AS
A PUBLICY ACCESSIBLE LINK.
If this flag is disabled or LangSmith tracing is not enabled for
the runnable, then 400 errors will be thrown when accessing the
endpoint.
per_req_config_modifier: optional function that can be used to update the
RunnableConfig for a given run based on the raw request. This is useful,
for example, if the user wants to pass in a header containing
@@ -625,30 +548,13 @@ class APIHandler:
self._per_req_config_modifier = per_req_config_modifier
self._serializer = WellKnownLCSerializer()
self._enable_feedback_endpoint = enable_feedback_endpoint
self._enable_public_trace_link_endpoint = enable_public_trace_link_endpoint
self._names_in_stream_allow_list = stream_log_name_allow_list
if token_feedback_config:
if len(token_feedback_config["key_configs"]) != 1:
raise NotImplementedError(
"Only one key is supported for now for token feedback "
"configuration. For example specify: "
"{'key_configs': [{'key': 'correctness'}]}"
)
warn_beta(
message="Token feedback is in beta. This API may change in the future."
)
self._token_feedback_config = token_feedback_config
self._token_feedback_enabled = token_feedback_config is not None
# Client is patched using mock.patch, if changing the names
# remember to make relevant updates in the unit tests.
self._langsmith_client = (
ls_client.Client()
if tracing_is_enabled()
and (enable_feedback_endpoint or self._token_feedback_enabled)
if tracing_is_enabled() and enable_feedback_endpoint
else None
)
@@ -699,12 +605,9 @@ class APIHandler:
)
# Generate the response models
self._InvokeResponse = create_invoke_response_model(
model_namespace, output_type_, include_callback_events
model_namespace, output_type_
)
self._BatchResponse = create_batch_response_model(
model_namespace, output_type_, include_callback_events
)
self.playground_type = playground_type
self._BatchResponse = create_batch_response_model(model_namespace, output_type_)
@property
def InvokeRequest(self) -> Type[BaseModel]:
@@ -807,33 +710,10 @@ class APIHandler:
endpoint="invoke",
server_config=server_config,
)
run_id = config["run_id"]
event_aggregator = AsyncEventAggregatorCallback()
_add_callbacks(config, [event_aggregator])
invoke_coro = self._runnable.ainvoke(
input_,
config=config,
)
feedback_key: Optional[str]
# If there's feedback enabled, let's create a presigned feedback token
if self._token_feedback_enabled:
feedback_key = self._token_feedback_config["key_configs"][0]["key"]
feedback_coro = run_in_executor(
None,
self._langsmith_client.create_presigned_feedback_token,
run_id,
feedback_key,
)
output, feedback_token = await asyncio.gather(invoke_coro, feedback_coro)
else:
feedback_key = None
output = await invoke_coro
feedback_token = None
output = await self._runnable.ainvoke(input_, config=config)
if self._include_callback_events:
callback_events = [
@@ -849,17 +729,8 @@ class APIHandler:
# Callbacks are scrubbed and exceptions are converted to
# serializable format before returned in the response.
callback_events=callback_events,
metadata=InvokeResponseMetadata(
run_id=run_id,
feedback_tokens=[
FeedbackToken(
key=feedback_key,
token_url=feedback_token.url,
expires_at=feedback_token.expires_at.isoformat(),
)
]
if feedback_token
else [],
metadata=SingletonResponseMetadata(
run_id=_get_base_run_id_as_str(event_aggregator)
),
),
)
@@ -955,31 +826,7 @@ class APIHandler:
)
)
run_ids = [config["run_id"] for config in final_configs]
batch_coro = self._runnable.abatch(inputs, config=final_configs)
feedback_key: Optional[str]
# If there's feedback enabled, let's create a presigned feedback token
if self._token_feedback_enabled:
feedback_key = self._token_feedback_config["key_configs"][0]["key"]
feedback_coros = [
run_in_executor(
None,
self._langsmith_client.create_presigned_feedback_token,
run_id,
feedback_key,
)
for run_id in run_ids
]
response = await asyncio.gather(batch_coro, *feedback_coros)
output = response[0]
feedback_tokens = response[1:]
else:
feedback_key = None
output = await batch_coro
feedback_tokens = []
output = await self._runnable.abatch(inputs, config=final_configs)
if self._include_callback_events:
callback_events = [
@@ -994,35 +841,13 @@ class APIHandler:
else:
callback_events = []
if feedback_tokens:
metadatas = [
InvokeResponseMetadata(
run_id=run_id,
feedback_tokens=[
FeedbackToken(
key=feedback_key,
token_url=feedback_token.url,
expires_at=feedback_token.expires_at.isoformat(),
)
],
)
for run_id, feedback_token in zip(run_ids, feedback_tokens)
]
else:
metadatas = [
InvokeResponseMetadata(run_id=run_id, feedback_tokens=[])
for run_id in run_ids
]
obj = self._BatchResponse(
output=self._serializer.dumpd(output),
callback_events=callback_events,
metadata=BatchResponseMetadata(
run_ids=run_ids,
responses=metadatas,
run_ids=[_get_base_run_id_as_str(agg) for agg in aggregators]
),
)
return _json_encode_response(obj)
async def stream(
@@ -1083,7 +908,8 @@ class APIHandler:
Originates from the client side. This config must be validated.
server_config: optional server configuration that will be merged
"""
run_id = None
err_event = {}
validation_exception: Optional[BaseException] = None
try:
config, input_ = await self._get_config_and_input(
request,
@@ -1091,30 +917,36 @@ class APIHandler:
endpoint="stream",
server_config=server_config,
)
run_id = config["run_id"]
except BaseException:
# Exceptions will be properly translated by default FastAPI middleware
# to either 422 (on input validation) or 500 internal server errors.
raise
if self._token_feedback_enabled:
# Create task to create a presigned feedback token
feedback_key: Optional[str] = self._token_feedback_config["key_configs"][0][
"key"
]
feedback_coro = run_in_executor(
None,
self._langsmith_client.create_presigned_feedback_token,
run_id,
feedback_key,
)
task: Optional[asyncio.Task] = asyncio.create_task(feedback_coro)
else:
feedback_key = None
task = None
except BaseException as e:
validation_exception = e
if isinstance(e, RequestValidationError):
err_event = {
"event": "error",
"data": json.dumps(
{"status_code": 422, "message": repr(e.errors())}
),
}
else:
err_event = {
"event": "error",
# Do not expose the error message to the client since
# the message may contain sensitive information.
"data": json.dumps(
{"status_code": 500, "message": "Internal Server Error"}
),
}
async def _stream() -> AsyncIterator[dict]:
"""Stream the output of the runnable."""
if validation_exception:
yield err_event
if isinstance(validation_exception, RequestValidationError):
return
else:
raise AssertionError(
"Internal server error"
) from validation_exception
try:
config_w_callbacks = config.copy()
event_aggregator = AsyncEventAggregatorCallback()
@@ -1124,19 +956,16 @@ class APIHandler:
input_,
config=config_w_callbacks,
):
# Send a metadata event as soon as possible
if not has_sent_metadata:
if task is not None and not task.done():
continue
if task is None:
feedback_token = None
else:
feedback_token = task.result()
if not has_sent_metadata and event_aggregator.callback_events:
yield {
"event": "metadata",
"data": json.dumps(
{
"run_id": _get_base_run_id_as_str(event_aggregator),
}
),
}
has_sent_metadata = True
yield _create_metadata_event(
run_id, feedback_key, feedback_token
)
yield {
# EventSourceResponse expects a string for data
@@ -1172,6 +1001,8 @@ class APIHandler:
View documentation for endpoint at the end of the file.
It's attached to _stream_log_docs endpoint.
"""
err_event = {}
validation_exception: Optional[BaseException] = None
try:
config, input_ = await self._get_config_and_input(
request,
@@ -1179,39 +1010,56 @@ class APIHandler:
endpoint="stream_log",
server_config=server_config,
)
run_id = config["run_id"]
except BaseException:
# Exceptions will be properly translated by default FastAPI middleware
# to either 422 (on input validation) or 500 internal server errors.
raise
except BaseException as e:
validation_exception = e
if isinstance(e, RequestValidationError):
err_event = {
"event": "error",
"data": json.dumps(
{"status_code": 422, "message": repr(e.errors())}
),
}
else:
err_event = {
"event": "error",
# Do not expose the error message to the client since
# the message may contain sensitive information.
"data": json.dumps(
{"status_code": 500, "message": "Internal Server Error"}
),
}
try:
body = await request.json()
with _with_validation_error_translation():
stream_log_request = StreamLogParameters(**body)
except json.JSONDecodeError:
raise RequestValidationError(errors=["Invalid JSON body"])
except RequestValidationError:
raise
feedback_key: Optional[str]
if self._token_feedback_enabled:
# Create task to create a presigned feedback token
feedback_key: str = self._token_feedback_config["key_configs"][0]["key"]
feedback_coro = run_in_executor(
None,
self._langsmith_client.create_presigned_feedback_token,
run_id,
feedback_key,
)
task: Optional[asyncio.Task] = asyncio.create_task(feedback_coro)
else:
feedback_key = None
task = None
# Body as text
validation_exception = RequestValidationError(errors=["Invalid JSON body"])
err_event = {
"event": "error",
"data": json.dumps(
{"status_code": 422, "message": "Invalid JSON body"}
),
}
except RequestValidationError as e:
validation_exception = e
err_event = {
"event": "error",
"data": json.dumps({"status_code": 422, "message": repr(e.errors())}),
}
async def _stream_log() -> AsyncIterator[dict]:
"""Stream the output of the runnable."""
has_sent_metadata = False
if validation_exception:
yield err_event
if isinstance(validation_exception, RequestValidationError):
return
else:
raise AssertionError(
"Internal server error"
) from validation_exception
try:
async for chunk in self._runnable.astream_log(
input_,
@@ -1245,18 +1093,6 @@ class APIHandler:
"data": self._serializer.dumps(data).decode("utf-8"),
"event": "data",
}
# Send a metadata event as soon as possible
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():
continue
feedback_token = task.result()
yield _create_metadata_event(
run_id, feedback_key, feedback_token
)
has_sent_metadata = True
yield {"event": "end"}
except BaseException:
yield {
@@ -1280,7 +1116,8 @@ class APIHandler:
server_config: Optional[RunnableConfig] = None,
) -> EventSourceResponse:
"""Stream events from the runnable."""
run_id = None
err_event = {}
validation_exception: Optional[BaseException] = None
try:
config, input_ = await self._get_config_and_input(
request,
@@ -1288,36 +1125,44 @@ class APIHandler:
endpoint="stream_events",
server_config=server_config,
)
run_id = config["run_id"]
except BaseException:
# Exceptions will be properly translated by default FastAPI middleware
# to either 422 (on input validation) or 500 internal server errors.
raise
except BaseException as e:
validation_exception = e
if isinstance(e, RequestValidationError):
err_event = {
"event": "error",
"data": json.dumps(
{"status_code": 422, "message": repr(e.errors())}
),
}
else:
err_event = {
"event": "error",
# Do not expose the error message to the client since
# the message may contain sensitive information.
"data": json.dumps(
{"status_code": 500, "message": "Internal Server Error"}
),
}
try:
body = await request.json()
with _with_validation_error_translation():
stream_events_request = StreamEventsParameters(**body)
except json.JSONDecodeError:
raise RequestValidationError(errors=["Invalid JSON body"])
except RequestValidationError:
raise
feedback_key: Optional[str]
if self._token_feedback_enabled:
# Create task to create a presigned feedback token
feedback_key: str = self._token_feedback_config["key_configs"][0]["key"]
feedback_coro = run_in_executor(
None,
self._langsmith_client.create_presigned_feedback_token,
run_id,
feedback_key,
)
task: Optional[asyncio.Task] = asyncio.create_task(feedback_coro)
else:
feedback_key = None
task = None
# Body as text
validation_exception = RequestValidationError(errors=["Invalid JSON body"])
err_event = {
"event": "error",
"data": json.dumps(
{"status_code": 422, "message": "Invalid JSON body"}
),
}
except RequestValidationError as e:
validation_exception = e
err_event = {
"event": "error",
"data": json.dumps({"status_code": 422, "message": repr(e.errors())}),
}
async def _stream_events() -> AsyncIterator[dict]:
"""Stream the output of the runnable."""
@@ -1326,7 +1171,14 @@ class APIHandler:
"Please upgrade langchain-core>=0.1.14 to use astream_events"
)
has_sent_metadata = False
if validation_exception:
yield err_event
if isinstance(validation_exception, RequestValidationError):
return
else:
raise AssertionError(
"Internal server error"
) from validation_exception
try:
async for event in self._runnable.astream_events(
@@ -1354,18 +1206,6 @@ class APIHandler:
"data": self._serializer.dumps(event).decode("utf-8"),
"event": "data",
}
# Send a metadata event as soon as possible
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():
continue
feedback_token = task.result()
yield _create_metadata_event(
run_id, feedback_key, feedback_token
)
has_sent_metadata = True
yield {"event": "end"}
except BaseException:
yield {
@@ -1501,20 +1341,14 @@ class APIHandler:
+ "/playground"
)
feedback_enabled = tracing_is_enabled() and self._enable_feedback_endpoint
public_trace_link_enabled = (
tracing_is_enabled() and self._enable_public_trace_link_endpoint
)
return await serve_playground(
self._runnable.with_config(config),
self._runnable.with_config(config).input_schema,
self._runnable.with_config(config).output_schema,
self._config_keys,
playground_url,
file_path,
feedback_enabled,
public_trace_link_enabled,
playground_type=self.playground_type,
)
async def create_feedback(
@@ -1532,7 +1366,7 @@ class APIHandler:
raise HTTPException(
400,
"The feedback endpoint is only accessible when LangSmith is "
+ "enabled on your LangServe server.\n"
+ "enabled on your LangServe server."
+ "Please set `enable_feedback_endpoint=True` on your route and "
+ "set the proper environment variables",
)
@@ -1563,39 +1397,6 @@ class APIHandler:
comment=feedback_from_langsmith.comment,
)
async def create_feedback_from_token(
self, create_request: FeedbackCreateRequestTokenBased
) -> None:
"""Send feedback on an individual run to langsmith."""
if not tracing_is_enabled() or not self._token_feedback_enabled:
raise HTTPException(
400,
(
"The feedback endpoint is only accessible when LangSmith is "
"enabled on your LangServe server.\n "
"Please set the proper environment variables to enable LangSmith."
"In addition, please ensure that the token_feedback_config "
"has been properly specified when using add_routes or APIHandler."
),
)
metadata = create_request.metadata or {}
metadata.update(
{
"from_langserve": True,
}
)
self._langsmith_client.create_feedback_from_token(
create_request.token_or_url,
score=create_request.score,
value=create_request.value,
comment=create_request.comment,
correction=create_request.correction,
metadata=metadata,
)
async def _check_feedback_enabled(self) -> None:
"""Check if feedback is enabled for the runnable.
@@ -1605,7 +1406,7 @@ class APIHandler:
raise HTTPException(
400,
"The feedback endpoint is only accessible when LangSmith is "
+ "enabled on your LangServe server.\n"
+ "enabled on your LangServe server."
+ "Please set `enable_feedback_endpoint=True` on your route and "
+ "set the proper environment variables",
)
@@ -1613,44 +1414,3 @@ class APIHandler:
async def check_feedback_enabled(self) -> bool:
"""Check if feedback is enabled for the runnable."""
return self._enable_feedback_endpoint or not tracing_is_enabled()
async def create_public_trace_link(
self, public_trace_link_create_req: PublicTraceLinkCreateRequest
) -> PublicTraceLink:
"""Send feedback on an individual run to langsmith
Note that a successful response means that feedback was successfully
submitted. It does not guarantee that the feedback is recorded by
langsmith. Requests may be silently rejected if they are
unauthenticated or invalid by the server.
"""
if not tracing_is_enabled() or not self._enable_public_trace_link_endpoint:
raise HTTPException(
400,
"The public trace link endpoint is only accessible when "
+ "LangSmith is enabled on your LangServe server.\n"
+ "Please set `enable_public_trace_link_endpoint=True` on your "
+ "route and set the proper environment variables",
)
public_url = self._langsmith_client.share_run(
public_trace_link_create_req.run_id,
)
return PublicTraceLink(public_url=public_url)
async def _check_public_trace_link_enabled(self) -> None:
"""Check if public trace links are enabled for the runnable.
This endpoint is private since it will be deprecated in the future.
"""
if not (await self.check_public_trace_link_enabled()):
raise HTTPException(
400,
"The public trace link endpoint is only accessible when "
+ "LangSmith is enabled on your LangServe server.\n"
+ "Please set `enable_public_trace_link_endpoint=True` on your "
+ "route and set the proper environment variables",
)
async def check_public_trace_link_enabled(self) -> bool:
"""Check if public trace links are enabled for the runnable."""
return self._enable_public_trace_link_endpoint or not tracing_is_enabled()
+3 -6
View File
@@ -4,16 +4,13 @@ import uuid
from typing import Any, Dict, List, Optional, Sequence
from uuid import UUID
from langchain_core.agents import AgentAction, AgentFinish
from langchain_core.callbacks import AsyncCallbackHandler
from langchain_core.callbacks.manager import (
from langchain.callbacks.base import AsyncCallbackHandler
from langchain.callbacks.manager import (
BaseRunManager,
ahandle_event,
handle_event,
)
from langchain_core.documents import Document
from langchain_core.messages import BaseMessage
from langchain_core.outputs import LLMResult
from langchain.schema import AgentAction, AgentFinish, BaseMessage, Document, LLMResult
from typing_extensions import TypedDict
-18
View File
@@ -1,18 +0,0 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}
-28
View File
@@ -1,28 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.yarn
!dist
-27
View File
@@ -1,27 +0,0 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
- Configure the top-level `parserOptions` property like this:
```js
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: ['./tsconfig.json', './tsconfig.node.json'],
tsconfigRootDir: __dirname,
},
```
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

-42
View File
@@ -1,42 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<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-53ad47d4.js"></script>
<link rel="stylesheet" href="/____LANGSERVE_BASE_URL/assets/index-434ff580.css">
</head>
<body>
<div id="root"></div>
<script>
try {
window.CONFIG_SCHEMA = ____LANGSERVE_CONFIG_SCHEMA;
} catch (e) {
// pass
}
try {
window.INPUT_SCHEMA = ____LANGSERVE_INPUT_SCHEMA;
} catch (e) {
// pass
}
try {
window.OUTPUT_SCHEMA = ____LANGSERVE_OUTPUT_SCHEMA;
} catch (e) {
// pass
}
try {
window.FEEDBACK_ENABLED = ____LANGSERVE_FEEDBACK_ENABLED;
} catch (e) {
// pass
}
try {
window.PUBLIC_TRACE_LINK_ENABLED = ____LANGSERVE_PUBLIC_TRACE_LINK_ENABLED;
} catch (e) {
// pass
}
</script>
</body>
</html>
-40
View File
@@ -1,40 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Chat Playground</title>
</head>
<body>
<div id="root"></div>
<script>
try {
window.CONFIG_SCHEMA = ____LANGSERVE_CONFIG_SCHEMA;
} catch (e) {
// pass
}
try {
window.INPUT_SCHEMA = ____LANGSERVE_INPUT_SCHEMA;
} catch (e) {
// pass
}
try {
window.OUTPUT_SCHEMA = ____LANGSERVE_OUTPUT_SCHEMA;
} catch (e) {
// pass
}
try {
window.FEEDBACK_ENABLED = ____LANGSERVE_FEEDBACK_ENABLED;
} catch (e) {
// pass
}
try {
window.PUBLIC_TRACE_LINK_ENABLED = ____LANGSERVE_PUBLIC_TRACE_LINK_ENABLED;
} catch (e) {
// pass
}
</script>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
-52
View File
@@ -1,52 +0,0 @@
{
"name": "langserve-chat-playground",
"private": true,
"version": "0.0.0",
"type": "module",
"packageManager": "yarn@1.22.19",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@jsonforms/core": "^3.2.1",
"@microsoft/fetch-event-source": "^2.0.1",
"@mui/icons-material": "^5.14.11",
"@mui/material": "^5.14.11",
"@mui/x-date-pickers": "^6.16.0",
"@radix-ui/react-toggle-group": "^1.0.4",
"clsx": "^2.0.0",
"dayjs": "^1.11.10",
"fast-json-patch": "^3.1.1",
"lodash": "^4.17.21",
"lz-string": "^1.5.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-toastify": "^9.1.3",
"swr": "^2.2.4",
"tailwind-merge": "^1.14.0",
"use-debounce": "^9.0.4",
"vaul": "^0.7.3"
},
"devDependencies": {
"@types/lodash": "^4.14.200",
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"@vitejs/plugin-react": "^4.0.3",
"autoprefixer": "^10.4.16",
"eslint": "^8.45.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3",
"postcss": "^8.4.31",
"tailwindcss": "^3.3.3",
"typescript": "^5.0.2",
"vite": "^4.4.5",
"vite-plugin-svgr": "^4.1.0"
}
}
@@ -1,6 +0,0 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

-116
View File
@@ -1,116 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
* {
color: #043D5C;
font-weight: 300;
@font-face {
font-family: 'Manrope';
src: url('/dist/Manrope-VariableFont_wght.ttf') format('truetype');
}
border-color: #043D5C;
}
input,
textarea,
select {
background: transparent;
}
/* clash between MUI and Tailwind */
input:focus,
textarea:focus,
select:focus {
box-shadow: none;
outline: none;
}
:root {
--popover: 0 0% 100%;
--background: #F8F7FF;
--divider-500: 210 40% 96.1%; /* slate-100 */
--divider-700: 214.3 31.8% 91.4%; /* slate-200 */
--ls-blue: 211.5 91.8% 61.8%;
--ls-black: 222.2 47.4% 11.2%; /* slate-900 */
--ls-gray-100: 215.4 16.3% 46.9%; /* slate-500 */
--ls-gray-200: 212.7 26.8% 83.9%; /* slate-300 */
--ls-gray-300: 214.3 31.8% 91.4%; /* slate-200 */
--ls-gray-400: 210 40% 96.1%; /* slate-100 */
--button-green: #162E2E;
--button-green-disabled: rgba(4, 61, 92, 0.20);
--button-inline: #006BA41A;
}
@media (prefers-color-scheme: dark) {
:root {
--popover: 240 11.6% 8.4%;
--divider-500: 217.2 32.6% 17.5%; /* slate-800 */
--divider-700: 215.3 25% 26.7%; /* slate-700 */
--ls-blue: 211.5 91.8% 61.8%;
--ls-black: 0 0% 100%; /* white */
--ls-gray-100: 215 20.2% 65.1%; /* slate-400 */
--ls-gray-200: 215.4 16.3% 46.9%; /* slate-500 */
--ls-gray-300: 215.3 25% 26.7%; /* slate-700 */
--ls-gray-400: 217.2 32.6% 17.5%; /* slate-800 */
}
}
}
.control {
@apply flex flex-col border border-divider-700 rounded-lg p-3 gap-1 relative bg-background transition-all outline-ls-blue/20;
@apply focus-within:border-ls-blue focus-within:outline focus-within:outline-4 focus-within:outline-ls-blue/20;
}
.control > label,
.control h6 {
@apply text-xs uppercase font-semibold text-ls-gray-100;
}
.control div .MuiGrid-item {
@apply pt-0;
}
.control > select {
@apply -ml-1;
}
.control > .input-description,
.control > .validation {
@apply absolute right-3 top-3 text-xs;
}
.group-layout {
@apply flex flex-col gap-4 bg-background p-4 border border-divider-700 rounded-lg;
}
.no-scrollbar {
scrollbar-width: none;
}
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.vertical-layout {
@apply flex flex-col gap-4;
}
.share-button:hover {
background: linear-gradient(270deg, #BCB2FD 0.29%, #D65622 92%);
}
.share-button:hover > * {
color: white;
}
a {
color: blue;
text-decoration: underline;
}
-73
View File
@@ -1,73 +0,0 @@
import "./App.css";
import { ChatWindow } from "./components/ChatWindow";
import { AppCallbackContext, useAppStreamCallbacks } from "./useStreamCallback";
import { useInputSchema, useOutputSchema } from "./useSchemas";
import { useStreamLog } from "./useStreamLog";
export function App() {
const { context, callbacks } = useAppStreamCallbacks();
const { startStream, stopStream } = useStreamLog(callbacks);
const inputSchema = useInputSchema({});
const outputSchema = useOutputSchema({});
const inputProps = inputSchema?.data?.schema?.properties;
const outputDataSchema = outputSchema?.data?.schema;
const isLoading = inputProps === undefined || outputDataSchema === undefined;
const inputKeys = Object.keys(inputProps ?? {});
const inputSchemaSupported = (
inputKeys.length === 1 &&
inputProps?.[inputKeys[0]].type === "array"
) || (
inputKeys.length === 2 && (
(
inputProps?.[inputKeys[0]].type === "array" ||
inputProps?.[inputKeys[1]].type === "string"
) || (
inputProps?.[inputKeys[0]].type === "string" ||
inputProps?.[inputKeys[1]].type === "array"
)
)
);
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);
return (
<div className="flex items-center flex-col text-ls-black bg-background">
<AppCallbackContext.Provider value={context}>
{isSupported
? <ChatWindow
startStream={startStream}
stopStream={stopStream}
messagesInputKey={inputProps?.[inputKeys[0]].type === "array" ? inputKeys[0] : inputKeys[1]}
inputKey={inputProps?.[inputKeys[0]].type === "string" ? inputKeys[0] : inputKeys[1]}
></ChatWindow>
: <div className="h-[100vh] w-[100vw] flex justify-center items-center text-xl p-16">
<span>
The chat playground is only supported for chains that take one of the following as input:
<ul className="mt-8 list-disc ml-6">
<li>
a dict with a single key containing a list of messages
</li>
<li>
a dict with two keys: one a string input, one an list of messages
</li>
</ul>
<br />
and which return either an <code>AIMessage</code> or a string.
<br />
<br />
You can test this chain in the default LangServe playground instead.
<br />
<br />
To use the default playground, set <code>playground_type="default"</code> when adding the route in your backend.
</span>
</div>}
</AppCallbackContext.Provider>
</div>
);
}
export default App;
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-up"><line x1="12" y1="19" x2="12" y2="5"></line><polyline points="5 12 12 5 19 12"></polyline></svg>

Before

Width:  |  Height:  |  Size: 310 B

@@ -1,6 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M7.7588 2H16.2414C17.0464 1.99999 17.7107 1.99998 18.2519 2.04419C18.814 2.09012 19.3307 2.18868 19.8161 2.43597C20.5687 2.81947 21.1806 3.43139 21.5641 4.18404C21.8114 4.66937 21.91 5.18608 21.9559 5.74817C22.0001 6.28936 22.0001 6.95372 22.0001 7.75868V13.2413C22.0001 14.0463 22.0001 14.7106 21.9559 15.2518C21.91 15.8139 21.8114 16.3306 21.5641 16.816C21.1806 17.5686 20.5687 18.1805 19.8161 18.564C19.3307 18.8113 18.814 18.9099 18.2519 18.9558C17.7107 19 17.0464 19 16.2414 19H13.6838C13.0197 19 12.8263 19.0047 12.6504 19.0408C12.4738 19.0771 12.303 19.137 12.1425 19.219C11.9826 19.3007 11.8286 19.4178 11.31 19.8327L8.89688 21.7632C8.7132 21.9102 8.52597 22.06 8.36137 22.1689C8.20394 22.273 7.8987 22.4593 7.50172 22.4597C7.0449 22.4602 6.61276 22.2525 6.32778 21.8955C6.08012 21.5852 6.03492 21.2305 6.01785 21.0425C6 20.846 6.00005 20.6062 6.00009 20.371L6.0001 18.9918C5.60829 18.9789 5.27229 18.9461 4.96482 18.8637C3.58445 18.4938 2.50626 17.4156 2.13639 16.0353C1.9993 15.5236 1.99962 14.933 2.00005 14.1376C2.00007 14.0924 2.0001 14.0465 2.0001 14L2.0001 7.7587C2.00008 6.95373 2.00007 6.28937 2.04429 5.74817C2.09022 5.18608 2.18878 4.66937 2.43607 4.18404C2.81956 3.43139 3.43149 2.81947 4.18413 2.43597C4.66947 2.18868 5.18617 2.09012 5.74827 2.04419C6.28947 1.99998 6.95383 1.99999 7.7588 2ZM5.91113 4.03755C5.47272 4.07337 5.24852 4.1383 5.09212 4.21799C4.71579 4.40973 4.40983 4.7157 4.21808 5.09202C4.13839 5.24842 4.07347 5.47262 4.03765 5.91104C4.00087 6.36113 4.0001 6.94342 4.0001 7.8V14C4.0001 14.9944 4.00869 15.2954 4.06824 15.5176C4.25318 16.2078 4.79227 16.7469 5.48246 16.9319C5.70474 16.9914 6.00574 17 7.0001 17C7.55238 17 8.0001 17.4477 8.0001 18V19.9194L10.0606 18.271C10.0834 18.2528 10.1058 18.2348 10.1279 18.2171C10.55 17.8791 10.8691 17.6237 11.2326 17.4379C11.5536 17.274 11.8952 17.1541 12.2483 17.0817C12.6482 16.9996 13.0569 16.9998 13.5976 17C13.626 17 13.6547 17 13.6838 17H16.2001C17.0567 17 17.639 16.9992 18.0891 16.9624C18.5275 16.9266 18.7517 16.8617 18.9081 16.782C19.2844 16.5903 19.5904 16.2843 19.7821 15.908C19.8618 15.7516 19.9267 15.5274 19.9625 15.089C19.9993 14.6389 20.0001 14.0566 20.0001 13.2V7.8C20.0001 6.94342 19.9993 6.36113 19.9625 5.91104C19.9267 5.47262 19.8618 5.24842 19.7821 5.09202C19.5904 4.7157 19.2844 4.40973 18.9081 4.21799C18.7517 4.1383 18.5275 4.07337 18.0891 4.03755C17.639 4.00078 17.0567 4 16.2001 4H7.8001C6.94352 4 6.36122 4.00078 5.91113 4.03755Z"
fill="currentColor" />
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

@@ -1,5 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M12 5.45455C8.38505 5.45455 5.45455 8.38505 5.45455 12C5.45455 15.615 8.38505 18.5455 12 18.5455C15.615 18.5455 18.5455 15.615 18.5455 12C18.5455 8.38505 15.615 5.45455 12 5.45455ZM4 12C4 7.58172 7.58172 4 12 4C16.4183 4 20 7.58172 20 12C20 16.4183 16.4183 20 12 20C7.58172 20 4 16.4183 4 12ZM15.787 9.30392C16.071 9.58794 16.071 10.0484 15.787 10.3324L11.4233 14.6961C11.1393 14.9801 10.6788 14.9801 10.3948 14.6961L8.21301 12.5143C7.929 12.2303 7.929 11.7697 8.21301 11.4857C8.49703 11.2017 8.95751 11.2017 9.24153 11.4857L10.9091 13.1533L14.7585 9.30392C15.0425 9.01991 15.503 9.01991 15.787 9.30392Z"
fill="currentColor" />
</svg>

Before

Width:  |  Height:  |  Size: 789 B

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-check-circle"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline></svg>

Before

Width:  |  Height:  |  Size: 328 B

@@ -1,6 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M8.29289 5.29289C8.68342 4.90237 9.31658 4.90237 9.70711 5.29289L15.7071 11.2929C16.0976 11.6834 16.0976 12.3166 15.7071 12.7071L9.70711 18.7071C9.31658 19.0976 8.68342 19.0976 8.29289 18.7071C7.90237 18.3166 7.90237 17.6834 8.29289 17.2929L13.5858 12L8.29289 6.70711C7.90237 6.31658 7.90237 5.68342 8.29289 5.29289Z"
fill="currentColor" />
</svg>

Before

Width:  |  Height:  |  Size: 505 B

@@ -1,15 +0,0 @@
<svg
aria-hidden="true"
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill"
/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

@@ -1,6 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M8.70711 5.29289C9.09763 5.68342 9.09763 6.31658 8.70711 6.70711L3.41421 12L8.70711 17.2929C9.09763 17.6834 9.09763 18.3166 8.70711 18.7071C8.31658 19.0976 7.68342 19.0976 7.29289 18.7071L1.29289 12.7071C0.902369 12.3166 0.902369 11.6834 1.29289 11.2929L7.29289 5.29289C7.68342 4.90237 8.31658 4.90237 8.70711 5.29289ZM15.2929 5.29289C15.6834 4.90237 16.3166 4.90237 16.7071 5.29289L22.7071 11.2929C23.0976 11.6834 23.0976 12.3166 22.7071 12.7071L16.7071 18.7071C16.3166 19.0976 15.6834 19.0976 15.2929 18.7071C14.9024 18.3166 14.9024 17.6834 15.2929 17.2929L20.5858 12L15.2929 6.70711C14.9024 6.31658 14.9024 5.68342 15.2929 5.29289Z"
fill="currentColor" />
</svg>

Before

Width:  |  Height:  |  Size: 823 B

@@ -1,6 +0,0 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M4.5 19.7783C4.5 21.5132 5.35498 22.3848 7.07324 22.3848H14.876C16.5942 22.3848 17.4492 21.5049 17.4492 19.7783V18.2427H18.9019C20.6118 18.2427 21.4751 17.3628 21.4751 15.6362V8.896C21.4751 7.875 21.2676 7.22754 20.645 6.58838L16.4531 2.33008C15.8638 1.72412 15.1665 1.5 14.2783 1.5H11.0991C9.38916 1.5 8.52588 2.37988 8.52588 4.10645V5.64209H7.07324C5.36328 5.64209 4.5 6.51367 4.5 8.24854V19.7783ZM16.6606 11.0874L12.0869 6.43066C11.4561 5.7832 10.9331 5.64209 10.0034 5.64209H9.8623V4.13135C9.8623 3.30957 10.3022 2.83643 11.1655 2.83643H14.8345V7.09473C14.8345 8.05762 15.2993 8.51416 16.2539 8.51416H20.1387V15.6113C20.1387 16.4414 19.6904 16.9062 18.8271 16.9062H17.4492V13.2954C17.4492 12.2329 17.3247 11.7681 16.6606 11.0874ZM16.0381 6.89551V3.49219L19.79 7.31055H16.4448C16.1543 7.31055 16.0381 7.18604 16.0381 6.89551ZM5.83643 19.7534V8.26514C5.83643 7.45166 6.27637 6.97852 7.13965 6.97852H9.8623V11.793C9.8623 12.8389 10.3936 13.3618 11.4229 13.3618H16.1128V19.7534C16.1128 20.5835 15.6646 21.0483 14.8096 21.0483H7.13135C6.27637 21.0483 5.83643 20.5835 5.83643 19.7534ZM11.5806 12.1084C11.2485 12.1084 11.1157 11.9756 11.1157 11.6436V7.28564L15.8555 12.1084H11.5806Z"
fill="currentColor" />
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

@@ -1,21 +0,0 @@
<svg width="76" height="64" viewBox="0 0 76 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Group 145">
<path id="Vector" d="M46.897 46.0711C56.7342 36.282 58.3487 22.0173 50.5032 14.2101C42.6577 6.40285 28.3231 8.00951 18.486 17.7986C8.64888 27.5878 7.03435 41.8524 14.8798 49.6597C22.7253 57.4669 37.0599 55.8602 46.897 46.0711Z" stroke="url(#paint0_linear_10720_23418)" stroke-width="1.5" stroke-miterlimit="10"/>
<path id="Vector_2" d="M52.3165 46.0721C62.1536 36.2829 63.7681 22.0183 55.9226 14.211C48.0771 6.40383 33.7426 8.01049 23.9054 17.7996C14.0683 27.5888 12.4538 41.8534 20.2993 49.6606C28.1448 57.4679 42.4794 55.8612 52.3165 46.0721Z" stroke="url(#paint1_linear_10720_23418)" stroke-width="1.5" stroke-miterlimit="10"/>
<path id="Vector_3" d="M57.7354 46.0721C67.5725 36.2829 69.1871 22.0183 61.3416 14.211C53.4961 6.40383 39.1615 8.01049 29.3244 17.7996C19.4873 27.5888 17.8727 41.8534 25.7182 49.6606C33.5637 57.4679 47.8983 55.8612 57.7354 46.0721Z" stroke="url(#paint2_linear_10720_23418)" stroke-width="1.5" stroke-miterlimit="10"/>
</g>
<defs>
<linearGradient id="paint0_linear_10720_23418" x1="7.45971" y1="29.1331" x2="55.0891" y2="34.5103" gradientUnits="userSpaceOnUse">
<stop stop-color="#A75A3D"/>
<stop offset="1" stop-color="#BCB2FD"/>
</linearGradient>
<linearGradient id="paint1_linear_10720_23418" x1="12.9979" y1="29.1441" x2="60.6273" y2="34.5213" gradientUnits="userSpaceOnUse">
<stop stop-color="#BB5831"/>
<stop offset="1" stop-color="#BCB2FD"/>
</linearGradient>
<linearGradient id="paint2_linear_10720_23418" x1="18.5356" y1="29.1542" x2="66.165" y2="34.5313" gradientUnits="userSpaceOnUse">
<stop stop-color="#CF5726"/>
<stop offset="1" stop-color="#BCB2FD"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

@@ -1,12 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="140" height="23" viewBox="0 0 140 23" fill="none">
<path d="M51.7334 4.44824V17.796H60.8269V15.7806H53.7978V4.44824H51.7334Z" fill="#043D5C"/>
<path d="M65.6734 7.61523C63.6917 7.61523 62.2098 8.55468 61.6081 10.1923C61.5698 10.2971 61.454 10.6132 61.454 10.6132L63.1527 11.7243L63.3834 11.1159C63.777 10.0788 64.5049 9.59538 65.6734 9.59538C66.8418 9.59538 67.5105 10.1686 67.4983 11.2973C67.4983 11.3431 67.4949 11.4813 67.4949 11.4813C67.4949 11.4813 65.9485 11.7349 65.3112 11.8714C62.592 12.4525 61.4531 13.502 61.4531 15.2189C61.4531 16.1337 61.9555 17.1242 62.8724 17.6797C63.4226 18.0126 64.141 18.1385 64.9342 18.1385C65.4557 18.1385 65.9624 18.0601 66.4317 17.9157C67.4983 17.5574 67.7961 16.853 67.7961 16.853V17.774H69.5628V11.1899C69.5628 8.95177 68.1087 7.61523 65.6734 7.61523ZM67.5053 14.6413C67.5053 15.3333 66.76 16.308 65.0238 16.308C64.5336 16.308 64.1862 16.1768 63.9546 15.9813C63.6447 15.7198 63.5428 15.3439 63.5855 15.012C63.6037 14.8676 63.6899 14.5568 64.0095 14.2873C64.336 14.0118 64.9133 13.8145 65.8048 13.6182C66.538 13.4571 67.5062 13.2792 67.5062 13.2792V14.6422L67.5053 14.6413Z" fill="#043D5C"/>
<path d="M76.0944 7.61514C75.8489 7.61514 75.6095 7.63275 75.377 7.66532C73.7949 7.90569 73.3317 8.71835 73.3317 8.71835L73.3335 7.90569H71.3535V17.7756H73.4179V12.3009C73.4179 10.4405 74.7597 9.59353 76.0065 9.59353C77.3543 9.59353 78.0091 10.3261 78.0091 11.8343V17.7756H80.0735V11.5473C80.0735 9.12072 78.5489 7.61426 76.0953 7.61426L76.0944 7.61514Z" fill="#043D5C"/>
<path d="M88.6883 7.89874V8.91567C88.6883 8.91567 88.1824 7.61523 85.8812 7.61523C83.0218 7.61523 81.2456 9.61035 81.2456 12.8231C81.2456 14.636 81.8185 16.0632 82.8294 16.9648C83.6156 17.6657 84.6657 18.0249 85.916 18.0495C86.7858 18.0663 87.3492 17.8268 87.7009 17.6005C88.3766 17.1656 88.6273 16.7526 88.6273 16.7526C88.6273 16.7526 88.5986 17.0758 88.5464 17.5133C88.5089 17.8303 88.4384 18.0531 88.4384 18.0531C88.1241 19.1845 87.2046 19.8386 85.8638 19.8386C84.5229 19.8386 83.7105 19.3922 83.5495 18.5127L81.5425 19.1184C81.889 20.8089 83.458 21.8179 85.7392 21.8179C87.29 21.8179 88.5054 21.3918 89.3526 20.55C90.2068 19.7013 90.6404 18.4783 90.6404 16.9146V7.89786H88.6883V7.89874ZM88.5586 12.9129C88.5586 14.8887 87.6043 16.0685 86.0057 16.0685C84.293 16.0685 83.3109 14.8852 83.3109 12.8231C83.3109 10.7611 84.293 9.59538 86.0057 9.59538C87.566 9.59538 88.5438 10.7699 88.5586 12.6611V12.9129Z" fill="#043D5C"/>
<path d="M102.741 15.7585C103.02 15.247 103.161 14.6439 103.161 13.965C103.161 13.2862 103.042 12.765 102.806 12.3432C102.57 11.9197 102.265 11.5737 101.901 11.314C101.534 11.0525 101.147 10.8465 100.752 10.7003C100.355 10.5541 99.9898 10.4362 99.6686 10.3508L97.3116 9.68602C97.0138 9.60589 96.716 9.50464 96.4252 9.38402C96.1318 9.26252 95.8819 9.09435 95.6843 8.88392C95.4823 8.66997 95.3795 8.39174 95.3795 8.05453C95.3795 7.70058 95.4988 7.38626 95.733 7.12036C95.9655 6.8571 96.2755 6.65284 96.6551 6.51285C97.0321 6.37285 97.4474 6.30506 97.8897 6.31122C98.3442 6.32355 98.7787 6.41776 99.1801 6.59209C99.5832 6.76642 99.9298 7.02351 100.21 7.35632C100.483 7.68033 100.672 8.07478 100.773 8.52997L103.051 8.12937C102.857 7.34048 102.526 6.65636 102.066 6.09639C101.598 5.52585 101.008 5.08386 100.311 4.78363C99.6137 4.48251 98.8083 4.32667 97.9167 4.32051C97.0391 4.31434 96.2267 4.4561 95.5084 4.74224C94.7918 5.02751 94.2145 5.46246 93.7923 6.03476C93.3708 6.60617 93.1566 7.32287 93.1566 8.16458C93.1566 8.7404 93.2515 9.22906 93.4396 9.61822C93.6277 10.0083 93.8732 10.3323 94.1684 10.5832C94.4653 10.835 94.784 11.034 95.1166 11.1766C95.4518 11.3201 95.7679 11.4364 96.0569 11.5209L99.4544 12.5369C99.6999 12.6118 99.9193 12.7016 100.107 12.8037C100.295 12.9067 100.452 13.023 100.573 13.1489C100.695 13.2756 100.789 13.4218 100.852 13.5838C100.915 13.7458 100.946 13.9237 100.946 14.1121C100.946 14.5338 100.812 14.8957 100.546 15.1854C100.284 15.4724 99.9385 15.6943 99.5197 15.8431C99.1026 15.9919 98.6524 16.0676 98.1805 16.0676C97.3839 16.0676 96.6699 15.8475 96.0595 15.4134C95.4579 14.9855 95.0513 14.3745 94.851 13.5979L92.6499 13.936C92.7857 14.7742 93.0983 15.5094 93.5789 16.1204C94.0683 16.7429 94.7021 17.228 95.4614 17.5617C96.2223 17.8963 97.0913 18.0662 98.0421 18.0662C98.7108 18.0662 99.3595 17.9799 99.9716 17.8091C100.582 17.6392 101.132 17.3803 101.607 17.0396C102.08 16.7006 102.462 16.2683 102.739 15.7568L102.741 15.7585Z" fill="#043D5C"/>
<path d="M113.245 15.2689L111.168 14.6332C110.954 15.0814 110.651 15.4291 110.264 15.6686C109.869 15.9134 109.392 16.0375 108.848 16.0375C108.004 16.0375 107.351 15.754 106.908 15.1949C106.563 14.7582 106.35 14.1736 106.276 13.456L106.271 13.4076H113.371C113.449 12.2498 113.318 11.2302 112.981 10.3753C112.642 9.51334 112.114 8.83627 111.414 8.36258C110.714 7.88978 109.85 7.64941 108.847 7.64941C107.897 7.64941 107.05 7.86953 106.328 8.30359C105.607 8.73766 105.035 9.3575 104.63 10.1446C104.224 10.9326 104.018 11.8747 104.018 12.9462C104.018 13.9376 104.228 14.826 104.643 15.5859C105.057 16.3457 105.642 16.9488 106.382 17.3767C107.122 17.8055 107.994 18.0221 108.974 18.0221C109.955 18.0221 110.77 17.7747 111.542 17.2851C112.3 16.8053 112.873 16.1265 113.243 15.2671L113.245 15.2689ZM106.331 11.7744C106.432 11.1959 106.627 10.7178 106.909 10.3551C107.351 9.78717 108.034 9.49838 108.94 9.49838C109.742 9.49838 110.341 9.75459 110.717 10.2591C110.981 10.6148 111.156 11.1255 111.236 11.7761L111.243 11.8254H106.322L106.331 11.7735V11.7744Z" fill="#043D5C"/>
<path d="M117.531 10.7923C117.724 10.5316 117.978 10.3142 118.285 10.1469C118.589 9.96199 118.939 9.85193 119.323 9.82023C119.68 9.79118 120.004 9.81407 120.285 9.88891V7.91932C120.011 7.87618 119.724 7.86473 119.434 7.8841C119.13 7.90524 118.83 7.96335 118.542 8.05667C118.253 8.15 117.987 8.27943 117.749 8.44143C117.469 8.61665 117.228 8.8394 117.029 9.10618C116.941 9.22416 116.861 9.34919 116.783 9.49094L116.701 9.63798V7.92637H114.855V17.7488H116.964V12.7548C116.964 12.3736 117.01 12.0126 117.101 11.6833C117.194 11.3522 117.337 11.052 117.531 10.7923Z" fill="#043D5C"/>
<path d="M127.917 7.92578L125.426 15.201L122.928 7.92578H120.833L124.359 17.7482H126.494L130.019 7.92578H127.917Z" fill="#043D5C"/>
<path d="M138.918 10.3753C138.579 9.51334 138.051 8.83627 137.351 8.36258C136.651 7.88978 135.787 7.64941 134.784 7.64941C133.834 7.64941 132.987 7.86953 132.265 8.30359C131.544 8.73766 130.972 9.3575 130.567 10.1446C130.16 10.9326 129.955 11.8756 129.955 12.9462C129.955 13.9376 130.165 14.826 130.58 15.5859C130.994 16.3457 131.579 16.9488 132.319 17.3767C133.059 17.8055 133.931 18.0221 134.911 18.0221C135.892 18.0221 136.706 17.7747 137.479 17.2851C138.237 16.8053 138.81 16.1265 139.18 15.2671L137.103 14.6314C136.889 15.0796 136.585 15.4274 136.199 15.6669C135.804 15.9116 135.328 16.0358 134.783 16.0358C133.939 16.0358 133.286 15.7523 132.844 15.1932C132.499 14.7565 132.285 14.1718 132.211 13.4543L132.206 13.4058H139.307C139.384 12.248 139.254 11.2285 138.917 10.3736L138.918 10.3753ZM132.258 11.8254L132.266 11.7735C132.367 11.195 132.561 10.7178 132.844 10.3542C133.287 9.78628 133.969 9.49749 134.875 9.49749C135.678 9.49749 136.276 9.75371 136.652 10.2582C136.917 10.6139 137.092 11.1246 137.172 11.7752L137.178 11.8245H132.258V11.8254Z" fill="#043D5C"/>
<path d="M16.343 16.9848C16.3204 17.4497 15.7466 17.6381 15.3582 17.5808C15.2729 17.3722 15.1545 17.1635 14.9029 17.2797C14.6486 17.3819 14.37 17.2612 14.4292 16.9408C15.1449 16.9135 15.1911 15.9652 15.3321 15.4096C15.5837 16.0048 15.7631 16.6476 16.343 16.9848ZM43.1377 11.1333C43.1377 17.1987 38.2575 22.1337 32.2593 22.1337H11.2085C5.21031 22.1328 0.330078 17.1987 0.330078 11.1333C0.330078 5.06778 5.21031 0.132812 11.2085 0.132812H32.2601C38.2583 0.132812 43.1386 5.06778 43.1386 11.1333H43.1377ZM21.2937 16.6493C21.4653 16.4389 20.6729 15.8472 20.511 15.6289C20.1819 15.2679 20.1801 14.7493 19.9581 14.3276C19.4148 13.0553 18.7905 11.7918 17.9181 10.715C16.9951 9.53698 15.8571 8.56144 14.8567 7.45558C14.114 6.68342 13.9164 5.58461 13.2607 4.75434C12.3578 3.40547 9.50108 3.03744 9.08228 4.94275C9.08402 5.00175 9.06574 5.04048 9.01437 5.07834C8.78276 5.24827 8.57641 5.44197 8.40401 5.67794C7.98086 6.27401 7.91468 7.28653 8.44406 7.82185C8.46148 7.53923 8.47106 7.27333 8.69221 7.06994C9.10144 7.42477 9.71963 7.55067 10.1933 7.28565C11.2416 8.79828 10.9795 10.8911 11.8119 12.5209C12.0418 12.9065 12.2725 13.2992 12.5685 13.6382C12.808 14.015 13.6351 14.4596 13.683 14.8074C13.6917 15.4052 13.6221 16.0603 14.0104 16.5604C14.1924 16.9355 13.744 17.3123 13.3818 17.2656C12.9116 17.3308 12.3387 16.946 11.9268 17.1829C11.7814 17.3422 11.4967 17.1662 11.3713 17.3871C11.3278 17.5016 11.0927 17.6627 11.2329 17.7719C11.3887 17.6522 11.5333 17.528 11.7431 17.5985C11.7118 17.771 11.8467 17.7957 11.953 17.8459C11.9495 17.963 11.8816 18.0827 11.9704 18.1822C12.074 18.0765 12.1358 17.9269 12.3004 17.8828C12.8463 18.6198 13.4027 17.1371 14.5859 17.8045C14.3456 17.7922 14.1332 17.823 13.9704 18.0228C13.9303 18.0677 13.8963 18.1206 13.9669 18.1787C14.6051 17.7622 14.6016 18.3204 15.0161 18.1496C15.3347 17.9815 15.6508 17.771 16.0295 17.8309C15.6612 17.9383 15.6464 18.2377 15.4314 18.4904C15.3948 18.5291 15.3774 18.5731 15.4201 18.6374C16.1837 18.5722 16.2463 18.3152 16.8645 18.0008C17.3243 17.7164 17.7831 18.4058 18.1819 18.0132C18.2698 17.9277 18.39 17.9568 18.4988 17.9454C18.3595 17.1943 16.8288 18.0827 16.8532 17.0763C17.346 16.7374 17.2328 16.0894 17.2659 15.5655C17.8327 15.8833 18.4623 16.0682 19.0178 16.3711C19.2972 16.8281 19.7369 17.4329 20.3229 17.3933C20.3386 17.3475 20.3525 17.307 20.3691 17.2604C20.5467 17.2912 20.7748 17.41 20.8723 17.1829C21.1379 17.4637 21.528 17.4497 21.8754 17.3775C22.1322 17.1662 21.3921 16.8659 21.2929 16.6484L21.2937 16.6493ZM35.6828 14.8224C35.6828 13.5624 34.6728 12.5411 33.4268 12.5411C32.1809 12.5411 31.1709 13.5624 31.1709 14.8224C31.1709 16.0823 32.1809 17.1036 33.4268 17.1036C34.6728 17.1036 35.6828 16.0823 35.6828 14.8224ZM35.85 6.1754C34.1095 4.01123 30.551 4.02268 27.9032 6.20093C26.8549 7.0629 26.1069 8.13529 25.6925 9.25084C25.6368 9.40139 25.622 9.5634 25.6524 9.721C25.7378 10.1612 25.9084 11.1641 25.8492 11.9389C25.8048 12.5253 25.7317 12.9303 25.6768 13.1698C25.6394 13.3335 25.561 13.4832 25.4487 13.6073C25.4487 13.6073 24.9454 14.1629 24.7487 14.3804C24.5179 14.6357 24.1801 14.9492 23.9076 15.1825C23.7404 15.3251 23.433 15.5074 23.2754 15.5937C23.1239 15.6773 22.8932 15.8463 23.0743 16.0559C23.2554 16.2654 24.7077 17.639 24.7077 17.639C24.7077 17.639 24.9672 17.8388 25.1744 17.3669C25.3816 16.895 25.8719 16.3165 26.147 16.0427C26.43 15.7609 26.9576 15.2917 27.3781 15.1385C27.763 14.9985 28.7782 14.7194 30.4891 15.1385C30.6049 15.1534 30.7129 15.1684 30.8156 15.1842C30.8 15.0663 30.7913 14.9456 30.7913 14.8241C30.7913 13.3538 31.9737 12.1581 33.4277 12.1581C34.1565 12.1581 34.8173 12.4592 35.2945 12.9435C37.0228 10.8057 37.3215 8.00675 35.8491 6.17716L35.85 6.1754Z" fill="#043D5C"/>
</svg>

Before

Width:  |  Height:  |  Size: 11 KiB

@@ -1,6 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M4 5.33301C4 3.12387 5.79086 1.33301 8 1.33301C10.2091 1.33301 12 3.12387 12 5.33301V6.76656C12.1884 6.80784 12.3692 6.86796 12.544 6.95699C13.0457 7.21265 13.4537 7.6206 13.7093 8.12237C13.8742 8.44592 13.9399 8.79039 13.9705 9.16512C14 9.52592 14 9.96882 14 10.5055V10.8272C14 11.3639 14 11.8068 13.9705 12.1676C13.9399 12.5423 13.8742 12.8868 13.7093 13.2103C13.4537 13.7121 13.0457 14.12 12.544 14.3757C12.2204 14.5406 11.8759 14.6063 11.5012 14.6369C11.1404 14.6664 10.6975 14.6663 10.1609 14.6663H5.83912C5.30248 14.6663 4.85958 14.6664 4.49878 14.6369C4.12405 14.6063 3.77958 14.5406 3.45603 14.3757C2.95426 14.12 2.54631 13.7121 2.29065 13.2103C2.12579 12.8868 2.06008 12.5423 2.02946 12.1676C1.99998 11.8068 1.99999 11.3639 2 10.8272V10.5055C1.99999 9.96883 1.99998 9.52592 2.02946 9.16512C2.06008 8.79039 2.12579 8.44592 2.29065 8.12237C2.54631 7.6206 2.95426 7.21265 3.45603 6.95699C3.63076 6.86796 3.81159 6.80784 4 6.76656V5.33301ZM5.33333 6.66742C5.49181 6.66634 5.66026 6.66634 5.83913 6.66634H10.1609C10.3397 6.66634 10.5082 6.66634 10.6667 6.66742V5.33301C10.6667 3.86025 9.47276 2.66634 8 2.66634C6.52724 2.66634 5.33333 3.86025 5.33333 5.33301V6.66742ZM4.60736 8.02471C4.31508 8.04859 4.16561 8.09187 4.06135 8.145C3.81046 8.27283 3.60649 8.4768 3.47866 8.72769C3.42553 8.83195 3.38225 8.98142 3.35837 9.2737C3.33385 9.57376 3.33333 9.96195 3.33333 10.533V10.7997C3.33333 11.3707 3.33385 11.7589 3.35837 12.059C3.38225 12.3513 3.42553 12.5007 3.47866 12.605C3.60649 12.8559 3.81046 13.0599 4.06135 13.1877C4.16561 13.2408 4.31508 13.2841 4.60736 13.308C4.90742 13.3325 5.29561 13.333 5.86667 13.333H10.1333C10.7044 13.333 11.0926 13.3325 11.3926 13.308C11.6849 13.2841 11.8344 13.2408 11.9387 13.1877C12.1895 13.0599 12.3935 12.8559 12.5213 12.605C12.5745 12.5007 12.6178 12.3513 12.6416 12.059C12.6661 11.7589 12.6667 11.3707 12.6667 10.7997V10.533C12.6667 9.96195 12.6661 9.57376 12.6416 9.2737C12.6178 8.98142 12.5745 8.83195 12.5213 8.72769C12.3935 8.4768 12.1895 8.27283 11.9387 8.145C11.8344 8.09187 11.6849 8.04859 11.3926 8.02471C11.0926 8.00019 10.7044 7.99967 10.1333 7.99967H5.86667C5.29561 7.99967 4.90742 8.00019 4.60736 8.02471Z"
fill="currentColor" />
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

@@ -1,6 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M11.9999 2.51489C12.5522 2.51489 12.9999 2.96261 12.9999 3.51489V11.0002L20.4852 11.0002C21.0375 11.0002 21.4852 11.4479 21.4852 12.0002C21.4852 12.5525 21.0375 13.0002 20.4852 13.0002H12.9999V20.4855C12.9999 21.0377 12.5522 21.4855 11.9999 21.4855C11.4476 21.4855 10.9999 21.0377 10.9999 20.4855V13.0002H3.51465C2.96236 13.0002 2.51465 12.5525 2.51465 12.0002C2.51465 11.4479 2.96236 11.0002 3.51465 11.0002L10.9999 11.0002V3.51489C10.9999 2.96261 11.4476 2.51489 11.9999 2.51489Z"
fill="currentColor" />
</svg>

Before

Width:  |  Height:  |  Size: 670 B

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-refresh-cw"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg>

Before

Width:  |  Height:  |  Size: 400 B

@@ -1,6 +0,0 @@
<svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M3.36651 2.85015C3.37578 2.85432 3.38505 2.85849 3.39431 2.86266L17.353 9.14401C17.5431 9.22954 17.7338 9.31532 17.8826 9.39905C18.0208 9.47682 18.2876 9.63803 18.4396 9.94548C18.6122 10.2947 18.6122 10.7043 18.4396 11.0535C18.2876 11.361 18.0208 11.5222 17.8826 11.5999C17.7338 11.6837 17.5431 11.7694 17.353 11.855L3.37128 18.1467C3.17613 18.2346 2.98174 18.3221 2.81784 18.3789C2.6676 18.4309 2.36452 18.5263 2.02916 18.4327C1.65046 18.327 1.34355 18.0493 1.20065 17.6831C1.07411 17.3587 1.13883 17.0476 1.17565 16.8929C1.21583 16.7242 1.28354 16.522 1.35152 16.3191L3.28934 10.5306L1.35514 4.70306C1.35194 4.69342 1.34873 4.68377 1.34553 4.67412C1.27829 4.47166 1.21126 4.26982 1.17161 4.10129C1.13521 3.94656 1.07155 3.63604 1.19844 3.31251C1.34183 2.9469 1.64871 2.66994 2.02706 2.56467C2.36186 2.47151 2.66425 2.56656 2.81444 2.61859C2.97804 2.67526 3.17198 2.76257 3.36651 2.85015ZM3.05652 4.5383L4.75852 9.66616H8.75109C9.21133 9.66616 9.58442 10.0393 9.58442 10.4995C9.58442 10.9597 9.21133 11.3328 8.75109 11.3328H4.77834L3.06259 16.458L16.3037 10.4995L3.05652 4.5383Z"
fill="#fff" />
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

@@ -1,6 +0,0 @@
<svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M9.41009 2.41009C9.73553 2.08466 10.2632 2.08466 10.5886 2.41009L13.9219 5.74343C14.2474 6.06886 14.2474 6.5965 13.9219 6.92194C13.5965 7.24738 13.0689 7.24738 12.7434 6.92194L10.8327 5.01119V12.9993C10.8327 13.4596 10.4596 13.8327 9.99935 13.8327C9.53911 13.8327 9.16602 13.4596 9.16602 12.9993V5.01119L7.25527 6.92194C6.92984 7.24738 6.4022 7.24738 6.07676 6.92194C5.75132 6.5965 5.75132 6.06886 6.07676 5.74343L9.41009 2.41009ZM2.49935 9.66602C2.95959 9.66602 3.33268 10.0391 3.33268 10.4993V13.9993C3.33268 14.7132 3.33333 15.1984 3.36398 15.5735C3.39383 15.9388 3.44793 16.1257 3.51434 16.256C3.67413 16.5696 3.9291 16.8246 4.2427 16.9844C4.37303 17.0508 4.55987 17.1049 4.92521 17.1347C5.30029 17.1654 5.78553 17.166 6.49935 17.166H13.4993C14.2132 17.166 14.6984 17.1654 15.0735 17.1347C15.4388 17.1049 15.6257 17.0508 15.756 16.9844C16.0696 16.8246 16.3246 16.5696 16.4844 16.256C16.5508 16.1257 16.6049 15.9388 16.6347 15.5735C16.6654 15.1984 16.666 14.7132 16.666 13.9993V10.4993C16.666 10.0391 17.0391 9.66602 17.4993 9.66602C17.9596 9.66602 18.3327 10.0391 18.3327 10.4993V14.0338C18.3327 14.7046 18.3327 15.2582 18.2959 15.7092C18.2576 16.1776 18.1754 16.6082 17.9694 17.0127C17.6498 17.6399 17.1399 18.1498 16.5126 18.4694C16.1082 18.6754 15.6776 18.7576 15.2092 18.7959C14.7582 18.8327 14.2046 18.8327 13.5338 18.8327H6.46491C5.79411 18.8327 5.24049 18.8327 4.78949 18.7959C4.32108 18.7576 3.89049 18.6754 3.48605 18.4694C2.85884 18.1498 2.34891 17.6399 2.02933 17.0127C1.82325 16.6082 1.74112 16.1776 1.70284 15.7092C1.666 15.2582 1.66601 14.7046 1.66602 14.0338L1.66602 10.4993C1.66602 10.0391 2.03911 9.66602 2.49935 9.66602Z"
fill="currentColor" />
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-thumbs-down"><path d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"></path></svg>

Before

Width:  |  Height:  |  Size: 374 B

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-thumbs-up"><path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"></path></svg>

Before

Width:  |  Height:  |  Size: 354 B

@@ -1,6 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M8 3C8 2.44772 8.44772 2 9 2H15C15.5523 2 16 2.44772 16 3C16 3.55228 15.5523 4 15 4H9C8.44772 4 8 3.55228 8 3ZM4.99224 5H3C2.44772 5 2 5.44772 2 6C2 6.55228 2.44772 7 3 7H4.06445L4.70614 16.6254C4.75649 17.3809 4.79816 18.006 4.87287 18.5149C4.95066 19.0447 5.07405 19.5288 5.33109 19.98C5.73123 20.6824 6.33479 21.247 7.06223 21.5996C7.52952 21.826 8.0208 21.917 8.55459 21.9593C9.06728 22 9.69383 22 10.4509 22H13.5491C14.3062 22 14.9327 22 15.4454 21.9593C15.9792 21.917 16.4705 21.826 16.9378 21.5996C17.6652 21.247 18.2688 20.6824 18.6689 19.98C18.926 19.5288 19.0493 19.0447 19.1271 18.5149C19.2018 18.006 19.2435 17.3808 19.2939 16.6253L19.9356 7H21C21.5523 7 22 6.55228 22 6C22 5.44772 21.5523 5 21 5H19.0078C19.0019 4.99995 18.9961 4.99995 18.9903 5H5.00974C5.00392 4.99995 4.99809 4.99995 4.99224 5ZM17.9311 7H6.06889L6.69907 16.4528C6.75274 17.2578 6.78984 17.8034 6.85166 18.2243C6.9117 18.6333 6.98505 18.8429 7.06888 18.99C7.26895 19.3412 7.57072 19.6235 7.93444 19.7998C8.08684 19.8736 8.30086 19.9329 8.71286 19.9656C9.13703 19.9993 9.68385 20 10.4907 20H13.5093C14.3161 20 14.863 19.9993 15.2871 19.9656C15.6991 19.9329 15.9132 19.8736 16.0656 19.7998C16.4293 19.6235 16.7311 19.3412 16.9311 18.99C17.015 18.8429 17.0883 18.6333 17.1483 18.2243C17.2102 17.8034 17.2473 17.2578 17.3009 16.4528L17.9311 7Z"
fill="currentColor" />
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x-circle"><circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg>

Before

Width:  |  Height:  |  Size: 346 B

@@ -1,55 +0,0 @@
import { Ref } from "react";
import { cn } from "../utils/cn";
const COMMON_CLS = cn(
"text-lg col-[1] row-[1] m-0 resize-none overflow-hidden whitespace-pre-wrap break-words border-none bg-transparent p-0"
);
export function AutosizeTextarea(props: {
id?: string;
inputRef?: Ref<HTMLTextAreaElement>;
value?: string | null | undefined;
placeholder?: string;
className?: string;
onChange?: (e: string) => void;
onFocus?: () => void;
onBlur?: () => void;
onKeyDown?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
autoFocus?: boolean;
readOnly?: boolean;
cursorPointer?: boolean;
disabled?: boolean;
fullHeight?: boolean;
}) {
return (
<div className={cn("grid w-full", props.className) + (props.fullHeight ? "" : " max-h-80 overflow-auto")}>
<textarea
ref={props.inputRef}
id={props.id}
className={cn(
COMMON_CLS,
"text-transparent caret-black"
)}
disabled={props.disabled}
value={props.value ?? ""}
rows={1}
onChange={(e) => {
const target = e.target as HTMLTextAreaElement;
props.onChange?.(target.value);
}}
onFocus={props.onFocus}
onBlur={props.onBlur}
placeholder={props.placeholder}
readOnly={props.readOnly}
autoFocus={props.autoFocus && !props.readOnly}
onKeyDown={props.onKeyDown}
/>
<div
aria-hidden
className={cn(COMMON_CLS, "pointer-events-none select-none")}
>
{props.value}{" "}
</div>
</div>
);
}
@@ -1,118 +0,0 @@
import { useState } from "react";
import { CorrectnessFeedback } from "./feedback/CorrectnessFeedback";
import { resolveApiUrl } from "../utils/url";
import { AutosizeTextarea } from "./AutosizeTextarea";
import TrashIcon from "../assets/TrashIcon.svg?react";
import RefreshCW from "../assets/RefreshCW.svg?react";
export type ChatMessageType = "human" | "ai" | "function" | "tool" | "system";
export type ChatMessageBody = {
type: ChatMessageType;
content: string;
runId?: string;
}
export function ChatMessage(props: {
message: ChatMessageBody;
isLoading?: boolean;
onError?: (e: any) => void;
onTypeChange?: (newValue: string) => void;
onChange?: (newValue: string) => void;
onRemove?: (e: any) => void;
onRegenerate?: (e?: any) => void;
isFinalMessage?: boolean;
feedbackEnabled?: boolean;
publicTraceLinksEnabled?: boolean;
}) {
const { message, feedbackEnabled, publicTraceLinksEnabled, onError, isLoading } = props;
const { content, type, runId } = message;
const [publicTraceLink, setPublicTraceLink] = useState<string | null>(null);
const [messageActionIsLoading, setMessageActionIsLoading] = useState(false);
const openPublicTrace = async () => {
if (messageActionIsLoading) {
return;
}
if (publicTraceLink) {
window.open(publicTraceLink, '_blank');
return;
}
setMessageActionIsLoading(true);
const payload = { run_id: runId };
const response = await fetch(resolveApiUrl("/public_trace_link"), {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!response.ok) {
if (response.status === 404) {
onError?.(new Error(`Feedback endpoint not found. Please enable it in your LangServe endpoint.`));
} else {
try {
const errorResponse = await response.json();
onError?.(new Error(`${errorResponse.detail}`));
} catch (e) {
onError?.(new Error(`Request failed with status: ${response.status}`));
}
}
setMessageActionIsLoading(false);
throw new Error(`Failed request ${response.status}`)
}
const parsedResponse = await response.json();
setMessageActionIsLoading(false);
setPublicTraceLink(parsedResponse.public_url);
window.open(parsedResponse.public_url, '_blank');
};
return (
<div className="mb-8 group">
<div className="flex justify-between">
<select
className="font-medium text-transform uppercase mb-2 appearance-none"
defaultValue={type}
onChange={(e) => props.onTypeChange?.(e.target.value)}
>
<option value="human">HUMAN</option>
<option value="ai">AI</option>
<option value="system">SYSTEM</option>
</select>
<span className="flex">
{props.isFinalMessage &&
type === "human" &&
<RefreshCW className="opacity-0 group-hover:opacity-50 transition-opacity duration-200 cursor-pointer h-4 w-4 mr-2" onMouseUp={props.onRegenerate}></RefreshCW>}
<TrashIcon
className="opacity-0 group-hover:opacity-50 transition-opacity duration-200 cursor-pointer h-4 w-4"
onMouseUp={props.onRemove}
></TrashIcon>
</span>
</div>
<AutosizeTextarea value={content} fullHeight={true} onChange={props.onChange} onKeyDown={(e) => {
if (
e.key === 'Enter' &&
!e.shiftKey &&
props.isFinalMessage &&
type === "human"
) {
e.preventDefault();
props.onRegenerate?.();
}
}}></AutosizeTextarea>
{type === "ai" && !isLoading && runId != null && (
<div className="mt-2 flex items-center">
{feedbackEnabled && <span className="mr-2"><CorrectnessFeedback runId={runId} onError={props.onError}></CorrectnessFeedback></span>}
{publicTraceLinksEnabled && <>
<button
className="bg-button-inline p-2 rounded-lg text-xs font-medium hover:opacity-80"
disabled={messageActionIsLoading || isLoading}
onMouseUp={openPublicTrace}
>
🛠 View LangSmith trace
</button>
</>}
</div>
)}
</div>
)
};
@@ -1,217 +0,0 @@
import { useState, useRef } from "react";
import { ToastContainer, toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { AutosizeTextarea } from "./AutosizeTextarea";
import {
ChatMessage,
type ChatMessageType,
type ChatMessageBody,
} from "./ChatMessage";
import { ShareDialog } from "./ShareDialog";
import { useStreamCallback } from "../useStreamCallback";
import ArrowUp from "../assets/ArrowUp.svg?react";
import CircleSpinIcon from "../assets/CircleSpinIcon.svg?react";
import EmptyState from "../assets/EmptyState.svg?react";
import LangServeLogo from "../assets/LangServeLogo.svg?react";
import { useFeedback, usePublicTraceLink } from "../useSchemas";
export type AIMessage = {
content: string;
type: "AIMessage" | "AIMessageChunk";
name?: string;
additional_kwargs?: { [key: string]: unknown };
}
export function isAIMessage(x: unknown): x is AIMessage {
return x != null &&
typeof (x as AIMessage).content === "string" &&
["AIMessageChunk", "AIMessage"].includes((x as AIMessage).type);
}
export function ChatWindow(props: {
startStream: (input: unknown, config: unknown) => Promise<void>;
stopStream: (() => void) | undefined;
messagesInputKey: string;
inputKey?: string;
}) {
const { startStream, messagesInputKey, inputKey } = props;
const [currentInputValue, setCurrentInputValue] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [messages, setMessages] = useState<ChatMessageBody[]>([]);
const messageInputRef = useRef<HTMLTextAreaElement>(null);
const feedbackEnabled = useFeedback()
const publicTraceLinksEnabled = usePublicTraceLink();
const submitMessage = () => {
const submittedValue = currentInputValue;
if (submittedValue.length === 0 || isLoading) {
return;
}
setIsLoading(true);
const newMessages = [
...messages,
{ type: "human", content: submittedValue } as const
];
setMessages(newMessages);
setCurrentInputValue("");
// TODO: Add config schema support
if (inputKey === undefined) {
startStream({ [messagesInputKey]: newMessages }, {});
} else {
startStream({
[messagesInputKey]: newMessages.slice(0, -1),
[inputKey]: newMessages[newMessages.length - 1].content
}, {});
}
};
const regenerateMessages = () => {
if (isLoading) {
return;
}
setIsLoading(true);
// TODO: Add config schema support
if (inputKey === undefined) {
startStream({ [messagesInputKey]: messages }, {});
} else {
startStream({
[messagesInputKey]: messages.slice(0, -1),
[inputKey]: messages[messages.length - 1].content
}, {});
}
};
useStreamCallback("onStart", () => {
setMessages((prevMessages) => [
...prevMessages,
{ type: "ai", content: "" },
]);
});
useStreamCallback("onChunk", (_chunk, aggregatedState) => {
const finalOutput = aggregatedState?.final_output;
if (typeof finalOutput === "string") {
setMessages((prevMessages) => [
...prevMessages.slice(0, -1),
{ type: "ai", content: finalOutput, runId: aggregatedState?.id }
]);
} else if (isAIMessage(finalOutput)) {
setMessages((prevMessages) => [
...prevMessages.slice(0, -1),
{ type: "ai", content: finalOutput.content, runId: aggregatedState?.id }
]);
}
});
useStreamCallback("onSuccess", () => {
setIsLoading(false);
});
useStreamCallback("onError", (e) => {
setIsLoading(false);
toast(e.message + "\nCheck your backend logs for errors.", { hideProgressBar: true });
setCurrentInputValue(messages[messages.length - 2]?.content);
setMessages((prevMessages) => [
...prevMessages.slice(0, -2),
]);
});
return (
<div className="flex flex-col h-screen w-screen">
<nav className="flex items-center justify-between p-8">
<div className="flex items-center">
<LangServeLogo />
<span className="ml-1">Playground</span>
</div>
<div className="flex items-center space-x-4">
<ShareDialog config={{}}>
<button
type="button"
className="px-3 py-1 border rounded-full px-8 py-2 share-button"
>
<span>Share</span>
</button>
</ShareDialog>
</div>
</nav>
<div className="flex-grow flex flex-col items-center justify-center mt-8">
{messages.length > 0 ? (
<div className="flex flex-col-reverse basis-0 overflow-auto flex-re grow max-w-[640px] w-[640px]">
{messages.map((message, i) => {
return (
<ChatMessage
message={message}
key={i}
isLoading={isLoading}
onError={(e: any) => toast(e.message, { hideProgressBar: true })}
feedbackEnabled={feedbackEnabled.data}
publicTraceLinksEnabled={publicTraceLinksEnabled.data}
isFinalMessage={i === messages.length - 1}
onRemove={() => setMessages(
(previousMessages) => [...previousMessages.slice(0, i), ...previousMessages.slice(i + 1)]
)}
onTypeChange={(newValue) => {
setMessages(
(previousMessages) => [
...previousMessages.slice(0, i),
{...message, type: newValue as ChatMessageType},
...previousMessages.slice(i + 1)
]
)
}}
onChange={(newValue) => {
setMessages(
(previousMessages) => [
...previousMessages.slice(0, i),
{...message, content: newValue},
...previousMessages.slice(i + 1)
]
);
}}
onRegenerate={() => regenerateMessages()}
></ChatMessage>
);
}).reverse()}
</div>
) : (
<div className="flex flex-col items-center justify-center">
<EmptyState />
<h1 className="text-lg">Start testing your application</h1>
</div>
)}
</div>
<div className="m-16 mt-4 flex justify-center">
<div className="flex items-center p-3 rounded-[48px] border shadow-sm max-w-[768px] grow" onClick={() => messageInputRef.current?.focus()}>
<AutosizeTextarea
inputRef={messageInputRef}
className="flex-grow mr-4 ml-8 border-none focus:ring-0 py-2 cursor-text"
placeholder="Send a message..."
value={currentInputValue}
onChange={(newValue) => {
setCurrentInputValue(newValue);
}}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
submitMessage();
}
}}
/>
<button
className={"flex items-center justify-center px-3 py-1 rounded-[40px] " + (isLoading ? "" : currentInputValue.length > 0 ? "bg-button-green" : "bg-button-green-disabled")}
onClick={(e) => {
e.preventDefault();
submitMessage();
}}
>
{isLoading
? <CircleSpinIcon className="animate-spin w-5 h-5 text-background fill-background" />
: <ArrowUp className="mx-2 my-2 h-5 w-5 stroke-white" />}
</button>
</div>
</div>
<ToastContainer />
</div>
)
}
@@ -1,141 +0,0 @@
import { Drawer } from "vaul";
import { ReactNode, useEffect, useMemo, useRef, useState } from "react";
import CheckCircleIcon from "../assets/CheckCircleIcon.svg?react";
import CodeIcon from "../assets/CodeIcon.svg?react";
import CopyIcon from "../assets/CopyIcon.svg?react";
import PadlockIcon from "../assets/PadlockIcon.svg?react";
import ShareIcon from "../assets/ShareIcon.svg?react";
import { compressToEncodedURIComponent } from "lz-string";
import { getStateFromUrl } from "../utils/url";
const URL_LENGTH_LIMIT = 2000;
function CopyButton(props: { value: string }) {
const [copied, setCopied] = useState(false);
const cbRef = useRef<number | null>(null);
function toggleCopied() {
setCopied(true);
if (cbRef.current != null) window.clearTimeout(cbRef.current);
cbRef.current = window.setTimeout(() => setCopied(false), 1500);
}
useEffect(() => {
return () => {
if (cbRef.current != null) {
window.clearTimeout(cbRef.current);
}
};
}, []);
return (
<button
className="px-3 py-1"
onClick={() => {
navigator.clipboard.writeText(props.value).then(toggleCopied);
}}
>
{copied ? <CheckCircleIcon /> : <CopyIcon />}
</button>
);
}
export function ShareDialog(props: { config: unknown; children: ReactNode }) {
const hash = useMemo(() => {
return compressToEncodedURIComponent(JSON.stringify(props.config));
}, [props.config]);
const state = getStateFromUrl(window.location.href);
// get base URL
const targetUrl = `${state.basePath}/c/${hash}`;
// .../c/[hash]/playground
const playgroundUrl = `${targetUrl}/playground`;
// cURL, JS: .../c/[hash]/invoke
// Python: .../c/[hash]
const invokeUrl = `${targetUrl}/invoke`;
const pythonSnippet = `
from langserve import RemoteRunnable
chain = RemoteRunnable("${targetUrl}")
chain.invoke({ ... })
`;
const typescriptSnippet = `
import { RemoteRunnable } from "langchain/runnables/remote";
const chain = new RemoteRunnable({ url: \`${invokeUrl}\` });
const result = await chain.invoke({ ... });
`;
return (
<Drawer.Root>
<Drawer.Trigger asChild>{props.children}</Drawer.Trigger>
<Drawer.Portal>
<Drawer.Overlay className="fixed inset-0 bg-black/40" />
<Drawer.Content className="flex justify-center items-center mt-24 fixed bottom-0 left-0 right-0 !pointer-events-none after:!bg-background">
<div className="p-4 bg-background max-w-[calc(800px-2rem)] rounded-t-2xl border border-divider-500 border-b-background pointer-events-auto">
<h3 className="flex items-center text-lg font-light">
<ShareIcon className="flex-shrink-0 mr-2" />
<span>Share</span>
</h3>
<hr className="border-divider-500 my-4 -mx-4" />
<div className="flex flex-col gap-3">
{playgroundUrl.length < URL_LENGTH_LIMIT && (
<div className="flex flex-col gap-2 p-3 rounded-2xl">
<div className="flex items-center">
<div className="w-10 h-10 flex items-center justify-center text-center text-sm bg-background rounded-xl">
🦜
</div>
<span>Chat interface</span>
</div>
<div className="grid grid-cols-[auto,1fr,auto] rounded-xl text-sm items-center border">
<PadlockIcon className="mx-3" />
<div className="overflow-auto whitespace-nowrap py-3 no-scrollbar">
{playgroundUrl.split("://")[1]}
</div>
<CopyButton value={playgroundUrl} />
</div>
</div>
)}
<div className="flex flex-col gap-2 p-3 rounded-2xl">
<div className="flex items-center">
<div className="w-10 h-10 flex items-center justify-center text-center text-sm bg-background rounded-xl">
<CodeIcon className="w-4 h-4" />
</div>
<span>Get the code</span>
</div>
{targetUrl.length < URL_LENGTH_LIMIT && (
<div className="grid grid-cols-[1fr,auto] rounded-xl text-sm items-center border">
<div className="overflow-auto whitespace-nowrap px-3 py-3 no-scrollbar">
Python SDK
</div>
<CopyButton value={pythonSnippet.trim()} />
</div>
)}
{invokeUrl.length < URL_LENGTH_LIMIT && (
<div className="grid grid-cols-[1fr,auto] rounded-xl text-sm items-center border">
<div className="overflow-auto whitespace-nowrap px-3 py-3 no-scrollbar">
TypeScript SDK
</div>
<CopyButton value={typescriptSnippet.trim()} />
</div>
)}
</div>
</div>
</div>
</Drawer.Content>
</Drawer.Portal>
</Drawer.Root>
);
}
@@ -1,102 +0,0 @@
import { toast } from "react-toastify";
import ThumbsUpIcon from "../../assets/ThumbsUpIcon.svg?react";
import ThumbsDownIcon from "../../assets/ThumbsDownIcon.svg?react";
import CircleSpinIcon from "../../assets/CircleSpinIcon.svg?react";
import CheckCircleIcon2 from "../../assets/CheckCircleIcon2.svg?react";
import XCircle from "../../assets/XCircle.svg?react";
import { resolveApiUrl } from "../../utils/url";
import { useState } from "react";
import useSWRMutation from "swr/mutation";
const useFeedbackMutation = (runId: string, onError?: (e: any) => void) => {
interface FeedbackArguments {
key: string;
score: number;
}
const [lastArg, setLastArg] = useState<FeedbackArguments | null>(null);
const mutation = useSWRMutation(
["feedback", runId],
async ([, runId], { arg }: { arg: FeedbackArguments }) => {
const payload = { run_id: runId, key: arg.key, score: arg.score };
setLastArg(arg);
const request = await fetch(resolveApiUrl("/feedback"), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!request.ok) {
if (request.status === 404) {
onError?.(new Error(`Feedback endpoint not found. Please enable it in your LangServe endpoint.`));
} else {
try {
const errorResponse = await request.json();
onError?.(new Error(`${errorResponse.detail}`));
} catch (e) {
onError?.(new Error(`Request failed with status: ${request.status}`));
}
}
throw new Error(`Failed request ${request.status}`)
}
const json: {
id: string;
score: number;
} = await request.json();
toast("Feedback sent successfully!", { hideProgressBar: true });
return json;
}
);
return { lastArg: mutation.isMutating ? lastArg : null, mutation };
};
export function CorrectnessFeedback(props: { runId: string, onError?: (e: any) => void }) {
const score = useFeedbackMutation(props.runId, props.onError);
if (props.runId == null) return null;
return (
<>
<button
type="button"
className={"bg-background rounded p-1 hover:opacity-80"}
disabled={score.mutation.isMutating}
onClick={() => {
if (score.mutation.data?.score !== 1) {
score.mutation.trigger({ key: "correctness", score: 1 });
}
}}
>
{score.lastArg?.score === 1 ? (
<CircleSpinIcon className="animate-spin w-4 h-4 text-white/50 fill-white" />
) : (
(score.mutation.data?.score !== 1
? <ThumbsUpIcon className="w-4 h-4" />
: <CheckCircleIcon2 className="w-4 h-4 stroke-teal-500" />)
)}
</button>
<button
type="button"
className={"bg-background rounded p-1 hover:opacity-80"}
disabled={score.mutation.isMutating}
onClick={() => {
if (score.mutation.data?.score !== 0) {
score.mutation.trigger({ key: "correctness", score: 0 });
}
}}
>
{score.lastArg?.score === 0 ? (
<CircleSpinIcon className="animate-spin w-4 h-4 text-white/50 fill-white" />
) : (
(score.mutation.data?.score !== 0
? <ThumbsDownIcon className="w-4 h-4" />
: <XCircle className="w-4 h-4 stroke-red-500" />)
)}
</button>
</>
);
}
-11
View File
@@ -1,11 +0,0 @@
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import relativeDate from "dayjs/plugin/relativeTime";
dayjs.extend(relativeDate);
dayjs.extend(utc);
ReactDOM.createRoot(document.getElementById("root")!).render(<App />);
-9
View File
@@ -1,9 +0,0 @@
import type { Operation } from "fast-json-patch";
import type { RunState } from "./useStreamLog";
export interface StreamCallback {
onSuccess?: (ctx: { input: unknown; output: unknown }) => void;
onChunk?: (chunk: { ops?: Operation[] }, aggregatedState: RunState | null) => void;
onError?: (error: any) => void;
onStart?: (ctx: { input: unknown }) => void;
}
@@ -1,131 +0,0 @@
import { JsonSchema } from "@jsonforms/core";
import { compressToEncodedURIComponent } from "lz-string";
import { resolveApiUrl } from "./utils/url";
import { simplifySchema } from "./utils/simplifySchema";
import useSWR from "swr";
import defaults from "./utils/defaults";
declare global {
interface Window {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
CONFIG_SCHEMA?: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
INPUT_SCHEMA?: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
OUTPUT_SCHEMA?: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
FEEDBACK_ENABLED?: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
PUBLIC_TRACE_LINK_ENABLED?: any;
}
}
export function useFeedback() {
return useSWR(["/feedback"], async () => {
if (!import.meta.env.DEV && window.FEEDBACK_ENABLED) {
return window.FEEDBACK_ENABLED === "true";
}
const response = await fetch(resolveApiUrl("/feedback"), {
method: "HEAD",
});
return response.ok;
});
}
export function usePublicTraceLink() {
return useSWR(["/public_trace_link"], async () => {
if (!import.meta.env.DEV && window.PUBLIC_TRACE_LINK_ENABLED) {
return window.PUBLIC_TRACE_LINK_ENABLED === "true";
}
const response = await fetch(resolveApiUrl("/public_trace_link"), {
method: "HEAD",
});
return response.ok;
});
}
export function useConfigSchema() {
return useSWR(["/config_schema"], async () => {
let schema: JsonSchema | null = null;
if (!import.meta.env.DEV && window.CONFIG_SCHEMA) {
schema = await simplifySchema(window.CONFIG_SCHEMA);
} else {
const response = await fetch(resolveApiUrl(`/config_schema`));
if (!response.ok) throw new Error(await response.text());
const json = await response.json();
schema = await simplifySchema(json);
}
if (schema == null) return null;
return {
schema,
defaults: defaults(schema),
};
});
}
export function useInputSchema(configData?: unknown) {
return useSWR(
["/input_schema", configData],
async ([, configData]) => {
// TODO: this won't work if we're already seeing a prefixed URL
const prefix = configData
? `/c/${compressToEncodedURIComponent(JSON.stringify(configData))}`
: "";
let schema: JsonSchema | null = null;
if (!prefix && !import.meta.env.DEV && window.INPUT_SCHEMA) {
schema = await simplifySchema(window.INPUT_SCHEMA);
} else {
const response = await fetch(resolveApiUrl(`${prefix}/input_schema`));
if (!response.ok) throw new Error(await response.text());
const json = await response.json();
schema = await simplifySchema(json);
}
if (schema == null) return null;
return {
schema,
defaults: defaults(schema),
};
},
{ keepPreviousData: true }
);
}
export function useOutputSchema(configData?: unknown) {
return useSWR(
["/output_schema", configData],
async ([, configData]) => {
// TODO: this won't work if we're already seeing a prefixed URL
const prefix = configData
? `/c/${compressToEncodedURIComponent(JSON.stringify(configData))}`
: "";
let schema: JsonSchema | null = null;
if (!prefix && !import.meta.env.DEV && window.OUTPUT_SCHEMA) {
schema = await simplifySchema(window.OUTPUT_SCHEMA);
} else {
const response = await fetch(resolveApiUrl(`${prefix}/output_schema`));
if (!response.ok) throw new Error(await response.text());
const json = await response.json();
schema = await simplifySchema(json);
}
if (schema == null) return null;
return {
schema,
defaults: defaults(schema),
};
},
{ keepPreviousData: true }
);
}
@@ -1,78 +0,0 @@
import {
MutableRefObject,
createContext,
useContext,
useEffect,
useRef,
} from "react";
import { StreamCallback } from "./types";
export const AppCallbackContext = createContext<MutableRefObject<{
onStart: Exclude<StreamCallback["onStart"], undefined>[];
onChunk: Exclude<StreamCallback["onChunk"], undefined>[];
onSuccess: Exclude<StreamCallback["onSuccess"], undefined>[];
onError: Exclude<StreamCallback["onError"], undefined>[];
}> | null>(null);
export function useAppStreamCallbacks() {
// callbacks handling
const context = useRef<{
onStart: Exclude<StreamCallback["onStart"], undefined>[];
onChunk: Exclude<StreamCallback["onChunk"], undefined>[];
onSuccess: Exclude<StreamCallback["onSuccess"], undefined>[];
onError: Exclude<StreamCallback["onError"], undefined>[];
}>({ onStart: [], onChunk: [], onSuccess: [], onError: [] });
const callbacks: StreamCallback = {
onStart(...args) {
for (const callback of context.current.onStart) {
callback(...args);
}
},
onChunk(...args) {
for (const callback of context.current.onChunk) {
callback(...args);
}
},
onSuccess(...args) {
for (const callback of context.current.onSuccess) {
callback(...args);
}
},
onError(...args) {
for (const callback of context.current.onError) {
callback(...args);
}
},
};
return { context, callbacks };
}
export function useStreamCallback<
Type extends "onStart" | "onChunk" | "onSuccess" | "onError"
>(type: Type, callback: Exclude<StreamCallback[Type], undefined>) {
type CallbackType = Exclude<StreamCallback[Type], undefined>;
const appCbRef = useContext(AppCallbackContext);
const callbackRef = useRef<CallbackType>(callback);
callbackRef.current = callback;
useEffect(() => {
// @ts-expect-error Not sure why I can't expand the tuple
const current = (...args) => callbackRef.current?.(...args);
appCbRef?.current[type].push(current);
return () => {
if (!appCbRef) return;
// @ts-expect-error Assingability issues due to the tuple object
// eslint-disable-next-line react-hooks/exhaustive-deps
appCbRef.current[type] = appCbRef.current[type].filter(
(callbacks) => callbacks !== current
);
};
}, [type, appCbRef]);
}
@@ -1,106 +0,0 @@
import { useCallback, useRef, useState } from "react";
import { applyPatch, Operation } from "fast-json-patch";
import { fetchEventSource } from "@microsoft/fetch-event-source";
import { resolveApiUrl } from "./utils/url";
import { StreamCallback } from "./types";
export interface LogEntry {
// ID of the sub-run.
id: string;
// Name of the object being run.
name: string;
// Type of the object being run, eg. prompt, chain, llm, etc.
type: string;
// List of tags for the run.
tags: string[];
// Key-value pairs of metadata for the run.
metadata: { [key: string]: unknown };
// ISO-8601 timestamp of when the run started.
start_time: string;
// List of LLM tokens streamed by this run, if applicable.
streamed_output_str: string[];
// Final output of this run.
// Only available after the run has finished successfully.
final_output?: unknown;
// ISO-8601 timestamp of when the run ended.
// Only available after the run has finished.
end_time?: string;
}
export interface RunState {
// ID of the run.
id: string;
// List of output chunks streamed by Runnable.stream()
streamed_output: unknown[];
// Final output of the run, usually the result of aggregating (`+`) streamed_output.
// Only available after the run has finished successfully.
final_output?: unknown;
// Map of run names to sub-runs. If filters were supplied, this list will
// contain only the runs that matched the filters.
logs: { [name: string]: LogEntry };
}
function reducer(state: RunState | null, action: Operation[]) {
return applyPatch(state, action, true, false).newDocument;
}
export function useStreamLog(callbacks: StreamCallback = {}) {
const [latest, setLatest] = useState<RunState | null>(null);
const [controller, setController] = useState<AbortController | null>(null);
const startRef = useRef(callbacks.onStart);
startRef.current = callbacks.onStart;
const chunkRef = useRef(callbacks.onChunk);
chunkRef.current = callbacks.onChunk;
const successRef = useRef(callbacks.onSuccess);
successRef.current = callbacks.onSuccess;
const errorRef = useRef(callbacks.onError);
errorRef.current = callbacks.onError;
const startStream = useCallback(async (input: unknown, config: unknown) => {
const controller = new AbortController();
setController(controller);
startRef.current?.({ input });
let innerLatest: RunState | null = null;
await fetchEventSource(resolveApiUrl("/stream_log").toString(), {
signal: controller.signal,
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ input, config }),
onmessage(msg) {
if (msg.event === "data") {
innerLatest = reducer(innerLatest, JSON.parse(msg.data)?.ops);
setLatest(innerLatest);
chunkRef.current?.(JSON.parse(msg.data), innerLatest);
}
},
openWhenHidden: true,
onclose() {
setController(null);
successRef.current?.({ input, output: innerLatest?.final_output });
},
onerror(error) {
setController(null);
errorRef.current?.(error);
throw error;
},
});
}, []);
const stopStream = useCallback(() => {
controller?.abort();
setController(null);
}, [controller]);
return {
startStream,
stopStream: controller ? stopStream : undefined,
latest,
};
}
@@ -1,7 +0,0 @@
import clsx from "clsx";
import { ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
@@ -1,220 +0,0 @@
// (c) 2015 Chute Corporation. Released under the terms of the MIT License.
// Modified to use TypeScript and handle edge cases with tuples
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable no-prototype-builtins */
"use strict";
/**
* check whether item is plain object
* @param {*} item
* @return {Boolean}
*/
const isObject = (item: unknown): item is Record<string, unknown> => {
return (
typeof item === "object" &&
item !== null &&
item.toString() === {}.toString()
);
};
/**
* deep JSON object clone
*
* @param {Object} source
* @return {Object}
*/
const cloneJSON = (source: any): any => {
return JSON.parse(JSON.stringify(source));
};
/**
* returns a result of deep merge of two objects
*
* @param {Object} target
* @param {Object} source
* @return {Object}
*/
const merge = (
target: Record<string, unknown>,
source: Record<string, unknown>
) => {
target = cloneJSON(target);
for (const key in source) {
if (source.hasOwnProperty(key)) {
const sourceKeyValue = source[key];
const targetKeyValue = target[key];
if (isObject(sourceKeyValue) && isObject(targetKeyValue)) {
target[key] = merge(targetKeyValue, sourceKeyValue);
} else {
target[key] = sourceKeyValue;
}
}
}
return target;
};
/**
* get object by reference. works only with local references that points on
* definitions object
*
* @param {String} path
* @param {Object} definitions
* @return {Object}
*/
const getLocalRef = function (
inputPath: string,
definitions: Record<string, unknown>
) {
const path = inputPath.replace(/^#\/definitions\//, "").split("/");
const find = function (path: string[], root: any): any {
const key = path.shift();
if (!key) return {};
if (!root[key]) {
return {};
} else if (!path.length) {
return root[key];
} else {
return find(path, root[key]);
}
};
const result = find(path, definitions);
if (!isObject(result)) {
return result;
}
return cloneJSON(result);
};
/**
* merge list of objects from allOf properties
* if some of objects contains $ref field extracts this reference and merge it
*
* @param {Array} allOfList
* @param {Object} definitions
* @return {Object}
*/
const mergeAllOf = function (allOfList: any[], definitions: any) {
const length = allOfList.length;
let index = -1,
result = {};
while (++index < length) {
let item = allOfList[index];
item =
typeof item.$ref !== "undefined"
? getLocalRef(item.$ref, definitions)
: item;
result = merge(result, item);
}
return result;
};
/**
* returns a object that built with default values from json schema
*
* @param {Object} schema
* @param {Object} definitions
* @return {Object}
*/
const defaults = (schema: any, definitions: Record<string, any>): unknown => {
if (typeof schema["default"] !== "undefined") {
return schema["default"];
} else if (typeof schema.allOf !== "undefined") {
const mergedItem = mergeAllOf(schema.allOf, definitions);
return defaults(mergedItem, definitions);
} else if (typeof schema.$ref !== "undefined") {
const reference = getLocalRef(schema.$ref, definitions);
return defaults(reference, definitions);
} else if (schema.type === "object") {
if (!schema.properties) {
return {};
}
for (const key in schema.properties) {
if (schema.properties.hasOwnProperty(key)) {
schema.properties[key] = defaults(schema.properties[key], definitions);
if (typeof schema.properties[key] === "undefined") {
delete schema.properties[key];
}
}
}
return schema.properties;
} else if (schema.type === "array") {
if (!schema.items) {
return [];
}
// minimum item count
const ct = schema.minItems || 0;
// tuple-typed arrays
if (schema.items.constructor === Array) {
const values = schema.items.map((item: unknown) =>
defaults(item, definitions)
);
// remove undefined items at the end (unless required by minItems)
for (let i = values.length - 1; i >= 0; i--) {
if (typeof values[i] !== "undefined") {
break;
}
if (i + 1 > ct) {
values.pop();
}
}
// if all values are undefined -> return undefined even
// if minItems is set
if (values.every((item: unknown) => typeof item === "undefined")) {
return undefined;
}
return values;
}
// object-typed arrays
const value = defaults(schema.items, definitions);
if (typeof value === "undefined") {
return [];
} else {
const values = [];
for (let i = 0; i < Math.max(0, ct); i++) {
values.push(cloneJSON(value));
}
return values;
}
}
};
/**
* main function
*
* @param {Object} schema
* @param {Object|undefined} definitions
* @return {Object}
*/
export default function (
schema: any,
definitions?: Record<string, unknown> | undefined
) {
if (typeof definitions === "undefined") {
definitions = (schema.definitions as Record<string, unknown>) || {};
} else if (isObject(schema.definitions)) {
definitions = merge(definitions, schema.definitions);
}
return defaults(cloneJSON(schema), definitions);
}
-4
View File
@@ -1,4 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
export const JsonRefs: {
resolveRefs(schema: any): Promise<{ resolved: any }>;
};
File diff suppressed because it is too large Load Diff
@@ -1,7 +0,0 @@
export function getMessageContent(x: unknown) {
if (typeof x === "string") return x;
if (typeof x === "object" && x != null) {
if ("content" in x && typeof x.content === "string") return x.content;
}
return null;
}
@@ -1,31 +0,0 @@
function isAccessibleObject(x: unknown): x is Record<string | number, unknown> {
return typeof x === "object" && x != null;
}
export function getNormalizedJsonPath(
path: string | number | Array<string | number>
) {
return Array.isArray(path) ? path : [path];
}
export function traverseNaiveJsonPath(
x: unknown,
path: string | number | Array<string | number>
) {
const queue = getNormalizedJsonPath(path);
let tmp: unknown = x;
while (queue.length > 0) {
const first = queue.shift()!;
if (first === "") continue;
if (Array.isArray(tmp)) {
tmp = tmp[+first];
} else if (isAccessibleObject(tmp)) {
tmp = tmp[first];
} else {
return undefined;
}
}
return tmp;
}
@@ -1,28 +0,0 @@
import { JsonSchema } from "@jsonforms/core";
type JsonSchemaExtra = JsonSchema & {
extra: {
widget: {
type: string;
[key: string]: string | number | Array<string | number>;
};
};
};
export function isJsonSchemaExtra(x: JsonSchema): x is JsonSchemaExtra {
if (!("extra" in x && typeof x.extra === "object" && x.extra != null)) {
return false;
}
if (
!(
"widget" in x.extra &&
typeof x.extra.widget === "object" &&
x.extra.widget != null
)
) {
return false;
}
return true;
}
@@ -1,8 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { JsonRefs } from "./json-refs";
// jsonforms doesn't support schemas with root $ref
// so we resolve root $ref and replace it with the actual schema
export function simplifySchema(schema: any) {
return JsonRefs.resolveRefs(schema).then((r: any) => r.resolved);
}
@@ -1,5 +0,0 @@
export function str(o: unknown): React.ReactNode {
return typeof o === "object"
? JSON.stringify(o, null, 2)
: (o as React.ReactNode);
}
@@ -1,33 +0,0 @@
import { decompressFromEncodedURIComponent } from "lz-string";
export function getStateFromUrl(path: string) {
let configFromUrl = null;
let basePath = path;
if (basePath.endsWith("/")) {
basePath = basePath.slice(0, -1);
}
if (basePath.endsWith("/playground")) {
basePath = basePath.slice(0, -"/playground".length);
}
// check if we can omit the last segment
const [configHash, c, ...rest] = basePath.split("/").reverse();
if (c === "c") {
basePath = rest.reverse().join("/");
try {
configFromUrl = JSON.parse(decompressFromEncodedURIComponent(configHash));
} catch (error) {
console.error(error);
}
}
return { basePath, configFromUrl };
}
export function resolveApiUrl(path: string) {
const { basePath } = getStateFromUrl(window.location.href);
let prefix = new URL(basePath).pathname;
if (prefix.endsWith("/")) prefix = prefix.slice(0, -1);
return new URL(prefix + path, basePath);
}
-2
View File
@@ -1,2 +0,0 @@
/// <reference types="vite/client" />
/// <reference types="vite-plugin-svgr/client" />
@@ -1,36 +0,0 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {
colors: {
popover: {
DEFAULT: "hsl(var(--popover))",
},
background: {
DEFAULT: "var(--background)",
},
button: {
"green": "var(--button-green)",
"green-disabled": "var(--button-green-disabled)",
"inline": "var(--button-inline)"
},
ls: {
blue: "hsl(211.5, 91.8%, 61.8%)",
black: "hsl(var(--ls-black))",
gray: {
100: "hsl(var(--ls-gray-100))",
200: "hsl(var(--ls-gray-200))",
300: "hsl(var(--ls-gray-300))",
400: "hsl(var(--ls-gray-400))",
},
},
divider: {
500: "hsl(var(--divider-500))",
700: "hsl(var(--divider-700))",
},
},
},
},
plugins: [],
};
-25
View File
@@ -1,25 +0,0 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
@@ -1,10 +0,0 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
-18
View File
@@ -1,18 +0,0 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import svgr from "vite-plugin-svgr";
// https://vitejs.dev/config/
export default defineConfig({
base: "/____LANGSERVE_BASE_URL/",
plugins: [svgr(), react()],
server: {
proxy: {
"^/____LANGSERVE_BASE_URL.*/(config_schema|input_schema|output_schema|stream_log|feedback|public_trace_link)(/[a-zA-Z0-9-]*)?$": {
target: "http://127.0.0.1:8000",
changeOrigin: true,
rewrite: (path) => path.replace("/____LANGSERVE_BASE_URL", ""),
},
},
},
});
File diff suppressed because it is too large Load Diff
+62 -47
View File
@@ -22,21 +22,21 @@ from urllib.parse import urljoin
import httpx
from httpx._types import AuthTypes, CertTypes, CookieTypes, HeaderTypes, VerifyTypes
from langchain_core.callbacks import (
from langchain.callbacks.manager import (
AsyncCallbackManagerForChainRun,
CallbackManagerForChainRun,
)
from langchain_core.load.dump import dumpd
from langchain_core.runnables import Runnable
from langchain_core.runnables.config import (
from langchain.callbacks.tracers.log_stream import RunLogPatch
from langchain.load.dump import dumpd
from langchain.schema.runnable import Runnable
from langchain.schema.runnable.config import (
RunnableConfig,
ensure_config,
get_async_callback_manager_for_config,
get_callback_manager_for_config,
)
from langchain.schema.runnable.utils import AddableDict, Input, Output
from langchain_core.runnables.schema import StreamEvent
from langchain_core.runnables.utils import AddableDict, Input, Output
from langchain_core.tracers.log_stream import RunLogPatch
from typing_extensions import Literal
from langserve.callbacks import CallbackEventDict, ahandle_callbacks, handle_callbacks
@@ -45,7 +45,6 @@ from langserve.serialization import (
WellKnownLCSerializer,
load_events,
)
from langserve.server_sent_events import aconnect_sse, connect_sse
logger = logging.getLogger(__name__)
@@ -431,15 +430,11 @@ 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(f"kwargs not implemented yet. Got {kwargs}")
return self._batch_with_config(
self._batch, inputs, config, return_exceptions=return_exceptions
)
raise NotImplementedError("kwargs not implemented yet.")
return self._batch_with_config(self._batch, inputs, config)
async def _abatch(
self,
@@ -528,17 +523,25 @@ class RemoteRunnable(Runnable[Input, Output]):
}
endpoint = urljoin(self.url, "stream")
try:
from httpx_sse import connect_sse
except ImportError:
raise ImportError(
"Missing `httpx_sse` dependency to use the stream method. "
"Install via `pip install httpx_sse`'"
)
try:
with connect_sse(
self.sync_client, "POST", endpoint, json=data
) as event_source:
for sse in event_source.iter_sse():
if sse["event"] == "data":
chunk = self._lc_serializer.loads(sse["data"])
if sse.event == "data":
chunk = self._lc_serializer.loads(sse.data)
if isinstance(chunk, dict):
# Any dict returned from streaming end point
# is assumed to follow additive semantics
# and will be coverted to an AddableDict
# and will be converted to an AddableDict
# automatically
chunk = AddableDict(chunk)
yield chunk
@@ -560,21 +563,21 @@ class RemoteRunnable(Runnable[Input, Output]):
except TypeError:
final_output = None
final_output_supported = False
elif sse["event"] == "error":
elif sse.event == "error":
# This can only be a server side error
_raise_exception_from_data(
sse["data"], httpx.Request(method="POST", url=endpoint)
sse.data, httpx.Request(method="POST", url=endpoint)
)
elif sse["event"] == "metadata":
elif sse.event == "metadata":
# Nothing to do for metadata for the regular remote client.
continue
elif sse["event"] == "end":
elif sse.event == "end":
break
else:
_log_error_message_once(
f"Encountered an unsupported event type: `{sse['event']}`. "
f"Encountered an unsupported event type: `{sse.event}`. "
f"Try upgrading the remote client to the latest version."
f"Ignoring events of type `{sse['event']}`."
f"Ignoring events of type `{sse.event}`."
)
except BaseException as e:
run_manager.on_chain_error(e)
@@ -606,13 +609,18 @@ class RemoteRunnable(Runnable[Input, Output]):
}
endpoint = urljoin(self.url, "stream")
try:
from httpx_sse import aconnect_sse
except ImportError:
raise ImportError("You must install `httpx_sse` to use the stream method.")
try:
async with aconnect_sse(
self.async_client, "POST", endpoint, json=data
) as event_source:
async for sse in event_source.aiter_sse():
if sse["event"] == "data":
chunk = self._lc_serializer.loads(sse["data"])
if sse.event == "data":
chunk = self._lc_serializer.loads(sse.data)
if isinstance(chunk, dict):
# Any dict returned from streaming end point
# is assumed to follow additive semantics
@@ -639,21 +647,21 @@ class RemoteRunnable(Runnable[Input, Output]):
final_output = None
final_output_supported = False
elif sse["event"] == "error":
elif sse.event == "error":
# This can only be a server side error
_raise_exception_from_data(
sse["data"], httpx.Request(method="POST", url=endpoint)
sse.data, httpx.Request(method="POST", url=endpoint)
)
elif sse["event"] == "metadata":
elif sse.event == "metadata":
# Nothing to do for metadata for the regular remote client.
continue
elif sse["event"] == "end":
elif sse.event == "end":
break
else:
_log_error_message_once(
f"Encountered an unsupported event type: `{sse['event']}`. "
f"Encountered an unsupported event type: `{sse.event}`. "
f"Try upgrading the remote client to the latest version."
f"Ignoring events of type `{sse['event']}`."
f"Ignoring events of type `{sse.event}`."
)
except BaseException as e:
await run_manager.on_chain_error(e)
@@ -710,13 +718,18 @@ class RemoteRunnable(Runnable[Input, Output]):
}
endpoint = urljoin(self.url, "stream_log")
try:
from httpx_sse import aconnect_sse
except ImportError:
raise ImportError("You must install `httpx_sse` to use the stream method.")
try:
async with aconnect_sse(
self.async_client, "POST", endpoint, json=data
) as event_source:
async for sse in event_source.aiter_sse():
if sse["event"] == "data":
data = self._lc_serializer.loads(sse["data"])
if sse.event == "data":
data = self._lc_serializer.loads(sse.data)
# Create a copy of the data to yield since underlying
# code is using jsonpatch which does some stuff in-place
# that can cause unexpected consequences.
@@ -728,18 +741,18 @@ class RemoteRunnable(Runnable[Input, Output]):
final_output += chunk
else:
final_output = chunk
elif sse["event"] == "error":
elif sse.event == "error":
# This can only be a server side error
_raise_exception_from_data(
sse["data"], httpx.Request(method="POST", url=endpoint)
sse.data, httpx.Request(method="POST", url=endpoint)
)
elif sse["event"] == "end":
elif sse.event == "end":
break
else:
_log_error_message_once(
f"Encountered an unsupported event type: `{sse['event']}`. "
f"Encountered an unsupported event type: `{sse.event}`. "
f"Try upgrading the remote client to the latest version."
f"Ignoring events of type `{sse['event']}`."
f"Ignoring events of type `{sse.event}`."
)
except BaseException as e:
await run_manager.on_chain_error(e)
@@ -809,34 +822,36 @@ class RemoteRunnable(Runnable[Input, Output]):
"exclude_tags": exclude_tags,
}
endpoint = urljoin(self.url, "stream_events")
headers = kwargs.pop("headers", {})
headers["Accept"] = "text/event-stream"
headers["Cache-Control"] = "no-store"
try:
from httpx_sse import aconnect_sse
except ImportError:
raise ImportError("You must install `httpx_sse` to use the stream method.")
try:
async with aconnect_sse(
self.async_client, "POST", endpoint, json=data
) as event_source:
async for sse in event_source.aiter_sse():
if sse["event"] == "data":
event = self._lc_serializer.loads(sse["data"])
if sse.event == "data":
event = self._lc_serializer.loads(sse.data)
# Create a copy of the data to yield since underlying
# code is using jsonpatch which does some stuff in-place
# that can cause unexpected consequences.
yield event
events.append(event)
elif sse["event"] == "error":
elif sse.event == "error":
# This can only be a server side error
_raise_exception_from_data(
sse["data"], httpx.Request(method="POST", url=endpoint)
sse.data, httpx.Request(method="POST", url=endpoint)
)
elif sse["event"] == "end":
elif sse.event == "end":
break
else:
_log_error_message_once(
f"Encountered an unsupported event type: `{sse['event']}`. "
f"Encountered an unsupported event type: `{sse.event}`. "
f"Try upgrading the remote client to the latest version."
f"Ignoring events of type `{sse['event']}`."
f"Ignoring events of type `{sse.event}`."
)
except BaseException as e:
await run_manager.on_chain_error(e)
+9 -22
View File
@@ -2,11 +2,12 @@ import json
import mimetypes
import os
from string import Template
from typing import Literal, Sequence, Type
from typing import Sequence, Type
from fastapi.responses import Response
from langchain_core.runnables import Runnable
from pydantic import BaseModel
from langchain.schema.runnable import Runnable
from langserve.pydantic_v1 import BaseModel
class PlaygroundTemplate(Template):
@@ -49,37 +50,27 @@ def _get_mimetype(path: str) -> str:
async def serve_playground(
runnable: Runnable,
input_schema: Type[BaseModel],
output_schema: Type[BaseModel],
config_keys: Sequence[str],
base_url: str,
file_path: str,
feedback_enabled: bool,
public_trace_link_enabled: bool,
playground_type: Literal["default", "chat"],
) -> Response:
"""Serve the playground."""
if playground_type == "default":
path_to_dist = "./playground/dist"
elif playground_type == "chat":
path_to_dist = "./chat_playground/dist"
else:
raise ValueError(
f"Invalid playground type: {playground_type}. "
f"Use one of 'default' or 'chat'."
)
local_file_path = os.path.abspath(
os.path.join(
os.path.dirname(__file__),
path_to_dist,
"./playground/dist",
file_path or "index.html",
)
)
base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), path_to_dist))
base_dir = os.path.abspath(
os.path.join(os.path.dirname(__file__), "./playground/dist")
)
if base_dir != os.path.commonpath((base_dir, local_file_path)):
return Response("Not Found", status_code=404)
try:
with open(local_file_path, encoding="utf-8") as f:
mime_type = _get_mimetype(local_file_path)
@@ -92,13 +83,9 @@ async def serve_playground(
runnable.config_schema(include=config_keys).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"
),
LANGSERVE_PUBLIC_TRACE_LINK_ENABLED=json.dumps(
"true" if public_trace_link_enabled else "false"
),
)
else:
response = f.buffer.read()
File diff suppressed because one or more lines are too long
+1 -3
View File
@@ -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-400979f0.js"></script>
<script type="module" crossorigin src="/____LANGSERVE_BASE_URL/assets/index-dbc96538.js"></script>
<link rel="stylesheet" href="/____LANGSERVE_BASE_URL/assets/index-52e8ab2f.css">
</head>
<body>
@@ -14,9 +14,7 @@
try {
window.CONFIG_SCHEMA = ____LANGSERVE_CONFIG_SCHEMA;
window.INPUT_SCHEMA = ____LANGSERVE_INPUT_SCHEMA;
window.OUTPUT_SCHEMA = ____LANGSERVE_OUTPUT_SCHEMA;
window.FEEDBACK_ENABLED = ____LANGSERVE_FEEDBACK_ENABLED;
window.PUBLIC_TRACE_LINK_ENABLED = ____LANGSERVE_PUBLIC_TRACE_LINK_ENABLED;
} catch (error) {
// pass
}
-2
View File
@@ -12,9 +12,7 @@
try {
window.CONFIG_SCHEMA = ____LANGSERVE_CONFIG_SCHEMA;
window.INPUT_SCHEMA = ____LANGSERVE_INPUT_SCHEMA;
window.OUTPUT_SCHEMA = ____LANGSERVE_OUTPUT_SCHEMA;
window.FEEDBACK_ENABLED = ____LANGSERVE_FEEDBACK_ENABLED;
window.PUBLIC_TRACE_LINK_ENABLED = ____LANGSERVE_PUBLIC_TRACE_LINK_ENABLED;
} catch (error) {
// pass
}
@@ -6,30 +6,12 @@ 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(
@@ -52,11 +34,22 @@ export const chatMessagesTester = rankWith(
}
if ("anyOf" in schema.items && schema.items.anyOf != null) {
return schema.items.anyOf.every(checkItemSchema);
}
return schema.items.anyOf.every((schema) => {
const isObjectMessage =
schema.type === "object" &&
(schema.title?.endsWith("Message") ||
schema.title?.endsWith("MessageChunk"));
if ("oneOf" in schema.items && schema.items.oneOf != null) {
return schema.items.oneOf.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;
});
}
return false;
@@ -71,14 +64,10 @@ 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", () => {
@@ -92,10 +81,7 @@ 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
@@ -154,7 +140,7 @@ export const ChatMessagesControlRenderer = withJsonFormsControlProps(
props.path,
data.filter((_, i) => i !== index)
);
};
}
return (
<ChatMessageInput
message={message}
@@ -162,7 +148,7 @@ export const ChatMessagesControlRenderer = withJsonFormsControlProps(
handleRemoval={handleChatMessageRemoval}
path={props.path}
key={index}
></ChatMessageInput>
></ChatMessageInput>
);
})}
</div>
-4
View File
@@ -13,11 +13,7 @@ declare global {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
INPUT_SCHEMA?: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
OUTPUT_SCHEMA?: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
FEEDBACK_ENABLED?: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
PUBLIC_TRACE_LINK_ENABLED?: any;
}
}
+33
View File
@@ -0,0 +1,33 @@
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
+19 -82
View File
@@ -2,11 +2,9 @@ from datetime import datetime
from typing import Dict, List, Optional, Union
from uuid import UUID
from pydantic import (
BaseModel,
Field,
)
from pydantic import BaseModel as BaseModelV1
from pydantic import BaseModel # Floats between v1 and v2
from langserve.pydantic_v1 import BaseModel as BaseModelV1
class CustomUserType(BaseModelV1):
@@ -43,35 +41,14 @@ class SharedResponseMetadata(BaseModelV1):
pass
class FeedbackToken(BaseModelV1):
"""Represents the feedback tokens for a given request."""
key: str # The key of the feedback token
token_url: Optional[str] = None
expires_at: Optional[datetime] = None
class InvokeResponseMetadata(SharedResponseMetadata):
"""Represents response metadata used for just single input/output LangServe
class SingletonResponseMetadata(SharedResponseMetadata):
"""
Represents response metadata used for just single input/output LangServe
responses.
"""
# Represents the parent run id for a given request
run_id: UUID
feedback_tokens: List[FeedbackToken] = Field(
...,
description=(
"Feedback tokens from the given run."
"These tokens allow a user to provide feedback on the run."
"Only available if server was configured to provide feedback tokens."
),
)
# Alias for backwards compatibility
# Keep here in case clients are somehow using this for type checking
# TODO(Deprecate): This should be deprecated in 2025.
SingletonResponseMetadata = InvokeResponseMetadata
class BatchResponseMetadata(SharedResponseMetadata):
@@ -80,25 +57,17 @@ class BatchResponseMetadata(SharedResponseMetadata):
responses.
"""
# This namespace can include any additional metadata that is shared
# across all responses in the batch (e.g., if a batch run
# ID was a thing, it would go here)
# metadata for each individual response in the batch
# Parallel list of InvokeResponseMetadata objects matching
# the individual requests in the batch
responses: List[InvokeResponseMetadata]
# A list of UUIDs
# Represents each parent run id for a given request, in
# the same order in which they were received
run_ids: List[UUID] # For backwards compatibility, clients should not use this
run_ids: List[UUID]
class BaseFeedback(BaseModel):
"""Shared information between create requests of feedback and feedback objects"""
"""
Shared information between create requests of feedback and feedback objects
"""
run_id: Optional[UUID]
run_id: UUID
"""The associated run ID this feedback is logged for."""
key: str
@@ -114,34 +83,18 @@ class BaseFeedback(BaseModel):
"""Comment or explanation for the feedback."""
class FeedbackCreateRequestTokenBased(BaseModel):
"""Shared information between create requests of feedback and feedback objects."""
token_or_url: Union[UUID, str]
"""The associated run ID this feedback is logged for."""
score: Optional[Union[float, int, bool]] = None
"""Value or score to assign the run."""
value: Optional[Union[float, int, bool, str, Dict]] = None
"""The display value for the feedback if not a metric."""
comment: Optional[str] = None
"""Comment or explanation for the feedback."""
correction: Optional[Dict] = None
"""Correction for the run."""
metadata: Optional[Dict] = None
"""Metadata for the feedback."""
class FeedbackCreateRequest(BaseFeedback):
"""Represents a request that creates feedback for an individual run"""
"""
Represents a request that creates feedback for an individual run
"""
pass
class Feedback(BaseFeedback):
"""Represents feedback given on an individual run"""
"""
Represents feedback given on an individual run
"""
id: UUID
"""The unique ID of the feedback that was created."""
@@ -154,19 +107,3 @@ class Feedback(BaseFeedback):
correction: Optional[Dict] = None
"""Correction for the run."""
class PublicTraceLinkCreateRequest(BaseModel):
"""Represents a request that creates a public trace for an individual run."""
run_id: UUID
"""The unique ID of the run to share."""
class PublicTraceLink(BaseModel):
"""
Represents a public trace for an individual run
"""
public_url: str
"""Public URL for the trace."""
+44 -42
View File
@@ -13,12 +13,14 @@ sensitive information from the server to the client.
import abc
import logging
from functools import lru_cache
from typing import Annotated, Any, Dict, List, Union
from typing import Any, Dict, List, Union
import orjson
from langchain_core.agents import AgentAction, AgentActionMessageLog, AgentFinish
from langchain_core.documents import Document
from langchain_core.messages import (
from langchain.prompts.base import StringPromptValue
from langchain.prompts.chat import ChatPromptValueConcrete
from langchain.schema.agent import AgentAction, AgentActionMessageLog, AgentFinish
from langchain.schema.document import Document
from langchain.schema.messages import (
AIMessage,
AIMessageChunk,
ChatMessage,
@@ -30,15 +32,14 @@ from langchain_core.messages import (
SystemMessage,
SystemMessageChunk,
)
from langchain_core.outputs import (
from langchain.schema.output import (
ChatGeneration,
ChatGenerationChunk,
Generation,
LLMResult,
)
from langchain_core.prompt_values import ChatPromptValueConcrete
from langchain_core.prompts.base import StringPromptValue
from pydantic import BaseModel, Field, RootModel, ValidationError
from langserve.pydantic_v1 import BaseModel, ValidationError
from langserve.validation import CallbackEvent
logger = logging.getLogger(__name__)
@@ -50,35 +51,36 @@ def _log_error_message_once(error_message: str) -> None:
logger.error(error_message)
# 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.
WellKnownLCObject = RootModel[
Annotated[
Union[
Document,
HumanMessage,
SystemMessage,
ChatMessage,
FunctionMessage,
AIMessage,
HumanMessageChunk,
SystemMessageChunk,
ChatMessageChunk,
FunctionMessageChunk,
AIMessageChunk,
StringPromptValue,
ChatPromptValueConcrete,
AgentAction,
AgentFinish,
AgentActionMessageLog,
ChatGeneration,
Generation,
ChatGenerationChunk,
],
Field(discriminator="type"),
class WellKnownLCObject(BaseModel):
"""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,
]
]
def default(obj) -> Any:
@@ -94,12 +96,12 @@ def _decode_lc_objects(value: Any) -> Any:
v = {key: _decode_lc_objects(v) for key, v in value.items()}
try:
obj = WellKnownLCObject.model_validate(v)
parsed = obj.root
obj = WellKnownLCObject.parse_obj(v)
parsed = obj.__root__
if set(parsed.dict()) != set(value):
raise ValueError("Invalid object")
return parsed
except (ValidationError, ValueError, TypeError):
except (ValidationError, ValueError):
return v
elif isinstance(value, list):
return [_decode_lc_objects(item) for item in value]
@@ -122,11 +124,11 @@ def _decode_event_data(value: Any) -> Any:
if isinstance(value, dict):
try:
obj = CallbackEvent.parse_obj(value)
return obj.root
return obj.__root__
except ValidationError:
try:
obj = WellKnownLCObject.parse_obj(value)
return obj.root
return obj.__root__
except ValidationError:
return {key: _decode_event_data(v) for key, v in value.items()}
elif isinstance(value, list):
@@ -215,7 +217,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
+364 -400
View File
@@ -15,15 +15,14 @@ from typing import (
Union,
)
from langchain_core.runnables import Runnable
from pydantic import BaseModel
from langchain.schema.runnable import Runnable
from typing_extensions import Annotated
from langserve.api_handler import (
APIHandler,
PerRequestConfigModifier,
TokenFeedbackConfig,
_is_hosted,
from langserve.api_handler import APIHandler, PerRequestConfigModifier, _is_hosted
from langserve.pydantic_v1 import (
_PYDANTIC_MAJOR_VERSION,
PYDANTIC_VERSION,
BaseModel,
)
try:
@@ -40,15 +39,71 @@ except ImportError:
# Duplicated model names break fastapi's openapi generation.
_APP_SEEN = weakref.WeakSet()
# Keeps track of the paths that have been associated with each app.
# Each runnable registered with an APP will have a unique path.
# An APP can have multiple runnables registered with it.
# There are multiple APPs as it's common to use APIRouter in larger
# FastAPI applications.
_APP_TO_PATHS = weakref.WeakKeyDictionary()
def _setup_global_app_handlers(app: Union[FastAPI, APIRouter]) -> None:
@app.on_event("startup")
async def startup_event():
LANGSERVE = r"""
__ ___ .__ __. _______ _______. _______ .______ ____ ____ _______
| | / \ | \ | | / _____| / || ____|| _ \ \ \ / / | ____|
| | / ^ \ | \| | | | __ | (----`| |__ | |_) | \ \/ / | |__
| | / /_\ \ | . ` | | | |_ | \ \ | __| | / \ / | __|
| `----./ _____ \ | |\ | | |__| | .----) | | |____ | |\ \----. \ / | |____
|_______/__/ \__\ |__| \__| \______| |_______/ |_______|| _| `._____| \__/ |_______|
""" # noqa: E501
def green(text: str) -> str:
"""Return the given text in green."""
return "\x1b[1;32;40m" + text + "\x1b[0m"
def orange(text: str) -> str:
"""Return the given text in orange."""
return "\x1b[1;31;40m" + text + "\x1b[0m"
paths = _APP_TO_PATHS[app]
print(LANGSERVE)
for path in paths:
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()
def _register_path_for_app(app: Union[FastAPI, APIRouter], path: str) -> None:
"""Register a path when its added to app. Raise if path already seen."""
if app in _APP_TO_PATHS:
seen_paths = _APP_TO_PATHS.get(app)
if path in seen_paths:
raise ValueError(
f"A runnable already exists at path: {path}. If adding "
f"multiple runnables make sure they have different paths."
)
seen_paths.add(path)
else:
_setup_global_app_handlers(app)
_APP_TO_PATHS[app] = {path}
# This is the type annotation
EndpointName = Literal[
"invoke",
@@ -58,7 +113,6 @@ EndpointName = Literal[
"stream_events",
"playground",
"feedback",
"public_trace_link",
"input_schema",
"config_schema",
"output_schema",
@@ -75,8 +129,6 @@ KNOWN_ENDPOINTS = {
"stream_events",
"playground",
"feedback",
"token_feedback",
"public_trace_link",
"input_schema",
"config_schema",
"output_schema",
@@ -93,7 +145,6 @@ class _EndpointConfiguration:
enabled_endpoints: Optional[Sequence[EndpointName]] = None,
disabled_endpoints: Optional[Sequence[EndpointName]] = None,
enable_feedback_endpoint: bool = False,
enable_public_trace_link_endpoint: bool = False,
) -> None:
"""Initialize the endpoint configuration."""
if enabled_endpoints and disabled_endpoints:
@@ -127,7 +178,6 @@ class _EndpointConfiguration:
is_output_schema_enabled = True
is_config_schema_enabled = True
is_config_hash_enabled = True
is_token_feedback_enabled = True
else:
disabled_endpoints_ = set(name.lower() for name in disabled_endpoints)
if disabled_endpoints_ - KNOWN_ENDPOINTS:
@@ -145,7 +195,6 @@ class _EndpointConfiguration:
is_output_schema_enabled = "output_schema" not in disabled_endpoints_
is_config_schema_enabled = "config_schema" not in disabled_endpoints_
is_config_hash_enabled = "config_hashes" not in disabled_endpoints_
is_token_feedback_enabled = "token_feedback" not in disabled_endpoints_
else:
enabled_endpoints_ = set(name.lower() for name in enabled_endpoints)
if enabled_endpoints_ - KNOWN_ENDPOINTS:
@@ -162,7 +211,6 @@ class _EndpointConfiguration:
is_output_schema_enabled = "output_schema" in enabled_endpoints_
is_config_schema_enabled = "config_schema" in enabled_endpoints_
is_config_hash_enabled = "config_hashes" in enabled_endpoints_
is_token_feedback_enabled = "token_feedback" in enabled_endpoints_
self.is_invoke_enabled = is_invoke_enabled
self.is_batch_enabled = is_batch_enabled
@@ -175,63 +223,6 @@ class _EndpointConfiguration:
self.is_config_schema_enabled = is_config_schema_enabled
self.is_config_hash_enabled = is_config_hash_enabled
self.is_feedback_enabled = enable_feedback_endpoint
self.is_public_trace_link_enabled = enable_public_trace_link_endpoint
self.is_token_feedback_enabled = is_token_feedback_enabled
def _register_path_for_app(
app: Union[FastAPI, APIRouter],
path: str,
endpoint_configuration: _EndpointConfiguration,
) -> None:
"""Register a path when its added to app. Raise if path already seen."""
if app in _APP_TO_PATHS:
seen_paths = _APP_TO_PATHS.get(app)
if path in seen_paths:
raise ValueError(
f"A runnable already exists at path: {path}. If adding "
f"multiple runnables make sure they have different paths."
)
seen_paths.add(path)
else:
_setup_global_app_handlers(app, endpoint_configuration)
_APP_TO_PATHS[app] = {path}
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
def green(text: str) -> str:
"""Return the given text in green."""
return "\x1b[1;32;40m" + text + "\x1b[0m"
def orange(text: str) -> str:
"""Return the given text in orange."""
return "\x1b[1;31;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}/')
# PUBLIC API
@@ -248,13 +239,10 @@ def add_routes(
include_callback_events: bool = False,
per_req_config_modifier: Optional[PerRequestConfigModifier] = None,
enable_feedback_endpoint: bool = _is_hosted(),
token_feedback_config: Optional[TokenFeedbackConfig] = None,
enable_public_trace_link_endpoint: bool = False,
disabled_endpoints: Optional[Sequence[EndpointName]] = None,
stream_log_name_allow_list: Optional[Sequence[str]] = None,
enabled_endpoints: Optional[Sequence[EndpointName]] = None,
dependencies: Optional[Sequence[Depends]] = None,
playground_type: Literal["default", "chat"] = "default",
) -> None:
"""Register the routes on the given FastAPI app or APIRouter.
@@ -302,28 +290,12 @@ def add_routes(
enable_feedback_endpoint: Whether to enable an endpoint for logging feedback
to LangSmith. Enabled by default. If this flag is disabled or LangSmith
tracing is not enabled for the runnable, then 400 errors will be thrown
when accessing the feedback endpoint.
token_feedback_config: optional configuration for token based feedback.
**Attention** this is distinct from `enable_feedback_endpoint`.
When provided, feedback tokens will be included in the response
metadata that can be used to provide feedback on the run.
In addition, an endpoint will be created for submitting feedback
using the feedback tokens. This is a safer option for public facing
APIs as they scope the feedback to a specific run id and key
and include an expiration time.
This endpoint will be created at /token_feedback
**BETA**: This feature is in beta and may change in the future.
enable_public_trace_link_endpoint: Whether to enable an endpoint for
end-users to publicly view LangSmith traces of your chain runs.
WARNING: THIS WILL EXPOSE THE INTERNAL STATE OF YOUR RUN AND CHAIN AS
A PUBLICLY ACCESSIBLE LINK.
If this flag is disabled or LangSmith tracing is not enabled for
the runnable, then 400 errors will be thrown when accessing the endpoint.
when accessing the feedback endpoint
enabled_endpoints: A list of endpoints which should be enabled. If not
specified, all associated endpoints will be enabled. The list can contain
the following values: *invoke*, *batch*, *stream*, *stream_log*,
*playground*, *input_schema*, *output_schema*,
*config_schema*, *config_hashes*.
*playground*, *input_schema*, *output_schema*, *config_schema*,
*config_hashes*.
*config_hashes* represents the config hash variant (when it exists)
of each endpoint. Enabling this is useful when working with configurable
@@ -340,14 +312,13 @@ def add_routes(
)
```
Please note that the feedback endpoint and public trace link endpoints
are not included in this list and are controlled by their
respective flags.
Please note that the feedback endpoint is not included in this list
and is controlled by the `enable_feedback_endpoint` flag.
disabled_endpoints: A list of endpoints which should be disabled. If not
specified, all associated endpoints will be enabled. The list can contain
the following values: *invoke*, *batch*, *stream*, *stream_log*,
*playground*, *input_schema*, *output_schema*,
*config_schema*, *config_hashes*.
*playground*, *input_schema*, *output_schema*, *config_schema*,
*config_hashes*.
*config_hashes* represents the config hash variant (when it exists)
of each endpoint. Enabling this is useful when working with configurable
@@ -367,27 +338,11 @@ def add_routes(
stream as intermediate steps
dependencies: list of dependencies to be applied to the *path operation*.
See [FastAPI docs for Dependencies in path operation decorators](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-path-operation-decorators/).
playground_type: The type of playground to serve. The default is "default".
- default: supports more types of inputs / outputs. Not optimized
for any particular use case.
- chat: UX is optimized for chat-like interactions. Please review
the README in langserve for more details about constraints (e.g.,
which message types are supported etc.)
""" # noqa: E501
if not isinstance(runnable, Runnable):
raise TypeError(
f"Expected a Runnable, got {type(runnable)}. "
f"The second argument to add_routes should be a Runnable instance."
f"add_route(app, runnable, ...) is the correct usage."
f"Please make sure that you are using a runnable which is an instance of "
f"langchain_core.runnables.Runnable."
)
endpoint_configuration = _EndpointConfiguration(
enabled_endpoints=enabled_endpoints,
disabled_endpoints=disabled_endpoints,
enable_feedback_endpoint=enable_feedback_endpoint,
enable_public_trace_link_endpoint=enable_public_trace_link_endpoint,
)
try:
@@ -408,7 +363,7 @@ def add_routes(
if isinstance(app, FastAPI): # type: ignore
# Cannot do this checking logic for a router since
# API routers are not hashable
_register_path_for_app(app, path, endpoint_configuration)
_register_path_for_app(app, path)
# Determine the base URL for the playground endpoint
prefix = app.prefix if isinstance(app, APIRouter) else "" # type: ignore
@@ -431,11 +386,8 @@ def add_routes(
config_keys=config_keys,
include_callback_events=include_callback_events,
enable_feedback_endpoint=enable_feedback_endpoint,
token_feedback_config=token_feedback_config,
enable_public_trace_link_endpoint=enable_public_trace_link_endpoint,
per_req_config_modifier=per_req_config_modifier,
stream_log_name_allow_list=stream_log_name_allow_list,
playground_type=playground_type,
)
namespace = path or ""
@@ -455,9 +407,35 @@ def add_routes(
if hasattr(app, "openapi_tags") and (path or (app not in _APP_SEEN)):
if not path:
_APP_SEEN.add(app)
default_endpoint_tags = {
"name": route_tags[0] if route_tags else "default",
}
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}"
)
if endpoint_configuration.is_config_hash_enabled:
app.openapi_tags = [
*(getattr(app, "openapi_tags", []) or []),
@@ -702,12 +680,6 @@ def add_routes(
include_in_schema=False,
)(playground)
if endpoint_configuration.is_token_feedback_enabled:
app.post(
namespace + "/token_feedback",
dependencies=dependencies,
)(api_handler.create_feedback_from_token)
if enable_feedback_endpoint:
app.post(
namespace + "/feedback",
@@ -719,341 +691,333 @@ def add_routes(
dependencies=dependencies,
)(api_handler._check_feedback_enabled)
if enable_public_trace_link_endpoint:
app.put(
namespace + "/public_trace_link",
dependencies=dependencies,
)(api_handler.create_public_trace_link)
app.head(
namespace + "/public_trace_link",
dependencies=dependencies,
)(api_handler._check_public_trace_link_enabled)
#######################################
# Documentation variants of end points.
#######################################
# At the moment, we only support pydantic 1.x for documentation
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 _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
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",
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",
invoke_docs = app.post(
f"{namespace}/invoke",
response_model=api_handler.InvokeResponse,
tags=route_tags_with_config,
name=_route_name_with_config("invoke"),
tags=route_tags,
name=_route_name("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)
)(_invoke_docs)
if endpoint_configuration.is_batch_enabled:
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)
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.")
if endpoint_configuration.is_batch_enabled:
batch_docs = app.post(
f"{namespace}/batch",
response_model=BatchResponse,
tags=route_tags,
name=_route_name("batch"),
dependencies=dependencies,
)(_batch_docs)
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.")
if endpoint_configuration.is_config_hash_enabled:
app.post(
namespace + "/c/{config_hash}/batch",
batch_docs = app.post(
f"{namespace}/batch",
response_model=BatchResponse,
tags=route_tags_with_config,
name=_route_name_with_config("batch"),
tags=route_tags,
name=_route_name("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)
)(_batch_docs)
if endpoint_configuration.is_stream_enabled:
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)
async def _stream_docs(
stream_request: Annotated[StreamRequest, StreamRequest],
config_hash: str = "",
) -> EventSourceResponse:
"""Invoke the runnable stream the output.
if endpoint_configuration.is_stream_enabled:
This endpoint allows to stream the output of the runnable.
async def _stream_docs(
stream_request: Annotated[StreamRequest, StreamRequest],
config_hash: str = "",
) -> EventSourceResponse:
"""Invoke the runnable stream the output.
The endpoint uses a server sent event stream to stream the output.
This endpoint allows to stream the output of the runnable.
https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events
The endpoint uses a server sent event stream to stream the output.
Important: Set the "text/event-stream" media type for request headers if
not using an existing SDK.
https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events
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.
Important: Set the "text/event-stream" media type for request headers if
not using an existing SDK.
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 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.
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
Here are some examples of events that the endpoint can send:
Regular streaming event:
{
"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"
Internal server error:
{
"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.")
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(
namespace + "/c/{config_hash}/stream_log",
stream_docs = app.post(
f"{namespace}/stream",
include_in_schema=True,
tags=route_tags_with_config,
name=_route_name_with_config("stream_log"),
tags=route_tags,
name=_route_name("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_log endpoint without "
"the `c/{config_hash}` path parameter."
"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 has_astream_events and endpoint_configuration.is_stream_events_enabled:
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)
async def _stream_events_docs(
stream_events_request: Annotated[StreamEventsRequest, StreamEventsRequest],
config_hash: str = "",
) -> EventSourceResponse:
"""Stream events from the given runnable.
if has_astream_events and endpoint_configuration.is_stream_events_enabled:
This endpoint allows to stream events from the runnable, including
events from all intermediate steps.
async def _stream_events_docs(
stream_events_request: Annotated[
StreamEventsRequest, StreamEventsRequest
],
config_hash: str = "",
) -> EventSourceResponse:
"""Stream events from the given runnable.
**Attention**
This endpoint allows to stream events from the runnable, including
events from all intermediate steps.
This is a new endpoint that only works for langchain-core >= 0.1.14.
**Attention**
It belongs to a Beta API that may change in the future.
This is a new endpoint that only works for langchain-core >= 0.1.14.
**Important**
Specify filters to the events you want to receive by setting
the appropriate filters in the request body.
It belongs to a Beta API that may change in the future.
This will help avoid sending too much data over the network.
**Important**
Specify filters to the events you want to receive by setting
the appropriate filters in the request body.
It will also prevent serialization issues with
any unsupported types since it won't need to serialize events
that aren't transmitted.
This will help avoid sending too much data over the network.
The endpoint uses a server sent event stream to stream the output.
It will also prevent serialization issues with
any unsupported types since it won't need to serialize events
that aren't transmitted.
https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events
The endpoint uses a server sent event stream to stream the output.
The encoding of events follows the following format:
https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events
* data - for streaming the output of the runnable
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.
{
"event": "data",
"event": "error",
"data": {
...
"status_code": 500,
"message": "Internal Server Error"
}
}
* error - for signaling an error in the stream, also ends the stream.
* end - for signaling the end of the stream.
{
"event": "error",
"data": {
"status_code": 500,
"message": "Internal Server Error"
}
}
This helps the client to know when to stop listening for events and
know that the streaming has ended successfully.
* end - for signaling the end of the stream.
{
"event": "end",
}
This helps the client to know when to stop listening for events and
know that the streaming has ended successfully.
`data` for the `data` event is a JSON object that corresponds
to a serialized representation of a StreamEvent.
{
"event": "end",
}
See LangChain documentation for more information about astream_events.
"""
raise AssertionError("This endpoint should not be reachable.")
`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(
namespace + "/c/{config_hash}/stream_events",
f"{namespace}/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."
),
tags=route_tags,
name=_route_name("stream_events"),
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)

Some files were not shown because too many files have changed in this diff Show More