mirror of
https://github.com/langchain-ai/langserve.git
synced 2026-07-21 00:55:22 -04:00
Compare commits
114 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d0f8d04b5 | |||
| 3c94bf39b4 | |||
| ff5551e651 | |||
| a37b50cdf3 | |||
| 470f047c00 | |||
| b45a082b60 | |||
| 39ec0b42ad | |||
| 6ab86af83f | |||
| b3dd8a6ab3 | |||
| 1d6ffbc29f | |||
| e26a946e6c | |||
| 49ee20c75a | |||
| f044ddb755 | |||
| f85c1b37dd | |||
| 15a3c9b6a1 | |||
| 559f990d93 | |||
| 76669c2437 | |||
| cf180b81da | |||
| b3e728d95d | |||
| 08b12b3e22 | |||
| 93490c294d | |||
| 3c3e872b1d | |||
| 245ace4779 | |||
| aefd94aa50 | |||
| 5eaff89403 | |||
| 34b96e5aa5 | |||
| c1e0fb2a64 | |||
| 352b196e7a | |||
| 87b54a3f0d | |||
| 3c6cce8616 | |||
| 5ec58217cf | |||
| f66933452e | |||
| 7a82f03454 | |||
| b4f6ba0a10 | |||
| 76e9e83540 | |||
| 4a8ec72754 | |||
| f85f4a0b04 | |||
| 2d29e6d5ad | |||
| e2c4f50b16 | |||
| 699e95324f | |||
| e9917a3505 | |||
| 2ae30fbd6a | |||
| 483e24fecb | |||
| 5feccb62a7 | |||
| 942bc16db5 | |||
| 4acd480f2b | |||
| 4a283dd34f | |||
| b3c2577cf9 | |||
| 44b375cfdd | |||
| 177463aca0 | |||
| 98eeeb6ea4 | |||
| ce0428218c | |||
| 160ca683bc | |||
| 678d5b7151 | |||
| 19382cec4b | |||
| aa7f71a3b0 | |||
| 495a468739 | |||
| a51a6c0c09 | |||
| 04a950e770 | |||
| 477af9e6c4 | |||
| af904f5f6e | |||
| bcf833c3d4 | |||
| 728406056d | |||
| abba59ea55 | |||
| d4e7f3679c | |||
| 16dfd306dd | |||
| 7861659368 | |||
| 099fc666e5 | |||
| 0e971b367f | |||
| eef38b532d | |||
| b60b52daf6 | |||
| eb78a3f78d | |||
| 798739c406 | |||
| d8ffff9926 | |||
| 286d254c72 | |||
| 0adf0e7e3a | |||
| 22831cabed | |||
| 078453f2c4 | |||
| a01f547694 | |||
| b53fa466b0 | |||
| 4b62a4660d | |||
| f0ed9830b0 | |||
| 65dfbceac4 | |||
| 9ee157e91c | |||
| 1b61b04922 | |||
| f656c5f565 | |||
| e6323afadf | |||
| 7372bcff3a | |||
| abe9bb79ea | |||
| 2f4bcdd144 | |||
| 38b935644f | |||
| d591be4dce | |||
| ea7bb5d970 | |||
| d8d3262987 | |||
| fe3abc8914 | |||
| c93e3fde2c | |||
| 7e1a1a17c7 | |||
| 13dcfb4b26 | |||
| 6f917431bb | |||
| e8f441774a | |||
| f5a97a6db3 | |||
| 4de13afe0e | |||
| c2606efdbb | |||
| 35b31cf39a | |||
| 8e2e9a03b5 | |||
| 98edac4097 | |||
| 1078a86017 | |||
| 2eb2ed9d76 | |||
| e188f2e643 | |||
| 5b46eabed4 | |||
| c1a77fcad3 | |||
| 33b3f14b4b | |||
| b1479d601e | |||
| 87f4a1f6c7 |
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"contributors": ["eyurtsev", "hwchase17", "nfcampos", "efriis", "jacoblee93", "dqbd", "kreneskyp", "adarsh-jha-dev", "harris", "baskaryan", "hinthornw", "bracesproul", "jakerachleff"],
|
||||
"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/Ljhqvt9Gdi1N385W6 .\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."
|
||||
"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"],
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ env:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
timeout-minutes: 10
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
|
||||
@@ -11,6 +11,7 @@ on:
|
||||
- '.github/workflows/_test.yml'
|
||||
- '.github/workflows/langserve_ci.yml'
|
||||
- 'langserve/**'
|
||||
- 'tests/**'
|
||||
- 'examples/**'
|
||||
- 'pyproject.toml'
|
||||
- 'poetry.lock'
|
||||
@@ -46,6 +47,7 @@ jobs:
|
||||
working-directory: .
|
||||
secrets: inherit
|
||||
test:
|
||||
timeout-minutes: 10
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
|
||||
@@ -1,51 +1,83 @@
|
||||
# LangServe 🦜️🏓
|
||||
# 🦜️🏓 LangServe
|
||||
|
||||
[](https://github.com/langchain-ai/langserve/releases)
|
||||
[](https://pepy.tech/project/langserve)
|
||||
[](https://github.com/langchain-ai/langserve/issues)
|
||||
[](https://discord.com/channels/1038097195422978059/1170024642245832774)
|
||||
|
||||
🚩 We will be releasing a hosted version of LangServe for one-click deployments of LangChain applications. [Sign up here](https://airtable.com/app0hN6sd93QcKubv/shrAjst60xXa6quV2) to get on the waitlist.
|
||||
🚩 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` helps developers deploy `LangChain` [runnables and chains](https://python.langchain.com/docs/expression_language/) as a REST API.
|
||||
`LangServe` helps developers
|
||||
deploy `LangChain` [runnables and chains](https://python.langchain.com/docs/expression_language/)
|
||||
as a REST API.
|
||||
|
||||
This library is integrated with [FastAPI](https://fastapi.tiangolo.com/) and uses [pydantic](https://docs.pydantic.dev/latest/) for data validation.
|
||||
This library is integrated with [FastAPI](https://fastapi.tiangolo.com/) and
|
||||
uses [pydantic](https://docs.pydantic.dev/latest/) for data validation.
|
||||
|
||||
In addition, it provides a client that can be used to call into runnables deployed on a server.
|
||||
A javascript client is available in [LangChainJS](https://js.langchain.com/docs/api/runnables_remote/classes/RemoteRunnable).
|
||||
In addition, it provides a client that can be used to call into runnables deployed on a
|
||||
server.
|
||||
A javascript client is available
|
||||
in [LangChainJS](https://js.langchain.com/docs/api/runnables_remote/classes/RemoteRunnable).
|
||||
|
||||
## Features
|
||||
|
||||
- Input and Output schemas automatically inferred from your LangChain object, and enforced on every API call, with rich error messages
|
||||
- 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 concurrent requests on a single server
|
||||
- `/stream_log` endpoint for streaming all (or some) intermediate steps from your chain/agent
|
||||
- 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/)])
|
||||
- All built with battle-tested open-source Python libraries like FastAPI, Pydantic, uvloop and asyncio.
|
||||
- Use the client SDK to call a LangServe server as if it was a Runnable running locally (or call the HTTP API directly)
|
||||
- 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
|
||||
chain/agent
|
||||
- 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/))
|
||||
- All built with battle-tested open-source Python libraries like FastAPI, Pydantic,
|
||||
uvloop and asyncio.
|
||||
- Use the client SDK to call a LangServe server as if it was a Runnable running
|
||||
locally (or call the HTTP API directly)
|
||||
- [LangServe Hub](https://github.com/langchain-ai/langchain/blob/master/templates/README.md)
|
||||
|
||||
### Limitations
|
||||
## Limitations
|
||||
|
||||
- Client callbacks are not yet supported for events that originate on the server
|
||||
- Does not work with [pydantic v2 yet](https://github.com/tiangolo/fastapi/issues/10360)
|
||||
- OpenAPI docs will not be generated when using Pydantic V2. Fast API does not
|
||||
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/app0hN6sd93QcKubv/shrAjst60xXa6quV2) to get on the waitlist.
|
||||
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.
|
||||
|
||||
## Security
|
||||
|
||||
* 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).
|
||||
* 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).
|
||||
|
||||
## Installation
|
||||
|
||||
For both client and server:
|
||||
|
||||
```bash
|
||||
pip install "langserve[all]"
|
||||
```
|
||||
|
||||
or `pip install "langserve[client]"` for client code,
|
||||
and `pip install "langserve[server]"` for server code.
|
||||
|
||||
## LangChain CLI 🛠️
|
||||
|
||||
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`
|
||||
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`.
|
||||
|
||||
```sh
|
||||
@@ -54,16 +86,39 @@ langchain app new ../path/to/directory
|
||||
|
||||
## Examples
|
||||
|
||||
Get your LangServe instance started quickly with
|
||||
Get your LangServe instance started quickly with
|
||||
[LangChain Templates](https://github.com/langchain-ai/langchain/blob/master/templates/README.md).
|
||||
|
||||
For more examples, see the templates
|
||||
[index](https://github.com/langchain-ai/langchain/blob/master/templates/docs/INDEX.md)
|
||||
or the [examples](./examples) directory.
|
||||
For more examples, see the templates
|
||||
[index](https://github.com/langchain-ai/langchain/blob/master/templates/docs/INDEX.md)
|
||||
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** Implementation of an [Agent 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) |
|
||||
| [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) |
|
||||
| **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) |
|
||||
| **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/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) |
|
||||
|
||||
## Sample Application
|
||||
|
||||
### Server
|
||||
|
||||
Here's a server that deploys an OpenAI chat model, an Anthropic chat model, and a chain that uses
|
||||
Here's a server that deploys an OpenAI chat model, an Anthropic chat model, and a chain
|
||||
that uses
|
||||
the Anthropic model to tell a joke about a topic.
|
||||
|
||||
```python
|
||||
@@ -73,11 +128,10 @@ from langchain.prompts import ChatPromptTemplate
|
||||
from langchain.chat_models import ChatAnthropic, ChatOpenAI
|
||||
from langserve import add_routes
|
||||
|
||||
|
||||
app = FastAPI(
|
||||
title="LangChain Server",
|
||||
version="1.0",
|
||||
description="A simple api server using Langchain's Runnable interfaces",
|
||||
title="LangChain Server",
|
||||
version="1.0",
|
||||
description="A simple api server using Langchain's Runnable interfaces",
|
||||
)
|
||||
|
||||
add_routes(
|
||||
@@ -97,7 +151,7 @@ prompt = ChatPromptTemplate.from_template("tell me a joke about {topic}")
|
||||
add_routes(
|
||||
app,
|
||||
prompt | model,
|
||||
path="/chain",
|
||||
path="/joke",
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
@@ -110,17 +164,18 @@ if __name__ == "__main__":
|
||||
|
||||
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.
|
||||
|
||||
```sh
|
||||
curl localhost:8000/docs
|
||||
```
|
||||
|
||||
make sure to **add** the `/docs` suffix.
|
||||
make sure to **add** the `/docs` suffix.
|
||||
|
||||
Below will return a 404 until you define a `@app.get("/")`
|
||||
|
||||
```sh
|
||||
localhost:8000
|
||||
```
|
||||
> ⚠️ Index page `/` is not defined by **design**, so `curl localhost:8000` or visiting
|
||||
> the URL
|
||||
> will return a 404. If you want content at `/` define an endpoint `@app.get("/")`.
|
||||
|
||||
### Client
|
||||
|
||||
@@ -135,7 +190,7 @@ from langserve import RemoteRunnable
|
||||
|
||||
openai = RemoteRunnable("http://localhost:8000/openai/")
|
||||
anthropic = RemoteRunnable("http://localhost:8000/anthropic/")
|
||||
joke_chain = RemoteRunnable("http://localhost:8000/chain/")
|
||||
joke_chain = RemoteRunnable("http://localhost:8000/joke/")
|
||||
|
||||
joke_chain.invoke({"topic": "parrots"})
|
||||
|
||||
@@ -161,19 +216,19 @@ chain = prompt | RunnableMap({
|
||||
"anthropic": anthropic,
|
||||
})
|
||||
|
||||
chain.batch([{ "topic": "parrots" }, { "topic": "cats" }])
|
||||
chain.batch([{"topic": "parrots"}, {"topic": "cats"}])
|
||||
```
|
||||
|
||||
In TypeScript (requires LangChain.js version 0.0.166 or later):
|
||||
|
||||
```typescript
|
||||
import { RemoteRunnable } from "langchain/runnables/remote";
|
||||
import {RemoteRunnable} from "langchain/runnables/remote";
|
||||
|
||||
const chain = new RemoteRunnable({
|
||||
url: `http://localhost:8000/chain/invoke/`,
|
||||
url: `http://localhost:8000/joke/`,
|
||||
});
|
||||
const result = await chain.invoke({
|
||||
topic: "cats",
|
||||
topic: "cats",
|
||||
});
|
||||
```
|
||||
|
||||
@@ -181,8 +236,9 @@ Python using `requests`:
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
response = requests.post(
|
||||
"http://localhost:8000/chain/invoke/",
|
||||
"http://localhost:8000/joke/invoke",
|
||||
json={'input': {'topic': 'cats'}}
|
||||
)
|
||||
response.json()
|
||||
@@ -191,7 +247,7 @@ response.json()
|
||||
You can also use `curl`:
|
||||
|
||||
```sh
|
||||
curl --location --request POST 'http://localhost:8000/chain/invoke/' \
|
||||
curl --location --request POST 'http://localhost:8000/joke/invoke' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"input": {
|
||||
@@ -207,9 +263,9 @@ The following code:
|
||||
```python
|
||||
...
|
||||
add_routes(
|
||||
app,
|
||||
runnable,
|
||||
path="/my_runnable",
|
||||
app,
|
||||
runnable,
|
||||
path="/my_runnable",
|
||||
)
|
||||
```
|
||||
|
||||
@@ -218,42 +274,76 @@ adds of these endpoints to the server:
|
||||
- `POST /my_runnable/invoke` - invoke the runnable on a single input
|
||||
- `POST /my_runnable/batch` - invoke the runnable on a batch of inputs
|
||||
- `POST /my_runnable/stream` - invoke on a single input and stream the output
|
||||
- `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/stream_log` - invoke on a single input and stream the output,
|
||||
including output of intermediate steps as it's generated
|
||||
- `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
|
||||
|
||||
These endpoints match
|
||||
the [LangChain Expression Language interface](https://python.langchain.com/docs/expression_language/interface) --
|
||||
please reference this documentation for more details.
|
||||
|
||||
## Playground
|
||||
|
||||
You can find a playground page for your runnable at `/my_runnable/playground`. This exposes a simple UI to [configure](https://python.langchain.com/docs/expression_language/how_to/configure) and invoke your runnable with streaming output and intermediate steps.
|
||||
You can find a playground page for your runnable at `/my_runnable/playground/`. This
|
||||
exposes a simple UI
|
||||
to [configure](https://python.langchain.com/docs/expression_language/how_to/configure)
|
||||
and invoke your runnable with streaming output and intermediate steps.
|
||||
|
||||

|
||||
<p align="center">
|
||||
<img src="https://github.com/langchain-ai/langserve/assets/3205522/5ca56e29-f1bb-40f4-84b5-15916384a276" width="50%"/>
|
||||
</p>
|
||||
|
||||
## Installation
|
||||
### Widgets
|
||||
|
||||
For both client and server:
|
||||
The playground supports [widgets](#playground-widgets) and can be used to test your
|
||||
runnable with different inputs. See the [widgets](#widgets) section below for more
|
||||
details.
|
||||
|
||||
```bash
|
||||
pip install "langserve[all]"
|
||||
```
|
||||
### Sharing
|
||||
|
||||
or `pip install "langserve[client]"` for client code, and `pip install "langserve[server]"` for server code.
|
||||
In addition, for configurable runnables, the playground will allow you to configure the
|
||||
runnable and share a link with the configuration:
|
||||
|
||||
<p align="center">
|
||||
<img src="https://github.com/langchain-ai/langserve/assets/3205522/86ce9c59-f8e4-4d08-9fa3-62030e0f521d" width="50%"/>
|
||||
</p>
|
||||
|
||||
## Legacy Chains
|
||||
|
||||
LangServe works with both Runnables (constructed via [LangChain Expression Language](https://python.langchain.com/docs/expression_language/)) and legacy chains (inheriting from `Chain`).
|
||||
However, some of the input schemas for legacy chains may be incomplete/incorrect, leading to errors.
|
||||
LangServe works with both Runnables (constructed
|
||||
via [LangChain Expression Language](https://python.langchain.com/docs/expression_language/))
|
||||
and legacy chains (inheriting from `Chain`).
|
||||
However, some of the input schemas for legacy chains may be incomplete/incorrect,
|
||||
leading to errors.
|
||||
This can be fixed by updating the `input_schema` property of those chains in LangChain.
|
||||
If you encounter any errors, please open an issue on THIS repo, and we will work to address it.
|
||||
|
||||
## Handling Authentication
|
||||
|
||||
If you need to add authentication to your server,
|
||||
please reference FastAPI's [security documentation](https://fastapi.tiangolo.com/tutorial/security/)
|
||||
and [middleware documentation](https://fastapi.tiangolo.com/tutorial/middleware/).
|
||||
If you encounter any errors, please open an issue on THIS repo, and we will work to
|
||||
address it.
|
||||
|
||||
## Deployment
|
||||
|
||||
### Deploy to AWS
|
||||
|
||||
You can deploy to AWS using the [AWS Copilot CLI](https://aws.github.io/copilot-cli/)
|
||||
|
||||
```bash
|
||||
copilot init --app [application-name] --name [service-name] --type 'Load Balanced Web Service' --dockerfile './Dockerfile' --deploy
|
||||
```
|
||||
|
||||
Click [here](https://aws.amazon.com/containers/copilot/) to learn more.
|
||||
|
||||
### Deploy to Azure
|
||||
|
||||
You can deploy to Azure using Azure Container Apps (Serverless):
|
||||
|
||||
```
|
||||
az containerapp up --name [container-app-name] --source . --resource-group [resource-group-name] --environment [environment-name] --ingress external --target-port 8001 --env-vars=OPENAI_API_KEY=your_key
|
||||
```
|
||||
|
||||
You can find more
|
||||
info [here](https://learn.microsoft.com/en-us/azure/container-apps/containerapp-up)
|
||||
|
||||
### Deploy to GCP
|
||||
|
||||
You can deploy to GCP Cloud Run using the following command:
|
||||
@@ -262,24 +352,98 @@ 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
|
||||
```
|
||||
|
||||
### Community Contributed
|
||||
|
||||
#### Deploy to Railway
|
||||
|
||||
[Example Railway Repo](https://github.com/PaulLockett/LangServe-Railway/tree/main)
|
||||
|
||||
[](https://railway.app/template/pW9tXP?referralCode=c-aq4K)
|
||||
|
||||
## Pydantic
|
||||
|
||||
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].
|
||||
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)
|
||||
|
||||
Except for these limitations, we expect the API endpoints, the playground and any other
|
||||
features to work as expected.
|
||||
|
||||
## Advanced
|
||||
|
||||
### Handling Authentication
|
||||
|
||||
If you need to add authentication to your server, please read Fast API's documentation
|
||||
about [dependencies](https://fastapi.tiangolo.com/tutorial/dependencies/)
|
||||
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.
|
||||
|
||||
If you're not sure what you're doing, you could try using an existing solution [Auth0](https://auth0.com/).
|
||||
|
||||
#### Using add_routes
|
||||
|
||||
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) |
|
||||
|
||||
Alternatively, you can use FastAPI's [middleware](https://fastapi.tiangolo.com/tutorial/middleware/).
|
||||
|
||||
Using global dependencies and path dependencies has the advantage that auth will be properly supported in the OpenAPI docs page, but
|
||||
these are not sufficient for implement per user logic (e.g., making an application that can search only within user owned documents).
|
||||
|
||||
If you need to implement per user logic, you can use the `per_req_config_modifier` or `APIHandler` (below) to implement this logic.
|
||||
|
||||
**Per User**
|
||||
|
||||
If you need authorization or logic that is user dependent,
|
||||
specify `per_req_config_modifier` when using `add_routes`. Use a callable receives the
|
||||
raw `Request` object and can extract relevant information from it for authentication and
|
||||
authorization purposes.
|
||||
|
||||
#### Using APIHandler
|
||||
|
||||
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) |
|
||||
|
||||
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.
|
||||
|
||||
### Files
|
||||
|
||||
LLM applications often deal with files. There are different architectures
|
||||
that can be made to implement file processing; at a high level:
|
||||
|
||||
1. The file may be uploaded to the server via a dedicated endpoint and processed using a separate endpoint
|
||||
2. The file may be uploaded by either value (bytes of file) or reference (e.g., s3 url to file content)
|
||||
1. The file may be uploaded to the server via a dedicated endpoint and processed using a
|
||||
separate endpoint
|
||||
2. The file may be uploaded by either value (bytes of file) or reference (e.g., s3 url
|
||||
to file content)
|
||||
3. The processing endpoint may be blocking or non-blocking
|
||||
4. If significant processing is required, the processing may be offloaded to a dedicated process pool
|
||||
4. If significant processing is required, the processing may be offloaded to a dedicated
|
||||
process pool
|
||||
|
||||
You should determine what is the appropriate architecture for your application.
|
||||
|
||||
Currently, to upload files by value to a runnable, use base64 encoding for the
|
||||
file (`multipart/form-data` is not supported yet).
|
||||
Currently, to upload files by value to a runnable, use base64 encoding for the
|
||||
file (`multipart/form-data` is not supported yet).
|
||||
|
||||
Here's an [example](https://github.com/langchain-ai/langserve/tree/main/examples/file_processing) that shows
|
||||
Here's
|
||||
an [example](https://github.com/langchain-ai/langserve/tree/main/examples/file_processing)
|
||||
that shows
|
||||
how to use base64 encoding to send a file to a remote runnable.
|
||||
|
||||
Remember, you can always upload files by reference (e.g., s3 url) or upload them as
|
||||
@@ -320,7 +484,7 @@ add_routes(app, runnable)
|
||||
|
||||
### Custom User Types
|
||||
|
||||
Inherit from `CustomUserType` if you want the data to de-serialize into a
|
||||
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
|
||||
@@ -347,12 +511,13 @@ def func(foo: Foo) -> int:
|
||||
assert isinstance(foo, Foo)
|
||||
return foo.bar
|
||||
|
||||
|
||||
# Note that the input and output type are automatically inferred!
|
||||
# You do not need to specify them.
|
||||
# runnable = RunnableLambda(func).with_types( # <-- Not needed in this case
|
||||
# input_schema=Foo,
|
||||
# output_schema=int,
|
||||
#
|
||||
#
|
||||
add_routes(app, RunnableLambda(func), path="/foo")
|
||||
```
|
||||
|
||||
@@ -360,11 +525,20 @@ add_routes(app, RunnableLambda(func), path="/foo")
|
||||
|
||||
The playground allows you to define custom widgets for your runnable from the backend.
|
||||
|
||||
- A widget is specified at the field level and shipped as part of the JSON schema of the input type
|
||||
- A widget must contain a key called `type` with the value being one of a well known list of widgets
|
||||
- Other widget keys will be associated with values that describe paths in a JSON object
|
||||
Here are a few examples:
|
||||
|
||||
General schema:
|
||||
| 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/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
|
||||
|
||||
- A widget is specified at the field level and shipped as part of the JSON schema of the
|
||||
input type
|
||||
- A widget must contain a key called `type` with the value being one of a well known
|
||||
list of widgets
|
||||
- Other widget keys will be associated with values that describe paths in a JSON object
|
||||
|
||||
```typescript
|
||||
type JsonPath = number | string | (number | string)[];
|
||||
@@ -377,16 +551,32 @@ type Widget = {
|
||||
};
|
||||
```
|
||||
|
||||
### Available Widgets
|
||||
|
||||
There are only two widgets that the user can specify manually right now:
|
||||
|
||||
1. File Upload Widget
|
||||
2. Chat History Widget
|
||||
|
||||
See below more information about these widgets.
|
||||
|
||||
All other widgets on the playground UI are created and managed automatically by the UI
|
||||
based on the config schema of the Runnable. When you create Configurable Runnables,
|
||||
the playground should create appropriate widgets for you to control the behavior.
|
||||
|
||||
#### File Upload Widget
|
||||
|
||||
Allows creation of a file upload input in the UI playground for files
|
||||
that are uploaded as base64 encoded strings. Here's the full [example](https://github.com/langchain-ai/langserve/tree/main/examples/file_processing).
|
||||
that are uploaded as base64 encoded strings. Here's the
|
||||
full [example](https://github.com/langchain-ai/langserve/tree/main/examples/file_processing).
|
||||
|
||||
Snippet:
|
||||
|
||||
```python
|
||||
from pydantic import Field
|
||||
try:
|
||||
from pydantic.v1 import Field
|
||||
except ImportError:
|
||||
from pydantic import Field
|
||||
|
||||
from langserve import CustomUserType
|
||||
|
||||
@@ -401,3 +591,82 @@ class FileProcessingRequest(CustomUserType):
|
||||
num_chars: int = 100
|
||||
|
||||
```
|
||||
|
||||
Example widget:
|
||||
|
||||
<p align="center">
|
||||
<img src="https://github.com/langchain-ai/langserve/assets/3205522/52199e46-9464-4c2e-8be8-222250e08c3f" width="50%"/>
|
||||
</p>
|
||||
|
||||
### Chat Widget
|
||||
|
||||
Look
|
||||
at [widget example](https://github.com/langchain-ai/langserve/tree/main/examples/widgets/server.py).
|
||||
|
||||
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 (
|
||||
e.g., if the output is a list of chat messages.)
|
||||
|
||||
Here's a snippet:
|
||||
|
||||
```python
|
||||
|
||||
class ChatHistory(CustomUserType):
|
||||
chat_history: List[Tuple[str, str]] = Field(
|
||||
...,
|
||||
examples=[[("human input", "ai response")]],
|
||||
extra={"widget": {"type": "chat", "input": "question", "output": "answer"}},
|
||||
)
|
||||
question: str
|
||||
|
||||
|
||||
def _format_to_messages(input: ChatHistory) -> List[BaseMessage]:
|
||||
"""Format the input to a list of messages."""
|
||||
history = input.chat_history
|
||||
user_input = input.question
|
||||
|
||||
messages = []
|
||||
|
||||
for human, ai in history:
|
||||
messages.append(HumanMessage(content=human))
|
||||
messages.append(AIMessage(content=ai))
|
||||
messages.append(HumanMessage(content=user_input))
|
||||
return messages
|
||||
|
||||
|
||||
model = ChatOpenAI()
|
||||
chat_model = RunnableParallel({"answer": (RunnableLambda(_format_to_messages) | model)})
|
||||
add_routes(
|
||||
app,
|
||||
chat_model.with_types(input_type=ChatHistory),
|
||||
config_keys=["configurable"],
|
||||
path="/chat",
|
||||
)
|
||||
```
|
||||
|
||||
Example widget:
|
||||
<p align="center">
|
||||
<img src="https://github.com/langchain-ai/langserve/assets/3205522/a71ff37b-a6a9-4857-a376-cf27c41d3ca4" width="50%"/>
|
||||
</p>
|
||||
|
||||
### Enabling / Disabling Endpoints (LangServe >=0.0.33)
|
||||
|
||||
You can enable / disable which endpoints are exposed. Use `enabled_endpoints` if you
|
||||
want to make sure to never get a new endpoint when upgrading langserve to a newer
|
||||
verison.
|
||||
|
||||
Enable: The code below will only enable `invoke`, `batch` and the
|
||||
corresponding `config_hash` endpoint variants.
|
||||
|
||||
```python
|
||||
add_routes(app, chain, enabled_endpoints=["invoke", "batch", "config_hashes"])
|
||||
```
|
||||
|
||||
Disable: The code below will disable the playground for the chain
|
||||
|
||||
```python
|
||||
add_routes(app, chain, disabled_endpoints=["playground"])
|
||||
```
|
||||
|
||||
@@ -7,9 +7,10 @@ from langchain.agents.output_parsers import OpenAIFunctionsAgentOutputParser
|
||||
from langchain.chat_models import ChatOpenAI
|
||||
from langchain.embeddings import OpenAIEmbeddings
|
||||
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||
from langchain.pydantic_v1 import BaseModel
|
||||
from langchain.tools.render import format_tool_to_openai_function
|
||||
from langchain.vectorstores import FAISS
|
||||
from pydantic import BaseModel
|
||||
from langchain_core.runnables import ConfigurableField
|
||||
|
||||
from langserve import add_routes
|
||||
|
||||
@@ -35,7 +36,19 @@ prompt = ChatPromptTemplate.from_messages(
|
||||
]
|
||||
)
|
||||
|
||||
llm = ChatOpenAI()
|
||||
|
||||
llm = ChatOpenAI(temperature=0.5).configurable_alternatives(
|
||||
ConfigurableField(
|
||||
id="llm",
|
||||
name="LLM",
|
||||
description=(
|
||||
"Decide whether to use a high or a low temperature parameter for the LLM."
|
||||
),
|
||||
),
|
||||
high_temp=ChatOpenAI(temperature=0.9),
|
||||
low_temp=ChatOpenAI(temperature=0.1),
|
||||
default_key="medium_temp",
|
||||
)
|
||||
|
||||
llm_with_tools = llm.bind(functions=[format_tool_to_openai_function(t) for t in tools])
|
||||
|
||||
@@ -75,6 +88,7 @@ class Output(BaseModel):
|
||||
# /batch
|
||||
# /stream
|
||||
add_routes(app, agent_executor.with_types(input_type=Input, output_type=Output))
|
||||
add_routes(app, llm, path='/llms')
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
Executable
+125
@@ -0,0 +1,125 @@
|
||||
"""An example that shows how to use the API handler directly.
|
||||
|
||||
For this to work with RemoteClient, the routes must match those expected
|
||||
by the client; i.e., /invoke, /batch, /stream, etc. No trailing slashes should be used.
|
||||
"""
|
||||
from importlib import metadata
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import Depends, FastAPI, Request, Response
|
||||
from langchain_core.runnables import RunnableLambda
|
||||
from sse_starlette import EventSourceResponse
|
||||
|
||||
from langserve import APIHandler
|
||||
|
||||
PYDANTIC_VERSION = metadata.version("pydantic")
|
||||
_PYDANTIC_MAJOR_VERSION: int = int(PYDANTIC_VERSION.split(".")[0])
|
||||
|
||||
app = FastAPI(
|
||||
title="LangChain Server",
|
||||
version="1.0",
|
||||
description="Spin up a simple api server using Langchain's Runnable interfaces",
|
||||
)
|
||||
|
||||
|
||||
##
|
||||
# Example 1 -- invoke, batch together with doc-generation
|
||||
# This endpoint shows how to expose `invoke` and `batch` using the APIHandler.
|
||||
# It also shows how to generate documentation properly so it works correctly
|
||||
# depending on Fast API and pydantic versions.
|
||||
def add_one(x: int) -> int:
|
||||
"""Add one to the given number."""
|
||||
return x + 1
|
||||
|
||||
|
||||
chain = RunnableLambda(add_one)
|
||||
|
||||
api_handler = APIHandler(chain, path="/simple")
|
||||
|
||||
|
||||
# First register the endpoints without documentation
|
||||
@app.post("/simple/invoke", include_in_schema=False)
|
||||
async def simple_invoke(request: Request) -> Response:
|
||||
"""Handle a request."""
|
||||
# The API Handler validates the parts of the request
|
||||
# that are used by the runnnable (e.g., input, config fields)
|
||||
return await api_handler.invoke(request)
|
||||
|
||||
|
||||
@app.post("/simple/batch", include_in_schema=False)
|
||||
async def simple_batch(request: Request) -> Response:
|
||||
"""Handle a request."""
|
||||
# The API Handler validates the parts of the request
|
||||
# that are used by the runnnable (e.g., input, config fields)
|
||||
return await api_handler.batch(request)
|
||||
|
||||
|
||||
# Here, we show how to populate the documentation for the endpoint.
|
||||
# Please note that this is done separately from the actual endpoint.
|
||||
# This happens due to two reasons:
|
||||
# 1. FastAPI does not support using pydantic.v1 models in the docs endpoint.
|
||||
# "https://github.com/tiangolo/fastapi/issues/10360"
|
||||
# LangChain uses pydantic.v1 models!
|
||||
# 2. Configurable Runnables have a *dynamic* schema, which means that
|
||||
# the shape of the input depends on the config.
|
||||
# In this case, the openapi schema is a best effort showing the documentation
|
||||
# that will work for the default config (and any non-conflicting configs).
|
||||
if _PYDANTIC_MAJOR_VERSION == 1: # Do not use in your own
|
||||
# Add documentation
|
||||
@app.post("/simple/invoke")
|
||||
async def simple_invoke_docs(
|
||||
request: api_handler.InvokeRequest,
|
||||
) -> api_handler.InvokeResponse:
|
||||
"""API endpoint used only for documentation purposes. Populate /docs endpoint"""
|
||||
raise NotImplementedError(
|
||||
"This endpoint is only used for documentation purposes"
|
||||
)
|
||||
|
||||
@app.post("/simple/batch")
|
||||
async def simple_batch_docs(
|
||||
request: api_handler.BatchRequest,
|
||||
) -> api_handler.BatchResponse:
|
||||
"""API endpoint used only for documentation purposes. Populate /docs endpoint"""
|
||||
raise NotImplementedError(
|
||||
"This endpoint is only used for documentation purposes"
|
||||
)
|
||||
|
||||
else:
|
||||
print(
|
||||
"Skipping documentation generation for pydantic v2: "
|
||||
"https://github.com/tiangolo/fastapi/issues/10360"
|
||||
)
|
||||
|
||||
|
||||
##
|
||||
# Example 2 -- Expose `invoke` and `stream` using the API Handler.
|
||||
# Uses FastAPI Depends get a ready API handler.
|
||||
async def _get_api_handler() -> APIHandler:
|
||||
"""Prepare a RunnableLambda."""
|
||||
return APIHandler(RunnableLambda(add_one), path="/v2")
|
||||
|
||||
|
||||
@app.post("/v2/invoke")
|
||||
async def v2_invoke(
|
||||
request: Request, runnable: Annotated[APIHandler, Depends(_get_api_handler)]
|
||||
) -> Response:
|
||||
"""Handle invoke request."""
|
||||
# The API Handler validates the parts of the request
|
||||
# that are used by the runnnable (e.g., input, config fields)
|
||||
return await runnable.invoke(request)
|
||||
|
||||
|
||||
@app.post("/v2/stream")
|
||||
async def v2_stream(
|
||||
request: Request, runnable: Annotated[APIHandler, Depends(_get_api_handler)]
|
||||
) -> EventSourceResponse:
|
||||
"""Handle stream request."""
|
||||
# The API Handler validates the parts of the request
|
||||
# that are used by the runnnable (e.g., input, config fields)
|
||||
return await runnable.stream(request)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(app, host="localhost", port=8000)
|
||||
@@ -0,0 +1,212 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Client\n",
|
||||
"\n",
|
||||
"This is an example client that interacts with the server that has \"auth\".\n",
|
||||
"\n",
|
||||
"Please reference appropriate documentation in the server code and in FastAPI to actually make this secure.\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"**ATTENTION** Only the invoke endpoint has been defined by the server! \n",
|
||||
"So batch/stream won't work. If you want to add stream and batch, you can do so as well on the server side.\n",
|
||||
"The server is implemented using the APIHandler, it's more flexible, but does require a bit more code. :)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Login as Alice"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import requests\n",
|
||||
"\n",
|
||||
"response = requests.post(\"http://localhost:8000/token\", data={\"username\": \"alice\", \"password\": \"secret1\"})\n",
|
||||
"result = response.json()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"token = result['access_token']"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"inputs = {\"input\": \"hello\"}\n",
|
||||
"response = requests.post(\"http://localhost:8000/my_runnable/invoke\", \n",
|
||||
" json={\n",
|
||||
" 'input': 'hello',\n",
|
||||
" },\n",
|
||||
" headers={\n",
|
||||
" 'Authorization': f\"Bearer {token}\"\n",
|
||||
" }\n",
|
||||
")\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'output': [{'page_content': 'cats like mice',\n",
|
||||
" 'metadata': {'owner_id': 'alice'},\n",
|
||||
" 'type': 'Document'},\n",
|
||||
" {'page_content': 'cats like cheese',\n",
|
||||
" 'metadata': {'owner_id': 'alice'},\n",
|
||||
" 'type': 'Document'}],\n",
|
||||
" 'callback_events': [],\n",
|
||||
" 'metadata': {'run_id': '1732c9aa-c6d3-4736-b8ca-01265fa8ba06'}}"
|
||||
]
|
||||
},
|
||||
"execution_count": 4,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"response.json()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"source": [
|
||||
"You can also interact with this via the RemoteRunnable interface (to use in other chains)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langserve import RemoteRunnable\n",
|
||||
"\n",
|
||||
"remote_runnable = RemoteRunnable(\"http://localhost:8000/my_runnable\", headers={\"Authorization\": f\"Bearer {token}\"})"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"[Document(page_content='cats like mice', metadata={'owner_id': 'alice'}),\n",
|
||||
" Document(page_content='cats like cheese', metadata={'owner_id': 'alice'})]"
|
||||
]
|
||||
},
|
||||
"execution_count": 6,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"await remote_runnable.ainvoke(\"cat\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Login as John"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import requests\n",
|
||||
"\n",
|
||||
"response = requests.post(\"http://localhost:8000/token\", data={\"username\": \"john\", \"password\": \"secret2\"})\n",
|
||||
"token = response.json()['access_token']\n",
|
||||
"remote_runnable = RemoteRunnable(\"http://localhost:8000/my_runnable\", headers={\"Authorization\": f\"Bearer {token}\"})"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"[Document(page_content='i like walks by the ocean', metadata={'owner_id': 'john'}),\n",
|
||||
" Document(page_content='dogs like sticks', metadata={'owner_id': 'john'}),\n",
|
||||
" Document(page_content='my favorite food is cheese', metadata={'owner_id': 'john'})]"
|
||||
]
|
||||
},
|
||||
"execution_count": 8,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"await remote_runnable.ainvoke(\"water\")"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.9.6"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 4
|
||||
}
|
||||
Executable
+285
@@ -0,0 +1,285 @@
|
||||
#!/usr/bin/env python
|
||||
"""Example that shows how to use the underlying APIHandler class directly with Auth.
|
||||
|
||||
This example shows how to apply logic based on the user's identity.
|
||||
|
||||
You can build on these concepts to implement a more complex app:
|
||||
* Add endpoints that allow users to manage their documents.
|
||||
* Make a more complex runnable that does something with the retrieved documents; e.g.,
|
||||
a conversational agent that responds to the user's input with the retrieved documents
|
||||
(which are user specific documents).
|
||||
|
||||
For authentication, we use a fake token that's the same as the username, adapting
|
||||
the following example from the FastAPI docs:
|
||||
|
||||
https://fastapi.tiangolo.com/tutorial/security/simple-oauth2/
|
||||
|
||||
**ATTENTION**
|
||||
|
||||
This example is not actually secure and should not be used in production.
|
||||
|
||||
Once you understand how to use `per_req_config_modifier`, read through
|
||||
the FastAPI docs and implement proper auth:
|
||||
https://fastapi.tiangolo.com/tutorial/security/oauth2-jwt/
|
||||
|
||||
|
||||
**ATTENTION**
|
||||
|
||||
This example does not integrate auth with OpenAPI, so the OpenAPI docs won't
|
||||
be able to help with authentication. This is currently a limitation
|
||||
if using `add_routes`. If you need this functionality, you can use
|
||||
the underlying `APIHandler` class directly, which affords maximal flexibility.
|
||||
"""
|
||||
from importlib import metadata
|
||||
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 (
|
||||
ConfigurableField,
|
||||
RunnableConfig,
|
||||
RunnableSerializable,
|
||||
)
|
||||
from langchain_core.vectorstores import VectorStore
|
||||
from typing_extensions import Annotated
|
||||
|
||||
from langserve import APIHandler
|
||||
from langserve.pydantic_v1 import BaseModel
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
username: str
|
||||
email: Union[str, None] = None
|
||||
full_name: Union[str, None] = None
|
||||
disabled: Union[bool, None] = None
|
||||
|
||||
|
||||
class UserInDB(User):
|
||||
hashed_password: str
|
||||
|
||||
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
FAKE_USERS_DB = {
|
||||
"alice": {
|
||||
"username": "alice",
|
||||
"full_name": "Alice Wonderson",
|
||||
"email": "alice@example.com",
|
||||
"hashed_password": "fakehashedsecret1",
|
||||
"disabled": False,
|
||||
},
|
||||
"john": {
|
||||
"username": "john",
|
||||
"full_name": "John Doe",
|
||||
"email": "johndoe@example.com",
|
||||
"hashed_password": "fakehashedsecret2",
|
||||
"disabled": False,
|
||||
},
|
||||
"bob": {
|
||||
"username": "john",
|
||||
"full_name": "John Doe",
|
||||
"email": "johndoe@example.com",
|
||||
"hashed_password": "fakehashedsecret3",
|
||||
"disabled": True,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _fake_hash_password(password: str) -> str:
|
||||
"""Fake a hashed password."""
|
||||
return "fakehashed" + password
|
||||
|
||||
|
||||
def _get_user(db: dict, username: str) -> Union[UserInDB, None]:
|
||||
if username in db:
|
||||
user_dict = db[username]
|
||||
return UserInDB(**user_dict)
|
||||
|
||||
|
||||
def _fake_decode_token(token: str) -> Union[User, None]:
|
||||
# This doesn't provide any security at all
|
||||
# Check the next version
|
||||
user = _get_user(FAKE_USERS_DB, token)
|
||||
return user
|
||||
|
||||
|
||||
@app.post("/token")
|
||||
async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]):
|
||||
user_dict = FAKE_USERS_DB.get(form_data.username)
|
||||
if not user_dict:
|
||||
raise HTTPException(status_code=400, detail="Incorrect username or password")
|
||||
user = UserInDB(**user_dict)
|
||||
hashed_password = _fake_hash_password(form_data.password)
|
||||
if not hashed_password == user.hashed_password:
|
||||
raise HTTPException(status_code=400, detail="Incorrect username or password")
|
||||
|
||||
return {"access_token": user.username, "token_type": "bearer"}
|
||||
|
||||
|
||||
async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
|
||||
user = _fake_decode_token(token)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid authentication credentials",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
return user
|
||||
|
||||
|
||||
async def get_current_active_user(
|
||||
current_user: Annotated[User, Depends(get_current_user)],
|
||||
):
|
||||
if current_user.disabled:
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
return current_user
|
||||
|
||||
|
||||
class PerUserVectorstore(RunnableSerializable):
|
||||
"""A custom runnable that returns a list of documents for the given user.
|
||||
|
||||
The runnable is configurable by the user, and the search results are
|
||||
filtered by the user ID.
|
||||
"""
|
||||
|
||||
user_id: Optional[str]
|
||||
vectorstore: VectorStore
|
||||
|
||||
class Config:
|
||||
# Allow arbitrary types since VectorStore is an abstract interface
|
||||
# and not a pydantic model
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
def _invoke(
|
||||
self, input: str, config: Optional[RunnableConfig] = None, **kwargs: Any
|
||||
) -> List[Document]:
|
||||
"""Invoke the retriever."""
|
||||
# WARNING: Verify documentation of underlying vectorstore to make
|
||||
# sure that it actually uses filters.
|
||||
# Highly recommended to use unit-tests to verify this behavior, as
|
||||
# implementations can be different depending on the underlying vectorstore.
|
||||
retriever = self.vectorstore.as_retriever(
|
||||
search_kwargs={"filter": {"owner_id": self.user_id}}
|
||||
)
|
||||
return retriever.invoke(input, config=config)
|
||||
|
||||
def invoke(
|
||||
self, input: str, config: Optional[RunnableConfig] = None, **kwargs
|
||||
) -> List[Document]:
|
||||
"""Add one to an integer."""
|
||||
return self._call_with_config(self._invoke, input, config, **kwargs)
|
||||
|
||||
|
||||
vectorstore = Chroma(
|
||||
collection_name="some_collection",
|
||||
embedding_function=OpenAIEmbeddings(),
|
||||
)
|
||||
|
||||
vectorstore.add_documents(
|
||||
[
|
||||
Document(
|
||||
page_content="cats like cheese",
|
||||
metadata={"owner_id": "alice"},
|
||||
),
|
||||
Document(
|
||||
page_content="cats like mice",
|
||||
metadata={"owner_id": "alice"},
|
||||
),
|
||||
Document(
|
||||
page_content="dogs like sticks",
|
||||
metadata={"owner_id": "john"},
|
||||
),
|
||||
Document(
|
||||
page_content="my favorite food is cheese",
|
||||
metadata={"owner_id": "john"},
|
||||
),
|
||||
Document(
|
||||
page_content="i like walks by the ocean",
|
||||
metadata={"owner_id": "john"},
|
||||
),
|
||||
Document(
|
||||
page_content="dogs like grass",
|
||||
metadata={"owner_id": "bob"},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
per_user_retriever = PerUserVectorstore(
|
||||
user_id=None, # Placeholder ID that will be replaced by the per_req_config_modifier
|
||||
vectorstore=vectorstore,
|
||||
).configurable_fields(
|
||||
# Attention: Make sure to override the user ID for each request in the
|
||||
# per_req_config_modifier. This should not be client configurable.
|
||||
user_id=ConfigurableField(
|
||||
id="user_id",
|
||||
name="User ID",
|
||||
description="The user ID to use for the retriever.",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# Let's define the API Handler
|
||||
api_handler = APIHandler(
|
||||
per_user_retriever,
|
||||
# Namespace for the runnable.
|
||||
# Endpoints like batch / invoke should be under /my_runnable/invoke
|
||||
# and /my_runnable/batch etc.
|
||||
path="/my_runnable",
|
||||
)
|
||||
|
||||
|
||||
PYDANTIC_VERSION = metadata.version("pydantic")
|
||||
_PYDANTIC_MAJOR_VERSION: int = int(PYDANTIC_VERSION.split(".")[0])
|
||||
|
||||
|
||||
# **ATTENTION** Your code does not need to include both versions.
|
||||
# Use whichever version is appropriate given the pydantic version you are using.
|
||||
# Both versions are included here for demonstration purposes.
|
||||
#
|
||||
# If using pydantic <2, everything works as expected.
|
||||
# However, when using pydantic >=2 is installed, things are a bit
|
||||
# more complicated because LangChain uses the pydantic.v1 namespace
|
||||
# But the pydantic.v1 namespace is not supported by FastAPI.
|
||||
# See this issue: https://github.com/tiangolo/fastapi/issues/10360
|
||||
# So when using pydantic >=2, we need to use a vanilla starlette request
|
||||
# and response, and we will not have documentation.
|
||||
# Or we can create custom models for the request and response.
|
||||
# The underlying API Handler will still validate the request
|
||||
# correctly even if vanilla requests are used.
|
||||
if _PYDANTIC_MAJOR_VERSION == 1:
|
||||
|
||||
@app.post("/my_runnable/invoke")
|
||||
async def invoke_with_auth(
|
||||
# Included for documentation purposes
|
||||
invoke_request: api_handler.InvokeRequest,
|
||||
request: Request,
|
||||
current_user: Annotated[User, Depends(get_current_active_user)],
|
||||
) -> Response:
|
||||
"""Handle a request."""
|
||||
# The API Handler validates the parts of the request
|
||||
# that are used by the runnnable (e.g., input, config fields)
|
||||
config = {"configurable": {"user_id": current_user.username}}
|
||||
return await api_handler.invoke(request, server_config=config)
|
||||
else:
|
||||
|
||||
@app.post("/my_runnable/invoke")
|
||||
async def invoke_with_auth(
|
||||
request: Request,
|
||||
current_user: Annotated[User, Depends(get_current_active_user)],
|
||||
) -> Response:
|
||||
"""Handle a request."""
|
||||
# The API Handler validates the parts of the request
|
||||
# that are used by the runnnable (e.g., input, config fields)
|
||||
config = {"configurable": {"user_id": current_user.username}}
|
||||
return await api_handler.invoke(request, server_config=config)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(app, host="localhost", port=8000)
|
||||
Executable
+53
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python
|
||||
"""An example that uses Fast API global dependencies.
|
||||
|
||||
This approach can be used if the same authentication logic can be used
|
||||
for all endpoints in the application.
|
||||
|
||||
This may be a reasonable approach for simple applications.
|
||||
|
||||
See:
|
||||
|
||||
* https://fastapi.tiangolo.com/tutorial/dependencies/global-dependencies/
|
||||
* https://fastapi.tiangolo.com/tutorial/dependencies/
|
||||
* https://fastapi.tiangolo.com/tutorial/security/
|
||||
"""
|
||||
|
||||
from fastapi import Depends, FastAPI, Header, HTTPException
|
||||
from langchain_core.runnables import RunnableLambda
|
||||
from typing_extensions import Annotated
|
||||
|
||||
from langserve import add_routes
|
||||
|
||||
|
||||
async def verify_token(x_token: Annotated[str, Header()]) -> None:
|
||||
"""Verify the token is valid."""
|
||||
# Replace this with your actual authentication logic
|
||||
if x_token != "secret-token":
|
||||
raise HTTPException(status_code=400, detail="X-Token header invalid")
|
||||
|
||||
|
||||
app = FastAPI(
|
||||
title="LangChain Server",
|
||||
version="1.0",
|
||||
dependencies=[Depends(verify_token)],
|
||||
)
|
||||
|
||||
|
||||
def add_one(x: int) -> int:
|
||||
"""Add one to an integer."""
|
||||
return x + 1
|
||||
|
||||
|
||||
chain = RunnableLambda(add_one)
|
||||
|
||||
|
||||
add_routes(
|
||||
app,
|
||||
chain,
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(app, host="localhost", port=8000)
|
||||
Executable
+50
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env python
|
||||
"""An example that shows how to use path dependencies for authentication.
|
||||
|
||||
The path dependencies are applied to all the routes added by the `add_routes`.
|
||||
|
||||
To keep this example brief, we're providing a placeholder verify_token function
|
||||
that shows how to use path dependencies.
|
||||
|
||||
To implement proper auth, please see the FastAPI docs:
|
||||
|
||||
* https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-path-operation-decorators/
|
||||
* https://fastapi.tiangolo.com/tutorial/dependencies/
|
||||
* https://fastapi.tiangolo.com/tutorial/security/
|
||||
""" # noqa: E501
|
||||
|
||||
from fastapi import Depends, FastAPI, Header, HTTPException
|
||||
from langchain_core.runnables import RunnableLambda
|
||||
from typing_extensions import Annotated
|
||||
|
||||
from langserve import add_routes
|
||||
|
||||
|
||||
async def verify_token(x_token: Annotated[str, Header()]) -> None:
|
||||
"""Verify the token is valid."""
|
||||
# Replace this with your actual authentication logic
|
||||
if x_token != "secret-token":
|
||||
raise HTTPException(status_code=400, detail="X-Token header invalid")
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
def add_one(x: int) -> int:
|
||||
"""Add one to an integer."""
|
||||
return x + 1
|
||||
|
||||
|
||||
chain = RunnableLambda(add_one)
|
||||
|
||||
|
||||
add_routes(
|
||||
app,
|
||||
chain,
|
||||
dependencies=[Depends(verify_token)],
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(app, host="localhost", port=8000)
|
||||
@@ -0,0 +1,207 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Client\n",
|
||||
"\n",
|
||||
"This is an example client that interacts with the server that has \"auth\".\n",
|
||||
"\n",
|
||||
"Please reference appropriate documentation in the server code and in FastAPI to actually make this secure."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Login as Alice"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import requests\n",
|
||||
"\n",
|
||||
"response = requests.post(\"http://localhost:8000/token\", data={\"username\": \"alice\", \"password\": \"secret1\"})\n",
|
||||
"result = response.json()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"token = result['access_token']"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"inputs = {\"input\": \"hello\"}\n",
|
||||
"response = requests.post(\"http://localhost:8000/invoke\", \n",
|
||||
" json={\n",
|
||||
" 'input': 'hello',\n",
|
||||
" },\n",
|
||||
" headers={\n",
|
||||
" 'Authorization': f\"Bearer {token}\"\n",
|
||||
" }\n",
|
||||
")\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 9,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'output': [{'page_content': 'cats like mice',\n",
|
||||
" 'metadata': {'owner_id': 'alice'},\n",
|
||||
" 'type': 'Document'},\n",
|
||||
" {'page_content': 'cats like cheese',\n",
|
||||
" 'metadata': {'owner_id': 'alice'},\n",
|
||||
" 'type': 'Document'}],\n",
|
||||
" 'callback_events': [],\n",
|
||||
" 'metadata': {'run_id': '00000000-0000-0000-0000-000000000000'}}"
|
||||
]
|
||||
},
|
||||
"execution_count": 9,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"response.json()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"source": [
|
||||
"You can also interact with this via the RemoteRunnable interface (to use in other chains)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 10,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langserve import RemoteRunnable\n",
|
||||
"\n",
|
||||
"remote_runnable = RemoteRunnable(\"http://localhost:8000/\", headers={\"Authorization\": f\"Bearer {token}\"})"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 11,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"[Document(page_content='cats like mice', metadata={'owner_id': 'alice'}),\n",
|
||||
" Document(page_content='cats like cheese', metadata={'owner_id': 'alice'})]"
|
||||
]
|
||||
},
|
||||
"execution_count": 11,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"await remote_runnable.ainvoke(\"cat\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Login as John"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 23,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import requests\n",
|
||||
"\n",
|
||||
"response = requests.post(\"http://localhost:8000/token\", data={\"username\": \"john\", \"password\": \"secret2\"})\n",
|
||||
"token = response.json()['access_token']\n",
|
||||
"remote_runnable = RemoteRunnable(\"http://localhost:8000/\", headers={\"Authorization\": f\"Bearer {token}\"})"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 25,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"[Document(page_content='i like walks by the ocean', metadata={'owner_id': 'john'}),\n",
|
||||
" Document(page_content='dogs like sticks', metadata={'owner_id': 'john'}),\n",
|
||||
" Document(page_content='my favorite food is cheese', metadata={'owner_id': 'john'})]"
|
||||
]
|
||||
},
|
||||
"execution_count": 25,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"await remote_runnable.ainvoke(\"water\")"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.9.6"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 4
|
||||
}
|
||||
+243
@@ -0,0 +1,243 @@
|
||||
#!/usr/bin/env python
|
||||
"""Example that shows how to use `per_req_config_modifier`.
|
||||
|
||||
This is a simple example that shows how to use configurable runnables with
|
||||
per request configuration modification to achieve behavior that's different
|
||||
depending on the user.
|
||||
|
||||
You can build on these concepts to implement a more complex app:
|
||||
* Add endpoints that allow users to manage their documents.
|
||||
* Make a more complex runnable that does something with the retrieved documents; e.g.,
|
||||
a conversational agent that responds to the user's input with the retrieved documents
|
||||
(which are user specific documents).
|
||||
|
||||
For authentication, we use a fake token that's the same as the username, adapting
|
||||
the following example from the FastAPI docs:
|
||||
|
||||
https://fastapi.tiangolo.com/tutorial/security/simple-oauth2/
|
||||
|
||||
**ATTENTION**
|
||||
|
||||
This example is not actually secure and should not be used in production.
|
||||
|
||||
Once you understand how to use `per_req_config_modifier`, read through
|
||||
the FastAPI docs and implement proper auth:
|
||||
https://fastapi.tiangolo.com/tutorial/security/oauth2-jwt/
|
||||
|
||||
|
||||
**ATTENTION**
|
||||
|
||||
This example does not integrate auth with OpenAPI, so the OpenAPI docs won't
|
||||
be able to help with authentication. This is currently a limitation
|
||||
if using `add_routes`. If you need this functionality, you can use
|
||||
the underlying `APIHandler` class directly, which affords maximal flexibility.
|
||||
"""
|
||||
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 (
|
||||
ConfigurableField,
|
||||
RunnableConfig,
|
||||
RunnableSerializable,
|
||||
)
|
||||
from langchain_core.vectorstores import VectorStore
|
||||
from typing_extensions import Annotated
|
||||
|
||||
from langserve import add_routes
|
||||
from langserve.pydantic_v1 import BaseModel
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
username: str
|
||||
email: Union[str, None] = None
|
||||
full_name: Union[str, None] = None
|
||||
disabled: Union[bool, None] = None
|
||||
|
||||
|
||||
class UserInDB(User):
|
||||
hashed_password: str
|
||||
|
||||
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
FAKE_USERS_DB = {
|
||||
"alice": {
|
||||
"username": "alice",
|
||||
"full_name": "Alice Wonderson",
|
||||
"email": "alice@example.com",
|
||||
"hashed_password": "fakehashedsecret1",
|
||||
"disabled": False,
|
||||
},
|
||||
"john": {
|
||||
"username": "john",
|
||||
"full_name": "John Doe",
|
||||
"email": "johndoe@example.com",
|
||||
"hashed_password": "fakehashedsecret2",
|
||||
"disabled": False,
|
||||
},
|
||||
"bob": {
|
||||
"username": "john",
|
||||
"full_name": "John Doe",
|
||||
"email": "johndoe@example.com",
|
||||
"hashed_password": "fakehashedsecret3",
|
||||
"disabled": True,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _fake_hash_password(password: str) -> str:
|
||||
"""Fake a hashed password."""
|
||||
return "fakehashed" + password
|
||||
|
||||
|
||||
def _get_user(db: dict, username: str) -> Union[UserInDB, None]:
|
||||
if username in db:
|
||||
user_dict = db[username]
|
||||
return UserInDB(**user_dict)
|
||||
|
||||
|
||||
def _fake_decode_token(token: str) -> Union[User, None]:
|
||||
# This doesn't provide any security at all
|
||||
# Check the next version
|
||||
user = _get_user(FAKE_USERS_DB, token)
|
||||
return user
|
||||
|
||||
|
||||
@app.post("/token")
|
||||
async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]):
|
||||
user_dict = FAKE_USERS_DB.get(form_data.username)
|
||||
if not user_dict:
|
||||
raise HTTPException(status_code=400, detail="Incorrect username or password")
|
||||
user = UserInDB(**user_dict)
|
||||
hashed_password = _fake_hash_password(form_data.password)
|
||||
if not hashed_password == user.hashed_password:
|
||||
raise HTTPException(status_code=400, detail="Incorrect username or password")
|
||||
|
||||
return {"access_token": user.username, "token_type": "bearer"}
|
||||
|
||||
|
||||
async def get_current_active_user_from_request(request: Request) -> User:
|
||||
"""Get the current active user from the request."""
|
||||
token = await oauth2_scheme(request)
|
||||
user = _fake_decode_token(token)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid authentication credentials",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
if user.disabled:
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
return user
|
||||
|
||||
|
||||
class PerUserVectorstore(RunnableSerializable):
|
||||
"""A custom runnable that returns a list of documents for the given user.
|
||||
|
||||
The runnable is configurable by the user, and the search results are
|
||||
filtered by the user ID.
|
||||
"""
|
||||
|
||||
user_id: Optional[str]
|
||||
vectorstore: VectorStore
|
||||
|
||||
class Config:
|
||||
# Allow arbitrary types since VectorStore is an abstract interface
|
||||
# and not a pydantic model
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
def _invoke(
|
||||
self, input: str, config: Optional[RunnableConfig] = None, **kwargs: Any
|
||||
) -> List[Document]:
|
||||
"""Invoke the retriever."""
|
||||
# WARNING: Verify documentation of underlying vectorstore to make
|
||||
# sure that it actually uses filters.
|
||||
# Highly recommended to use unit-tests to verify this behavior, as
|
||||
# implementations can be different depending on the underlying vectorstore.
|
||||
retriever = self.vectorstore.as_retriever(
|
||||
search_kwargs={"filter": {"owner_id": self.user_id}}
|
||||
)
|
||||
return retriever.invoke(input, config=config)
|
||||
|
||||
def invoke(
|
||||
self, input: str, config: Optional[RunnableConfig] = None, **kwargs
|
||||
) -> List[Document]:
|
||||
"""Add one to an integer."""
|
||||
return self._call_with_config(self._invoke, input, config, **kwargs)
|
||||
|
||||
|
||||
async def per_req_config_modifier(config: Dict, request: Request) -> Dict:
|
||||
"""Modify the config for each request."""
|
||||
user = await get_current_active_user_from_request(request)
|
||||
config["configurable"] = {}
|
||||
# Attention: Make sure that the user ID is over-ridden for each request.
|
||||
# We should not be accepting a user ID from the user in this case!
|
||||
config["configurable"]["user_id"] = user.username
|
||||
return config
|
||||
|
||||
|
||||
vectorstore = Chroma(
|
||||
collection_name="some_collection",
|
||||
embedding_function=OpenAIEmbeddings(),
|
||||
)
|
||||
|
||||
vectorstore.add_documents(
|
||||
[
|
||||
Document(
|
||||
page_content="cats like cheese",
|
||||
metadata={"owner_id": "alice"},
|
||||
),
|
||||
Document(
|
||||
page_content="cats like mice",
|
||||
metadata={"owner_id": "alice"},
|
||||
),
|
||||
Document(
|
||||
page_content="dogs like sticks",
|
||||
metadata={"owner_id": "john"},
|
||||
),
|
||||
Document(
|
||||
page_content="my favorite food is cheese",
|
||||
metadata={"owner_id": "john"},
|
||||
),
|
||||
Document(
|
||||
page_content="i like walks by the ocean",
|
||||
metadata={"owner_id": "john"},
|
||||
),
|
||||
Document(
|
||||
page_content="dogs like grass",
|
||||
metadata={"owner_id": "bob"},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
per_user_retriever = PerUserVectorstore(
|
||||
user_id=None, # Placeholder ID that will be replaced by the per_req_config_modifier
|
||||
vectorstore=vectorstore,
|
||||
).configurable_fields(
|
||||
# Attention: Make sure to override the user ID for each request in the
|
||||
# per_req_config_modifier. This should not be client configurable.
|
||||
user_id=ConfigurableField(
|
||||
id="user_id",
|
||||
name="User ID",
|
||||
description="The user ID to use for the retriever.",
|
||||
)
|
||||
)
|
||||
|
||||
add_routes(
|
||||
app,
|
||||
per_user_retriever,
|
||||
per_req_config_modifier=per_req_config_modifier,
|
||||
enabled_endpoints=["invoke"],
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(app, host="localhost", port=8000)
|
||||
@@ -0,0 +1,277 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Chat History\n",
|
||||
"\n",
|
||||
"An example of a client interacting with a chatbot where message history is persisted on the backend."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import uuid\n",
|
||||
"from langserve import RemoteRunnable\n",
|
||||
"\n",
|
||||
"chat = RemoteRunnable(\"http://localhost:8000/\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Let's create a prompt composed of a system message and a human message."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"session_id = str(uuid.uuid4())"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"AIMessage(content=\" Hello Eugene! My name is Claude. It's nice to meet another cat lover.\")"
|
||||
]
|
||||
},
|
||||
"execution_count": 3,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"chat.invoke({\"human_input\": \"my name is eugene. i like cats. what is your name?\"}, {'configurable': { 'session_id': session_id } })"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"AIMessage(content=' You told me your name is Eugene.')"
|
||||
]
|
||||
},
|
||||
"execution_count": 4,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"chat.invoke({\"human_input\": \"what was my name?\"}, {'configurable': { 'session_id': session_id } })"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"AIMessage(content=' You said you like cats.')"
|
||||
]
|
||||
},
|
||||
"execution_count": 5,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"chat.invoke({\"human_input\": \"What animal do i like?\"}, {'configurable': { 'session_id': session_id } })"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\n",
|
||||
" Sure\n",
|
||||
",\n",
|
||||
" I\n",
|
||||
"'d\n",
|
||||
" be\n",
|
||||
" happy\n",
|
||||
" to\n",
|
||||
" count\n",
|
||||
" to\n",
|
||||
" 10\n",
|
||||
":\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"1\n",
|
||||
",\n",
|
||||
" 2\n",
|
||||
",\n",
|
||||
" 3\n",
|
||||
",\n",
|
||||
" 4\n",
|
||||
",\n",
|
||||
" 5\n",
|
||||
",\n",
|
||||
" 6\n",
|
||||
",\n",
|
||||
" 7\n",
|
||||
",\n",
|
||||
" 8\n",
|
||||
",\n",
|
||||
" 9\n",
|
||||
",\n",
|
||||
" 10\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"for chunk in chat.stream({'human_input': \"Can you count till 10?\"}, {'configurable': { 'session_id': session_id } }):\n",
|
||||
" print()\n",
|
||||
" print(chunk.content, end='', flush=True)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 12,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\u001b[1;39m[\n",
|
||||
" \u001b[1;39m{\n",
|
||||
" \u001b[0m\u001b[34;1m\"type\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"human\"\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"data\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{\n",
|
||||
" \u001b[0m\u001b[34;1m\"content\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"my name is eugene. i like cats. what is your name?\"\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"additional_kwargs\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{}\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"type\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"human\"\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"example\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39mfalse\u001b[0m\u001b[1;39m\n",
|
||||
" \u001b[1;39m}\u001b[0m\u001b[1;39m\n",
|
||||
" \u001b[1;39m}\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[1;39m{\n",
|
||||
" \u001b[0m\u001b[34;1m\"type\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"ai\"\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"data\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{\n",
|
||||
" \u001b[0m\u001b[34;1m\"content\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\" Hello Eugene! My name is Claude. It's nice to meet another cat lover.\"\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"additional_kwargs\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{}\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"type\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"ai\"\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"example\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39mfalse\u001b[0m\u001b[1;39m\n",
|
||||
" \u001b[1;39m}\u001b[0m\u001b[1;39m\n",
|
||||
" \u001b[1;39m}\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[1;39m{\n",
|
||||
" \u001b[0m\u001b[34;1m\"type\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"human\"\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"data\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{\n",
|
||||
" \u001b[0m\u001b[34;1m\"content\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"what was my name?\"\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"additional_kwargs\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{}\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"type\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"human\"\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"example\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39mfalse\u001b[0m\u001b[1;39m\n",
|
||||
" \u001b[1;39m}\u001b[0m\u001b[1;39m\n",
|
||||
" \u001b[1;39m}\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[1;39m{\n",
|
||||
" \u001b[0m\u001b[34;1m\"type\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"ai\"\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"data\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{\n",
|
||||
" \u001b[0m\u001b[34;1m\"content\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\" You told me your name is Eugene.\"\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"additional_kwargs\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{}\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"type\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"ai\"\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"example\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39mfalse\u001b[0m\u001b[1;39m\n",
|
||||
" \u001b[1;39m}\u001b[0m\u001b[1;39m\n",
|
||||
" \u001b[1;39m}\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[1;39m{\n",
|
||||
" \u001b[0m\u001b[34;1m\"type\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"human\"\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"data\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{\n",
|
||||
" \u001b[0m\u001b[34;1m\"content\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"What animal do i like?\"\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"additional_kwargs\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{}\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"type\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"human\"\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"example\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39mfalse\u001b[0m\u001b[1;39m\n",
|
||||
" \u001b[1;39m}\u001b[0m\u001b[1;39m\n",
|
||||
" \u001b[1;39m}\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[1;39m{\n",
|
||||
" \u001b[0m\u001b[34;1m\"type\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"ai\"\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"data\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{\n",
|
||||
" \u001b[0m\u001b[34;1m\"content\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\" You said you like cats.\"\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"additional_kwargs\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{}\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"type\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"ai\"\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"example\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39mfalse\u001b[0m\u001b[1;39m\n",
|
||||
" \u001b[1;39m}\u001b[0m\u001b[1;39m\n",
|
||||
" \u001b[1;39m}\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[1;39m{\n",
|
||||
" \u001b[0m\u001b[34;1m\"type\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"human\"\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"data\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{\n",
|
||||
" \u001b[0m\u001b[34;1m\"content\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"Can you count till 10?\"\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"additional_kwargs\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{}\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"type\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"human\"\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"example\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39mfalse\u001b[0m\u001b[1;39m\n",
|
||||
" \u001b[1;39m}\u001b[0m\u001b[1;39m\n",
|
||||
" \u001b[1;39m}\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[1;39m{\n",
|
||||
" \u001b[0m\u001b[34;1m\"type\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"AIMessageChunk\"\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"data\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{\n",
|
||||
" \u001b[0m\u001b[34;1m\"content\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\" Sure, I'd be happy to count to 10:\\n\\n1, 2, 3, 4, 5, 6, 7, 8, 9, 10\"\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"additional_kwargs\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{}\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"type\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"AIMessageChunk\"\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"example\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39mfalse\u001b[0m\u001b[1;39m\n",
|
||||
" \u001b[1;39m}\u001b[0m\u001b[1;39m\n",
|
||||
" \u001b[1;39m}\u001b[0m\u001b[1;39m\n",
|
||||
"\u001b[1;39m]\u001b[0m\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"!cat chat_histories/c7a327f3-5578-4fb7-a8f2-3082d7cb58cc.json | jq ."
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.9.6"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 4
|
||||
}
|
||||
Executable
+105
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env python
|
||||
"""Example of a chat server with persistence handled on the backend.
|
||||
|
||||
For simplicity, we're using file storage here -- to avoid the need to set up
|
||||
a database. This is obviously not a good idea for a production environment,
|
||||
but will help us to demonstrate the RunnableWithMessageHistory interface.
|
||||
|
||||
We'll use cookies to identify the user and/or session. This will help illustrate how to
|
||||
fetch configuration from the request.
|
||||
"""
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Callable, Union
|
||||
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from langchain.chat_models import ChatAnthropic
|
||||
from langchain.memory import FileChatMessageHistory
|
||||
from langchain_core.chat_history import BaseChatMessageHistory
|
||||
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||
from langchain_core.runnables.history import RunnableWithMessageHistory
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langserve import add_routes
|
||||
|
||||
|
||||
def _is_valid_identifier(value: str) -> bool:
|
||||
"""Check if the session ID is in a valid format."""
|
||||
# Use a regular expression to match the allowed characters
|
||||
valid_characters = re.compile(r"^[a-zA-Z0-9-_]+$")
|
||||
return bool(valid_characters.match(value))
|
||||
|
||||
|
||||
def create_session_factory(
|
||||
base_dir: Union[str, Path],
|
||||
) -> Callable[[str], BaseChatMessageHistory]:
|
||||
"""Create a session ID factory that creates session IDs from a base dir.
|
||||
|
||||
Args:
|
||||
base_dir: Base directory to use for storing the chat histories.
|
||||
|
||||
Returns:
|
||||
A session ID factory that creates session IDs from a base path.
|
||||
"""
|
||||
base_dir_ = Path(base_dir) if isinstance(base_dir, str) else base_dir
|
||||
if not base_dir_.exists():
|
||||
base_dir_.mkdir(parents=True)
|
||||
|
||||
def get_chat_history(session_id: str) -> FileChatMessageHistory:
|
||||
"""Get a chat history from a session ID."""
|
||||
if not _is_valid_identifier(session_id):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Session ID `{session_id}` is not in a valid format. "
|
||||
"Session ID must only contain alphanumeric characters, "
|
||||
"hyphens, and underscores.",
|
||||
)
|
||||
file_path = base_dir_ / f"{session_id}.json"
|
||||
return FileChatMessageHistory(str(file_path))
|
||||
|
||||
return get_chat_history
|
||||
|
||||
|
||||
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're an assistant by the name of Bob."),
|
||||
MessagesPlaceholder(variable_name="history"),
|
||||
("human", "{human_input}"),
|
||||
]
|
||||
)
|
||||
|
||||
chain = prompt | ChatAnthropic(model="claude-2")
|
||||
|
||||
|
||||
class InputChat(TypedDict):
|
||||
"""Input for the chat endpoint."""
|
||||
|
||||
human_input: str
|
||||
"""Human input"""
|
||||
|
||||
|
||||
chain_with_history = RunnableWithMessageHistory(
|
||||
chain,
|
||||
create_session_factory("chat_histories"),
|
||||
input_messages_key="human_input",
|
||||
history_messages_key="history",
|
||||
).with_types(input_type=InputChat)
|
||||
|
||||
|
||||
add_routes(
|
||||
app,
|
||||
chain_with_history,
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(app, host="localhost", port=8000)
|
||||
@@ -0,0 +1,359 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Chat History\n",
|
||||
"\n",
|
||||
"Here we'll be interacting with a server that's exposing a chat bot with message history being persisted on the backend."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import uuid\n",
|
||||
"from langserve import RemoteRunnable\n",
|
||||
"\n",
|
||||
"conversation_id = str(uuid.uuid4())\n",
|
||||
"chat = RemoteRunnable(\"http://localhost:8000/\", cookies={\"user_id\": \"eugene\"})"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Let's create a prompt composed of a system message and a human message."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"AIMessage(content=\"Hello Eugene! I'm Bob, your virtual assistant. How can I assist you today?\")"
|
||||
]
|
||||
},
|
||||
"execution_count": 4,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"chat.invoke({\"human_input\": \"my name is eugene. what is your name?\"}, {'configurable': { 'conversation_id': conversation_id } })"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"AIMessage(content='Your name is Eugene. Is there something specific you would like assistance with, Eugene?')"
|
||||
]
|
||||
},
|
||||
"execution_count": 5,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"chat.invoke({\"human_input\": \"what was my name?\"}, {'configurable': { 'conversation_id': conversation_id } })"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Use different user but same conversation id"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"chat = RemoteRunnable(\"http://localhost:8000/\", cookies={\"user_id\": \"nuno\"})"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"AIMessage(content=\"I apologize, but I don't have access to personal information about users. As an AI assistant, I prioritize user privacy and data protection.\")"
|
||||
]
|
||||
},
|
||||
"execution_count": 7,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"chat.invoke({\"human_input\": \"what was my name?\"}, {'configurable': { 'conversation_id': conversation_id }})"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\n",
|
||||
"\n",
|
||||
"Of\n",
|
||||
" course\n",
|
||||
"!\n",
|
||||
" Here\n",
|
||||
" you\n",
|
||||
" go\n",
|
||||
":\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"1\n",
|
||||
",\n",
|
||||
" \n",
|
||||
"2\n",
|
||||
",\n",
|
||||
" \n",
|
||||
"3\n",
|
||||
",\n",
|
||||
" \n",
|
||||
"4\n",
|
||||
",\n",
|
||||
" \n",
|
||||
"5\n",
|
||||
",\n",
|
||||
" \n",
|
||||
"6\n",
|
||||
",\n",
|
||||
" \n",
|
||||
"7\n",
|
||||
",\n",
|
||||
" \n",
|
||||
"8\n",
|
||||
",\n",
|
||||
" \n",
|
||||
"9\n",
|
||||
",\n",
|
||||
" \n",
|
||||
"10\n",
|
||||
".\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"for chunk in chat.stream({'human_input': \"Can you count till 10?\"}, {'configurable': { 'conversation_id': conversation_id } }):\n",
|
||||
" print()\n",
|
||||
" print(chunk.content, end='', flush=True)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 11,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"'cd8e5a55-0295-41cd-a885-775e0403fd25'"
|
||||
]
|
||||
},
|
||||
"execution_count": 11,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"conversation_id"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 12,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\u001b[01;34mchat_histories/\u001b[0m\n",
|
||||
"├── \u001b[01;34meugene\u001b[0m\n",
|
||||
"│ └── cd8e5a55-0295-41cd-a885-775e0403fd25.json\n",
|
||||
"└── \u001b[01;34mnuno\u001b[0m\n",
|
||||
" └── cd8e5a55-0295-41cd-a885-775e0403fd25.json\n",
|
||||
"\n",
|
||||
"2 directories, 2 files\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"!tree chat_histories/"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 17,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\u001b[1;39m[\n",
|
||||
" \u001b[1;39m{\n",
|
||||
" \u001b[0m\u001b[34;1m\"type\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"human\"\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"data\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{\n",
|
||||
" \u001b[0m\u001b[34;1m\"content\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"my name is eugene. what is your name?\"\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"additional_kwargs\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{}\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"type\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"human\"\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"example\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39mfalse\u001b[0m\u001b[1;39m\n",
|
||||
" \u001b[1;39m}\u001b[0m\u001b[1;39m\n",
|
||||
" \u001b[1;39m}\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[1;39m{\n",
|
||||
" \u001b[0m\u001b[34;1m\"type\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"ai\"\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"data\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{\n",
|
||||
" \u001b[0m\u001b[34;1m\"content\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"Hello Eugene! I'm Bob, your virtual assistant. How can I assist you today?\"\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"additional_kwargs\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{}\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"type\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"ai\"\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"example\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39mfalse\u001b[0m\u001b[1;39m\n",
|
||||
" \u001b[1;39m}\u001b[0m\u001b[1;39m\n",
|
||||
" \u001b[1;39m}\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[1;39m{\n",
|
||||
" \u001b[0m\u001b[34;1m\"type\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"human\"\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"data\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{\n",
|
||||
" \u001b[0m\u001b[34;1m\"content\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"what was my name?\"\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"additional_kwargs\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{}\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"type\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"human\"\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"example\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39mfalse\u001b[0m\u001b[1;39m\n",
|
||||
" \u001b[1;39m}\u001b[0m\u001b[1;39m\n",
|
||||
" \u001b[1;39m}\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[1;39m{\n",
|
||||
" \u001b[0m\u001b[34;1m\"type\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"ai\"\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"data\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{\n",
|
||||
" \u001b[0m\u001b[34;1m\"content\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"Your name is Eugene. Is there something specific you would like assistance with, Eugene?\"\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"additional_kwargs\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{}\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"type\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"ai\"\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"example\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39mfalse\u001b[0m\u001b[1;39m\n",
|
||||
" \u001b[1;39m}\u001b[0m\u001b[1;39m\n",
|
||||
" \u001b[1;39m}\u001b[0m\u001b[1;39m\n",
|
||||
"\u001b[1;39m]\u001b[0m\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"!cat chat_histories/eugene/cd8e5a55-0295-41cd-a885-775e0403fd25.json | jq ."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 18,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\u001b[1;39m[\n",
|
||||
" \u001b[1;39m{\n",
|
||||
" \u001b[0m\u001b[34;1m\"type\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"human\"\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"data\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{\n",
|
||||
" \u001b[0m\u001b[34;1m\"content\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"what was my name?\"\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"additional_kwargs\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{}\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"type\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"human\"\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"example\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39mfalse\u001b[0m\u001b[1;39m\n",
|
||||
" \u001b[1;39m}\u001b[0m\u001b[1;39m\n",
|
||||
" \u001b[1;39m}\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[1;39m{\n",
|
||||
" \u001b[0m\u001b[34;1m\"type\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"ai\"\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"data\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{\n",
|
||||
" \u001b[0m\u001b[34;1m\"content\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"I apologize, but I don't have access to personal information about users. As an AI assistant, I prioritize user privacy and data protection.\"\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"additional_kwargs\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{}\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"type\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"ai\"\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"example\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39mfalse\u001b[0m\u001b[1;39m\n",
|
||||
" \u001b[1;39m}\u001b[0m\u001b[1;39m\n",
|
||||
" \u001b[1;39m}\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[1;39m{\n",
|
||||
" \u001b[0m\u001b[34;1m\"type\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"human\"\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"data\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{\n",
|
||||
" \u001b[0m\u001b[34;1m\"content\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"Can you count till 10?\"\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"additional_kwargs\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{}\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"type\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"human\"\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"example\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39mfalse\u001b[0m\u001b[1;39m\n",
|
||||
" \u001b[1;39m}\u001b[0m\u001b[1;39m\n",
|
||||
" \u001b[1;39m}\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[1;39m{\n",
|
||||
" \u001b[0m\u001b[34;1m\"type\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"AIMessageChunk\"\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"data\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{\n",
|
||||
" \u001b[0m\u001b[34;1m\"content\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"Of course! Here you go:\\n\\n1, 2, 3, 4, 5, 6, 7, 8, 9, 10.\"\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"additional_kwargs\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[1;39m{}\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"type\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;32m\"AIMessageChunk\"\u001b[0m\u001b[1;39m,\n",
|
||||
" \u001b[0m\u001b[34;1m\"example\"\u001b[0m\u001b[1;39m: \u001b[0m\u001b[0;39mfalse\u001b[0m\u001b[1;39m\n",
|
||||
" \u001b[1;39m}\u001b[0m\u001b[1;39m\n",
|
||||
" \u001b[1;39m}\u001b[0m\u001b[1;39m\n",
|
||||
"\u001b[1;39m]\u001b[0m\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"!cat chat_histories/nuno/cd8e5a55-0295-41cd-a885-775e0403fd25.json | jq ."
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.9.6"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 4
|
||||
}
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
#!/usr/bin/env python
|
||||
"""Example of a chat server with persistence handled on the backend.
|
||||
|
||||
For simplicity, we're using file storage here -- to avoid the need to set up
|
||||
a database. This is obviously not a good idea for a production environment,
|
||||
but will help us to demonstrate the RunnableWithMessageHistory interface.
|
||||
|
||||
We'll use cookies to identify the user. This will help illustrate how to
|
||||
fetch configuration from the request.
|
||||
"""
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Dict, Union
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Request
|
||||
from langchain.chat_models import ChatOpenAI
|
||||
from langchain.memory import FileChatMessageHistory
|
||||
from langchain.schema.runnable.utils import ConfigurableFieldSpec
|
||||
from langchain_core import __version__
|
||||
from langchain_core.chat_history import BaseChatMessageHistory
|
||||
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||
from langchain_core.runnables.history import RunnableWithMessageHistory
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langserve import add_routes
|
||||
|
||||
# Define the minimum required version as (0, 1, 0)
|
||||
# Earlier versions did not allow specifying custom config fields in
|
||||
# RunnableWithMessageHistory.
|
||||
MIN_VERSION_LANGCHAIN_CORE = (0, 1, 0)
|
||||
|
||||
# Split the version string by "." and convert to integers
|
||||
LANGCHAIN_CORE_VERSION = tuple(map(int, __version__.split(".")))
|
||||
|
||||
if LANGCHAIN_CORE_VERSION < MIN_VERSION_LANGCHAIN_CORE:
|
||||
raise RuntimeError(
|
||||
f"Minimum required version of langchain-core is {MIN_VERSION_LANGCHAIN_CORE}, "
|
||||
f"but found {LANGCHAIN_CORE_VERSION}"
|
||||
)
|
||||
|
||||
|
||||
def _is_valid_identifier(value: str) -> bool:
|
||||
"""Check if the value is a valid identifier."""
|
||||
# Use a regular expression to match the allowed characters
|
||||
valid_characters = re.compile(r"^[a-zA-Z0-9-_]+$")
|
||||
return bool(valid_characters.match(value))
|
||||
|
||||
|
||||
def create_session_factory(
|
||||
base_dir: Union[str, Path],
|
||||
) -> Callable[[str], BaseChatMessageHistory]:
|
||||
"""Create a factory that can retrieve chat histories.
|
||||
|
||||
The chat histories are keyed by user ID and conversation ID.
|
||||
|
||||
Args:
|
||||
base_dir: Base directory to use for storing the chat histories.
|
||||
|
||||
Returns:
|
||||
A factory that can retrieve chat histories keyed by user ID and conversation ID.
|
||||
"""
|
||||
base_dir_ = Path(base_dir) if isinstance(base_dir, str) else base_dir
|
||||
if not base_dir_.exists():
|
||||
base_dir_.mkdir(parents=True)
|
||||
|
||||
def get_chat_history(user_id: str, conversation_id: str) -> FileChatMessageHistory:
|
||||
"""Get a chat history from a user id and conversation id."""
|
||||
if not _is_valid_identifier(user_id):
|
||||
raise ValueError(
|
||||
f"User ID {user_id} is not in a valid format. "
|
||||
"User ID must only contain alphanumeric characters, "
|
||||
"hyphens, and underscores."
|
||||
"Please include a valid cookie in the request headers called 'user-id'."
|
||||
)
|
||||
if not _is_valid_identifier(conversation_id):
|
||||
raise ValueError(
|
||||
f"Conversation ID {conversation_id} is not in a valid format. "
|
||||
"Conversation ID must only contain alphanumeric characters, "
|
||||
"hyphens, and underscores. Please provide a valid conversation id "
|
||||
"via config. For example, "
|
||||
"chain.invoke(.., {'configurable': {'conversation_id': '123'}})"
|
||||
)
|
||||
|
||||
user_dir = base_dir_ / user_id
|
||||
if not user_dir.exists():
|
||||
user_dir.mkdir(parents=True)
|
||||
file_path = user_dir / f"{conversation_id}.json"
|
||||
return FileChatMessageHistory(str(file_path))
|
||||
|
||||
return get_chat_history
|
||||
|
||||
|
||||
app = FastAPI(
|
||||
title="LangChain Server",
|
||||
version="1.0",
|
||||
description="Spin up a simple api server using Langchain's Runnable interfaces",
|
||||
)
|
||||
|
||||
|
||||
def _per_request_config_modifier(
|
||||
config: Dict[str, Any], request: Request
|
||||
) -> Dict[str, Any]:
|
||||
"""Update the config"""
|
||||
config = config.copy()
|
||||
configurable = config.get("configurable", {})
|
||||
# Look for a cookie named "user_id"
|
||||
user_id = request.cookies.get("user_id", None)
|
||||
|
||||
if user_id is None:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="No user id found. Please set a cookie named 'user_id'.",
|
||||
)
|
||||
|
||||
configurable["user_id"] = user_id
|
||||
config["configurable"] = configurable
|
||||
return config
|
||||
|
||||
|
||||
# Declare a chain
|
||||
prompt = ChatPromptTemplate.from_messages(
|
||||
[
|
||||
("system", "You're an assistant by the name of Bob."),
|
||||
MessagesPlaceholder(variable_name="history"),
|
||||
("human", "{human_input}"),
|
||||
]
|
||||
)
|
||||
|
||||
chain = prompt | ChatOpenAI()
|
||||
|
||||
|
||||
class InputChat(TypedDict):
|
||||
"""Input for the chat endpoint."""
|
||||
|
||||
human_input: str
|
||||
"""Human input"""
|
||||
|
||||
|
||||
chain_with_history = RunnableWithMessageHistory(
|
||||
chain,
|
||||
create_session_factory("chat_histories"),
|
||||
input_messages_key="human_input",
|
||||
history_messages_key="history",
|
||||
history_factory_config=[
|
||||
ConfigurableFieldSpec(
|
||||
id="user_id",
|
||||
annotation=str,
|
||||
name="User ID",
|
||||
description="Unique identifier for the user.",
|
||||
default="",
|
||||
is_shared=True,
|
||||
),
|
||||
ConfigurableFieldSpec(
|
||||
id="conversation_id",
|
||||
annotation=str,
|
||||
name="Conversation ID",
|
||||
description="Unique identifier for the conversation.",
|
||||
default="",
|
||||
is_shared=True,
|
||||
),
|
||||
],
|
||||
).with_types(input_type=InputChat)
|
||||
|
||||
|
||||
add_routes(
|
||||
app,
|
||||
chain_with_history,
|
||||
per_req_config_modifier=_per_request_config_modifier,
|
||||
# Disable playground and batch
|
||||
# 1) Playground we're passing information via headers, which is not supported via
|
||||
# the playground right now.
|
||||
# 2) Disable batch to avoid users being confused. Batch will work fine
|
||||
# as long as users invoke it with multiple configs appropriately, but
|
||||
# without validation users are likely going to forget to do that.
|
||||
# In addition, there's likely little sense in support batch for a chatbot.
|
||||
disabled_endpoints=["playground", "batch"],
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(app, host="localhost", port=8000)
|
||||
@@ -59,13 +59,32 @@ prompt = PromptTemplate.from_template(
|
||||
)
|
||||
chain = prompt | model | StrOutputParser()
|
||||
|
||||
# Add routes requires you to specify which config keys are accepted
|
||||
# specifically, you must accept `configurable` as a config key.
|
||||
add_routes(app, chain, path="/configurable_temp", config_keys=["configurable"])
|
||||
add_routes(app, chain, path="/configurable_temp")
|
||||
|
||||
|
||||
###############################################################################
|
||||
# EXAMPLE 2: Configure fields based on Request metadata #
|
||||
# EXAMPLE 2: Configure prompt based on RunnableConfig #
|
||||
###############################################################################
|
||||
configurable_prompt = PromptTemplate.from_template(
|
||||
"tell me a joke about {topic}."
|
||||
).configurable_alternatives(
|
||||
ConfigurableField(
|
||||
id="prompt",
|
||||
name="Prompt",
|
||||
description="The prompt to use. Must contain {topic}.",
|
||||
),
|
||||
default_key="joke",
|
||||
fact=PromptTemplate.from_template(
|
||||
"tell me a fact about {topic} in {language} language."
|
||||
),
|
||||
)
|
||||
prompt_chain = configurable_prompt | model | StrOutputParser()
|
||||
|
||||
add_routes(app, prompt_chain, path="/configurable_prompt")
|
||||
|
||||
|
||||
###############################################################################
|
||||
# EXAMPLE 3: Configure fields based on Request metadata #
|
||||
###############################################################################
|
||||
|
||||
|
||||
@@ -96,7 +115,6 @@ add_routes(
|
||||
app,
|
||||
dynamic_auth_chain,
|
||||
path="/auth_from_header",
|
||||
config_keys=["configurable"],
|
||||
per_req_config_modifier=fetch_api_key_from_header,
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Client\n",
|
||||
"\n",
|
||||
"Demo of a client interacting with a configurable retriever (see server code)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"You can interact with this via API directly"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'output': [{'page_content': 'cats like fish',\n",
|
||||
" 'metadata': {},\n",
|
||||
" 'type': 'Document'},\n",
|
||||
" {'page_content': 'dogs like sticks', 'metadata': {}, 'type': 'Document'}],\n",
|
||||
" 'callback_events': [],\n",
|
||||
" 'metadata': {'run_id': 'f375cdf6-2848-4976-9565-f69e175c24ce'}}"
|
||||
]
|
||||
},
|
||||
"execution_count": 1,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"import requests\n",
|
||||
"\n",
|
||||
"inputs = {\"input\": \"cat\"}\n",
|
||||
"response = requests.post(\"http://localhost:8000/invoke\", json=inputs)\n",
|
||||
"\n",
|
||||
"response.json()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"You can also interact with this via the RemoteRunnable interface (to use in other chains)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langserve import RemoteRunnable\n",
|
||||
"\n",
|
||||
"remote_runnable = RemoteRunnable(\"http://localhost:8000/\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Remote runnable has the same interface as local runnables"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"[Document(page_content='cats like fish'),\n",
|
||||
" Document(page_content='dogs like sticks')]"
|
||||
]
|
||||
},
|
||||
"execution_count": 3,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"await remote_runnable.ainvoke(\"cat\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"[Document(page_content='cats like fish'),\n",
|
||||
" Document(page_content='dogs like sticks')]"
|
||||
]
|
||||
},
|
||||
"execution_count": 4,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"await remote_runnable.ainvoke(\"cat\", {\"configurable\": {\"collection_name\": \"Index 1\"}})"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"[Document(page_content='x_n+1=a * xn * (1-xn)')]"
|
||||
]
|
||||
},
|
||||
"execution_count": 5,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"await remote_runnable.ainvoke(\"cat\", {\"configurable\": {\"collection_name\": \"Index 2\"}})"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.9.6"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 4
|
||||
}
|
||||
Executable
+126
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env python
|
||||
"""A more complex example that shows how to configure index name at run time."""
|
||||
from typing import Any, Iterable, List, Optional, Type
|
||||
|
||||
from fastapi import FastAPI
|
||||
from langchain.embeddings import OpenAIEmbeddings
|
||||
from langchain.schema import Document
|
||||
from langchain.schema.embeddings import Embeddings
|
||||
from langchain.schema.retriever import BaseRetriever
|
||||
from langchain.schema.runnable import (
|
||||
ConfigurableFieldSingleOption,
|
||||
RunnableConfig,
|
||||
RunnableSerializable,
|
||||
)
|
||||
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()
|
||||
)
|
||||
|
||||
vectorstore2 = FAISS.from_texts(["x_n+1=a * xn * (1-xn)"], embedding=OpenAIEmbeddings())
|
||||
|
||||
|
||||
app = FastAPI(
|
||||
title="LangChain Server",
|
||||
version="1.0",
|
||||
description="Spin up a simple api server using Langchain's Runnable interfaces",
|
||||
)
|
||||
|
||||
|
||||
class UnderlyingVectorStore(VectorStore):
|
||||
"""This is a fake vectorstore for demo purposes."""
|
||||
|
||||
def __init__(self, collection_name: str) -> None:
|
||||
"""Fake vectorstore that has a collection name."""
|
||||
self.collection_name = collection_name
|
||||
|
||||
def as_retriever(self) -> BaseRetriever:
|
||||
if self.collection_name == "index1":
|
||||
return vectorstore1.as_retriever()
|
||||
elif self.collection_name == "index2":
|
||||
return vectorstore2.as_retriever()
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f"No retriever for collection {self.collection_name}"
|
||||
)
|
||||
|
||||
def add_texts(
|
||||
self,
|
||||
texts: Iterable[str],
|
||||
metadatas: Optional[List[dict]] = None,
|
||||
**kwargs: Any,
|
||||
) -> List[str]:
|
||||
raise NotImplementedError()
|
||||
|
||||
@classmethod
|
||||
def from_texts(
|
||||
cls: Type[VST],
|
||||
texts: List[str],
|
||||
embedding: Embeddings,
|
||||
metadatas: Optional[List[dict]] = None,
|
||||
**kwargs: Any,
|
||||
) -> VST:
|
||||
raise NotImplementedError()
|
||||
|
||||
def similarity_search(
|
||||
self, embedding: List[float], k: int = 4, **kwargs: Any
|
||||
) -> List[Document]:
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class ConfigurableRetriever(RunnableSerializable[str, List[Document]]):
|
||||
"""Create a custom retriever that can be configured by the user.
|
||||
|
||||
This is an example of how to create a custom runnable that can be configured
|
||||
to use a different collection name at run time.
|
||||
|
||||
Configuration involves instantiating a VectorStore with a collection name.
|
||||
at run time, so the underlying vectorstore should be *cheap* to instantiate.
|
||||
|
||||
For example, it should not be making any network requests at instantiation time.
|
||||
|
||||
Make sure that the vectorstore you use meets this criteria.
|
||||
"""
|
||||
|
||||
collection_name: str
|
||||
|
||||
def invoke(
|
||||
self, input: str, config: Optional[RunnableConfig] = None
|
||||
) -> List[Document]:
|
||||
"""Invoke the retriever."""
|
||||
vectorstore = UnderlyingVectorStore(self.collection_name)
|
||||
retriever = vectorstore.as_retriever()
|
||||
return retriever.invoke(input, config=config)
|
||||
|
||||
|
||||
configurable_collection_name = ConfigurableRetriever(
|
||||
collection_name="index1"
|
||||
).configurable_fields(
|
||||
collection_name=ConfigurableFieldSingleOption(
|
||||
id="collection_name",
|
||||
name="Collection Name",
|
||||
description="The name of the collection to use for the retriever.",
|
||||
options={
|
||||
"Index 1": "index1",
|
||||
"Index 2": "index2",
|
||||
},
|
||||
default="Index 1",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class Request(BaseModel):
|
||||
__root__: str = Field(default="cat", description="Search query")
|
||||
|
||||
|
||||
add_routes(app, configurable_collection_name.with_types(input_type=Request))
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(app, host="localhost", port=8000)
|
||||
@@ -18,7 +18,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"execution_count": 11,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
@@ -26,10 +26,15 @@
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'output': {'answer': 'Cats like fish.'}, 'callback_events': []}"
|
||||
"{'output': {'content': 'Based on the given context, the information we have about Harrison is that he worked at Kensho.',\n",
|
||||
" 'additional_kwargs': {},\n",
|
||||
" 'type': 'ai',\n",
|
||||
" 'example': False},\n",
|
||||
" 'callback_events': [],\n",
|
||||
" 'metadata': {'run_id': '3455df2b-93f8-4e67-b1a3-27f90670cf7b'}}"
|
||||
]
|
||||
},
|
||||
"execution_count": 1,
|
||||
"execution_count": 11,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
@@ -37,7 +42,7 @@
|
||||
"source": [
|
||||
"import requests\n",
|
||||
"\n",
|
||||
"inputs = {\"input\": {\"question\": \"what do cats like?\", \"chat_history\": []}}\n",
|
||||
"inputs = {\"input\": {\"question\": \"what do you know about harrison\", \"chat_history\": []}}\n",
|
||||
"response = requests.post(\"http://localhost:8000/invoke\", json=inputs)\n",
|
||||
"\n",
|
||||
"response.json()"
|
||||
@@ -52,7 +57,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"execution_count": 12,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
@@ -72,7 +77,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"execution_count": 13,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
@@ -80,21 +85,21 @@
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'answer': 'Cats like fish.'}"
|
||||
"AIMessage(content='Based on the given context, the only information we have about Harrison is that he worked at Kensho.')"
|
||||
]
|
||||
},
|
||||
"execution_count": 3,
|
||||
"execution_count": 13,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"await remote_runnable.ainvoke({\"question\": \"what do cats like?\", \"chat_history\": []})"
|
||||
"await remote_runnable.ainvoke({\"question\": \"what do you know about harrison\", \"chat_history\": []})"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"execution_count": 14,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
@@ -102,26 +107,501 @@
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'answer': 'Cats like fish.'}"
|
||||
"AIMessage(content='Harrison worked at Kensho.')"
|
||||
]
|
||||
},
|
||||
"execution_count": 6,
|
||||
"execution_count": 14,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"await remote_runnable.ainvoke(\n",
|
||||
" {\"question\": \"what do cats like?\", \"chat_history\": [(\"hi\", \"hi\")]}\n",
|
||||
" {\"question\": \"what do you know about harrison\", \"chat_history\": [(\"hi\", \"hi\")]}\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"execution_count": 16,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"content=''\n",
|
||||
"content='H'\n",
|
||||
"content='arrison'\n",
|
||||
"content=' worked'\n",
|
||||
"content=' at'\n",
|
||||
"content=' Kens'\n",
|
||||
"content='ho'\n",
|
||||
"content='.'\n",
|
||||
"content=''\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"async for chunk in remote_runnable.astream(\n",
|
||||
" {\"question\": \"what do you know about harrison\", \"chat_history\": [(\"hi\", \"hi\")]}\n",
|
||||
"):\n",
|
||||
" print(chunk)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
"source": [
|
||||
"stream log shows all intermediate steps as well!"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 18,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"RunLogPatch({'op': 'replace',\n",
|
||||
" 'path': '',\n",
|
||||
" 'value': {'final_output': None,\n",
|
||||
" 'id': '2ff5a98d-49f0-40ae-92fe-489c3047d1c3',\n",
|
||||
" 'logs': {},\n",
|
||||
" 'streamed_output': []}})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/RunnableParallel',\n",
|
||||
" 'value': {'end_time': None,\n",
|
||||
" 'final_output': None,\n",
|
||||
" 'id': 'ffdda3c9-a0ba-49a6-af18-c01748c31801',\n",
|
||||
" 'metadata': {},\n",
|
||||
" 'name': 'RunnableParallel',\n",
|
||||
" 'start_time': '2023-11-16T15:59:23.348',\n",
|
||||
" 'streamed_output_str': [],\n",
|
||||
" 'tags': ['seq:step:1'],\n",
|
||||
" 'type': 'chain'}})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/RunnableSequence',\n",
|
||||
" 'value': {'end_time': None,\n",
|
||||
" 'final_output': None,\n",
|
||||
" 'id': 'a9da3f2c-f3ae-44c1-a2f4-1035faa0d1c2',\n",
|
||||
" 'metadata': {},\n",
|
||||
" 'name': 'RunnableSequence',\n",
|
||||
" 'start_time': '2023-11-16T15:59:23.349',\n",
|
||||
" 'streamed_output_str': [],\n",
|
||||
" 'tags': ['map:key:standalone_question'],\n",
|
||||
" 'type': 'chain'}})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/RunnableParallel:2',\n",
|
||||
" 'value': {'end_time': None,\n",
|
||||
" 'final_output': None,\n",
|
||||
" 'id': '4d32b08f-a989-4c01-8068-479bed506d99',\n",
|
||||
" 'metadata': {},\n",
|
||||
" 'name': 'RunnableParallel',\n",
|
||||
" 'start_time': '2023-11-16T15:59:23.349',\n",
|
||||
" 'streamed_output_str': [],\n",
|
||||
" 'tags': ['seq:step:1'],\n",
|
||||
" 'type': 'chain'}})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/<lambda>',\n",
|
||||
" 'value': {'end_time': None,\n",
|
||||
" 'final_output': None,\n",
|
||||
" 'id': 'c7b9bd78-cbb4-41f9-b31a-50a5f8940a8d',\n",
|
||||
" 'metadata': {},\n",
|
||||
" 'name': '<lambda>',\n",
|
||||
" 'start_time': '2023-11-16T15:59:23.350',\n",
|
||||
" 'streamed_output_str': [],\n",
|
||||
" 'tags': ['map:key:chat_history'],\n",
|
||||
" 'type': 'chain'}})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/<lambda>/final_output',\n",
|
||||
" 'value': {'output': '\\nHuman: hi\\nAssistant: hi'}},\n",
|
||||
" {'op': 'add',\n",
|
||||
" 'path': '/logs/<lambda>/end_time',\n",
|
||||
" 'value': '2023-11-16T15:59:23.350'})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/RunnableParallel:2/final_output',\n",
|
||||
" 'value': {'chat_history': '\\nHuman: hi\\nAssistant: hi'}},\n",
|
||||
" {'op': 'add',\n",
|
||||
" 'path': '/logs/RunnableParallel:2/end_time',\n",
|
||||
" 'value': '2023-11-16T15:59:23.351'})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/PromptTemplate',\n",
|
||||
" 'value': {'end_time': None,\n",
|
||||
" 'final_output': None,\n",
|
||||
" 'id': 'ef9f8729-2d4a-4887-86a8-a284d64f5882',\n",
|
||||
" 'metadata': {},\n",
|
||||
" 'name': 'PromptTemplate',\n",
|
||||
" 'start_time': '2023-11-16T15:59:23.351',\n",
|
||||
" 'streamed_output_str': [],\n",
|
||||
" 'tags': ['seq:step:2'],\n",
|
||||
" 'type': 'prompt'}})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/PromptTemplate/final_output',\n",
|
||||
" 'value': StringPromptValue(text='Given the following conversation and a follow up question, rephrase the \\nfollow up question to be a standalone question, in its original language.\\n\\nChat History:\\n\\nHuman: hi\\nAssistant: hi\\nFollow Up Input: what do you know about harrison\\nStandalone question:')},\n",
|
||||
" {'op': 'add',\n",
|
||||
" 'path': '/logs/PromptTemplate/end_time',\n",
|
||||
" 'value': '2023-11-16T15:59:23.351'})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/ChatOpenAI',\n",
|
||||
" 'value': {'end_time': None,\n",
|
||||
" 'final_output': None,\n",
|
||||
" 'id': '7cda5923-ed2a-42ea-aee7-a2391371ff2f',\n",
|
||||
" 'metadata': {},\n",
|
||||
" 'name': 'ChatOpenAI',\n",
|
||||
" 'start_time': '2023-11-16T15:59:23.352',\n",
|
||||
" 'streamed_output_str': [],\n",
|
||||
" 'tags': ['seq:step:3'],\n",
|
||||
" 'type': 'llm'}})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/StrOutputParser',\n",
|
||||
" 'value': {'end_time': None,\n",
|
||||
" 'final_output': None,\n",
|
||||
" 'id': '6a0e199d-51fa-4306-8a7d-054444c6855f',\n",
|
||||
" 'metadata': {},\n",
|
||||
" 'name': 'StrOutputParser',\n",
|
||||
" 'start_time': '2023-11-16T15:59:24.613',\n",
|
||||
" 'streamed_output_str': [],\n",
|
||||
" 'tags': ['seq:step:4'],\n",
|
||||
" 'type': 'parser'}})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/RunnableParallel:3',\n",
|
||||
" 'value': {'end_time': None,\n",
|
||||
" 'final_output': None,\n",
|
||||
" 'id': '74a13de1-c2d1-43c0-813a-7ce0246118b3',\n",
|
||||
" 'metadata': {},\n",
|
||||
" 'name': 'RunnableParallel',\n",
|
||||
" 'start_time': '2023-11-16T15:59:24.616',\n",
|
||||
" 'streamed_output_str': [],\n",
|
||||
" 'tags': ['seq:step:2'],\n",
|
||||
" 'type': 'chain'}})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/RunnableSequence:2',\n",
|
||||
" 'value': {'end_time': None,\n",
|
||||
" 'final_output': None,\n",
|
||||
" 'id': '625e2183-7ee5-4f1f-8f47-56541afc96ee',\n",
|
||||
" 'metadata': {},\n",
|
||||
" 'name': 'RunnableSequence',\n",
|
||||
" 'start_time': '2023-11-16T15:59:24.619',\n",
|
||||
" 'streamed_output_str': [],\n",
|
||||
" 'tags': ['map:key:context'],\n",
|
||||
" 'type': 'chain'}})\n",
|
||||
"RunLogPatch({'op': 'add', 'path': '/logs/ChatOpenAI/streamed_output_str/-', 'value': ''})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/ChatOpenAI/streamed_output_str/-',\n",
|
||||
" 'value': 'What'})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/ChatOpenAI/streamed_output_str/-',\n",
|
||||
" 'value': ' information'})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/ChatOpenAI/streamed_output_str/-',\n",
|
||||
" 'value': ' do'})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/ChatOpenAI/streamed_output_str/-',\n",
|
||||
" 'value': ' you'})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/ChatOpenAI/streamed_output_str/-',\n",
|
||||
" 'value': ' have'})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/ChatOpenAI/streamed_output_str/-',\n",
|
||||
" 'value': ' about'})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/ChatOpenAI/streamed_output_str/-',\n",
|
||||
" 'value': ' Harrison'})\n",
|
||||
"RunLogPatch({'op': 'add', 'path': '/logs/ChatOpenAI/streamed_output_str/-', 'value': '?'})\n",
|
||||
"RunLogPatch({'op': 'add', 'path': '/logs/ChatOpenAI/streamed_output_str/-', 'value': ''})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/ChatOpenAI/final_output',\n",
|
||||
" 'value': LLMResult(generations=[[ChatGenerationChunk(text='What information do you have about Harrison?', generation_info={'finish_reason': 'stop'}, message=AIMessageChunk(content='What information do you have about Harrison?'))]], llm_output=None, run=None)},\n",
|
||||
" {'op': 'add',\n",
|
||||
" 'path': '/logs/ChatOpenAI/end_time',\n",
|
||||
" 'value': '2023-11-16T15:59:24.832'})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/StrOutputParser/final_output',\n",
|
||||
" 'value': {'output': 'What information do you have about Harrison?'}},\n",
|
||||
" {'op': 'add',\n",
|
||||
" 'path': '/logs/StrOutputParser/end_time',\n",
|
||||
" 'value': '2023-11-16T15:59:24.833'})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/RunnableSequence/final_output',\n",
|
||||
" 'value': {'output': 'What information do you have about Harrison?'}},\n",
|
||||
" {'op': 'add',\n",
|
||||
" 'path': '/logs/RunnableSequence/end_time',\n",
|
||||
" 'value': '2023-11-16T15:59:24.834'})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/RunnableParallel/final_output',\n",
|
||||
" 'value': {'standalone_question': 'What information do you have about '\n",
|
||||
" 'Harrison?'}},\n",
|
||||
" {'op': 'add',\n",
|
||||
" 'path': '/logs/RunnableParallel/end_time',\n",
|
||||
" 'value': '2023-11-16T15:59:24.835'})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/RunnableLambda',\n",
|
||||
" 'value': {'end_time': None,\n",
|
||||
" 'final_output': None,\n",
|
||||
" 'id': '71b2e8dc-753f-4bf2-83de-b87b26828370',\n",
|
||||
" 'metadata': {},\n",
|
||||
" 'name': 'RunnableLambda',\n",
|
||||
" 'start_time': '2023-11-16T15:59:24.837',\n",
|
||||
" 'streamed_output_str': [],\n",
|
||||
" 'tags': ['seq:step:1'],\n",
|
||||
" 'type': 'chain'}})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/RunnableLambda/final_output',\n",
|
||||
" 'value': {'output': 'What information do you have about Harrison?'}},\n",
|
||||
" {'op': 'add',\n",
|
||||
" 'path': '/logs/RunnableLambda/end_time',\n",
|
||||
" 'value': '2023-11-16T15:59:24.837'})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/Retriever',\n",
|
||||
" 'value': {'end_time': None,\n",
|
||||
" 'final_output': None,\n",
|
||||
" 'id': 'd3d6254d-d073-478f-b00b-bc27eafa24fd',\n",
|
||||
" 'metadata': {},\n",
|
||||
" 'name': 'Retriever',\n",
|
||||
" 'start_time': '2023-11-16T15:59:24.839',\n",
|
||||
" 'streamed_output_str': [],\n",
|
||||
" 'tags': ['seq:step:2', 'FAISS', 'OpenAIEmbeddings'],\n",
|
||||
" 'type': 'retriever'}})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/<lambda>:2',\n",
|
||||
" 'value': {'end_time': None,\n",
|
||||
" 'final_output': None,\n",
|
||||
" 'id': '77f7132a-d98f-4121-89cf-10e462c26496',\n",
|
||||
" 'metadata': {},\n",
|
||||
" 'name': '<lambda>',\n",
|
||||
" 'start_time': '2023-11-16T15:59:24.839',\n",
|
||||
" 'streamed_output_str': [],\n",
|
||||
" 'tags': ['map:key:question'],\n",
|
||||
" 'type': 'chain'}})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/<lambda>:2/final_output',\n",
|
||||
" 'value': {'output': 'What information do you have about Harrison?'}},\n",
|
||||
" {'op': 'add',\n",
|
||||
" 'path': '/logs/<lambda>:2/end_time',\n",
|
||||
" 'value': '2023-11-16T15:59:24.840'})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/Retriever/final_output',\n",
|
||||
" 'value': {'documents': [Document(page_content='harrison worked at kensho')]}},\n",
|
||||
" {'op': 'add',\n",
|
||||
" 'path': '/logs/Retriever/end_time',\n",
|
||||
" 'value': '2023-11-16T15:59:25.074'})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/_combine_documents',\n",
|
||||
" 'value': {'end_time': None,\n",
|
||||
" 'final_output': None,\n",
|
||||
" 'id': '82aecf3e-ca9d-4b48-a281-03f8e834ea62',\n",
|
||||
" 'metadata': {},\n",
|
||||
" 'name': '_combine_documents',\n",
|
||||
" 'start_time': '2023-11-16T15:59:25.075',\n",
|
||||
" 'streamed_output_str': [],\n",
|
||||
" 'tags': ['seq:step:3'],\n",
|
||||
" 'type': 'chain'}})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/_combine_documents/final_output',\n",
|
||||
" 'value': {'output': 'harrison worked at kensho'}},\n",
|
||||
" {'op': 'add',\n",
|
||||
" 'path': '/logs/_combine_documents/end_time',\n",
|
||||
" 'value': '2023-11-16T15:59:25.075'})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/RunnableSequence:2/final_output',\n",
|
||||
" 'value': {'output': 'harrison worked at kensho'}},\n",
|
||||
" {'op': 'add',\n",
|
||||
" 'path': '/logs/RunnableSequence:2/end_time',\n",
|
||||
" 'value': '2023-11-16T15:59:25.075'})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/RunnableParallel:3/final_output',\n",
|
||||
" 'value': {'context': 'harrison worked at kensho',\n",
|
||||
" 'question': 'What information do you have about Harrison?'}},\n",
|
||||
" {'op': 'add',\n",
|
||||
" 'path': '/logs/RunnableParallel:3/end_time',\n",
|
||||
" 'value': '2023-11-16T15:59:25.076'})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/ChatPromptTemplate',\n",
|
||||
" 'value': {'end_time': None,\n",
|
||||
" 'final_output': None,\n",
|
||||
" 'id': 'b37f36ce-3a27-4402-81c4-6893f03ec179',\n",
|
||||
" 'metadata': {},\n",
|
||||
" 'name': 'ChatPromptTemplate',\n",
|
||||
" 'start_time': '2023-11-16T15:59:25.076',\n",
|
||||
" 'streamed_output_str': [],\n",
|
||||
" 'tags': ['seq:step:3'],\n",
|
||||
" 'type': 'prompt'}})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/ChatPromptTemplate/final_output',\n",
|
||||
" 'value': {'messages': [HumanMessage(content='Answer the question based only on the following context:\\nharrison worked at kensho\\n\\nQuestion: What information do you have about Harrison?\\n')]}},\n",
|
||||
" {'op': 'add',\n",
|
||||
" 'path': '/logs/ChatPromptTemplate/end_time',\n",
|
||||
" 'value': '2023-11-16T15:59:25.077'})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/ChatOpenAI:2',\n",
|
||||
" 'value': {'end_time': None,\n",
|
||||
" 'final_output': None,\n",
|
||||
" 'id': 'b1179088-eb7c-49c5-af1e-bd09c48408bf',\n",
|
||||
" 'metadata': {},\n",
|
||||
" 'name': 'ChatOpenAI',\n",
|
||||
" 'start_time': '2023-11-16T15:59:25.078',\n",
|
||||
" 'streamed_output_str': [],\n",
|
||||
" 'tags': ['seq:step:4'],\n",
|
||||
" 'type': 'llm'}})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/streamed_output/-',\n",
|
||||
" 'value': AIMessageChunk(content='')})\n",
|
||||
"RunLogPatch({'op': 'add', 'path': '/logs/ChatOpenAI:2/streamed_output_str/-', 'value': ''})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/streamed_output/-',\n",
|
||||
" 'value': AIMessageChunk(content='Based')})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n",
|
||||
" 'value': 'Based'})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/streamed_output/-',\n",
|
||||
" 'value': AIMessageChunk(content=' on')})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n",
|
||||
" 'value': ' on'})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/streamed_output/-',\n",
|
||||
" 'value': AIMessageChunk(content=' the')})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n",
|
||||
" 'value': ' the'})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/streamed_output/-',\n",
|
||||
" 'value': AIMessageChunk(content=' given')})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n",
|
||||
" 'value': ' given'})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/streamed_output/-',\n",
|
||||
" 'value': AIMessageChunk(content=' context')})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n",
|
||||
" 'value': ' context'})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/streamed_output/-',\n",
|
||||
" 'value': AIMessageChunk(content=',')})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n",
|
||||
" 'value': ','})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/streamed_output/-',\n",
|
||||
" 'value': AIMessageChunk(content=' the')})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n",
|
||||
" 'value': ' the'})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/streamed_output/-',\n",
|
||||
" 'value': AIMessageChunk(content=' only')})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n",
|
||||
" 'value': ' only'})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/streamed_output/-',\n",
|
||||
" 'value': AIMessageChunk(content=' information')})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n",
|
||||
" 'value': ' information'})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/streamed_output/-',\n",
|
||||
" 'value': AIMessageChunk(content=' we')})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n",
|
||||
" 'value': ' we'})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/streamed_output/-',\n",
|
||||
" 'value': AIMessageChunk(content=' have')})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n",
|
||||
" 'value': ' have'})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/streamed_output/-',\n",
|
||||
" 'value': AIMessageChunk(content=' about')})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n",
|
||||
" 'value': ' about'})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/streamed_output/-',\n",
|
||||
" 'value': AIMessageChunk(content=' Harrison')})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n",
|
||||
" 'value': ' Harrison'})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/streamed_output/-',\n",
|
||||
" 'value': AIMessageChunk(content=' is')})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n",
|
||||
" 'value': ' is'})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/streamed_output/-',\n",
|
||||
" 'value': AIMessageChunk(content=' that')})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n",
|
||||
" 'value': ' that'})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/streamed_output/-',\n",
|
||||
" 'value': AIMessageChunk(content=' he')})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n",
|
||||
" 'value': ' he'})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/streamed_output/-',\n",
|
||||
" 'value': AIMessageChunk(content=' worked')})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n",
|
||||
" 'value': ' worked'})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/streamed_output/-',\n",
|
||||
" 'value': AIMessageChunk(content=' at')})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n",
|
||||
" 'value': ' at'})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/streamed_output/-',\n",
|
||||
" 'value': AIMessageChunk(content=' Kens')})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n",
|
||||
" 'value': ' Kens'})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/streamed_output/-',\n",
|
||||
" 'value': AIMessageChunk(content='ho')})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n",
|
||||
" 'value': 'ho'})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/streamed_output/-',\n",
|
||||
" 'value': AIMessageChunk(content='.')})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/ChatOpenAI:2/streamed_output_str/-',\n",
|
||||
" 'value': '.'})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/streamed_output/-',\n",
|
||||
" 'value': AIMessageChunk(content='')})\n",
|
||||
"RunLogPatch({'op': 'add', 'path': '/logs/ChatOpenAI:2/streamed_output_str/-', 'value': ''})\n",
|
||||
"RunLogPatch({'op': 'add',\n",
|
||||
" 'path': '/logs/ChatOpenAI:2/final_output',\n",
|
||||
" 'value': LLMResult(generations=[[ChatGenerationChunk(text='Based on the given context, the only information we have about Harrison is that he worked at Kensho.', generation_info={'finish_reason': 'stop'}, message=AIMessageChunk(content='Based on the given context, the only information we have about Harrison is that he worked at Kensho.'))]], llm_output=None, run=None)},\n",
|
||||
" {'op': 'add',\n",
|
||||
" 'path': '/logs/ChatOpenAI:2/end_time',\n",
|
||||
" 'value': '2023-11-16T15:59:26.547'})\n",
|
||||
"RunLogPatch({'op': 'replace',\n",
|
||||
" 'path': '/final_output',\n",
|
||||
" 'value': {'output': AIMessageChunk(content='Based on the given context, the only information we have about Harrison is that he worked at Kensho.')}})\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"async for chunk in remote_runnable.astream_log(\n",
|
||||
" {\"question\": \"what do you know about harrison\", \"chat_history\": [(\"hi\", \"hi\")]}\n",
|
||||
"):\n",
|
||||
" print(chunk)"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
|
||||
@@ -1,24 +1,84 @@
|
||||
#!/usr/bin/env python
|
||||
"""Example LangChain server exposes a conversational retrieval chain."""
|
||||
"""Example LangChain server exposes a conversational retrieval chain.
|
||||
|
||||
Follow the reference here:
|
||||
|
||||
https://python.langchain.com/docs/expression_language/cookbook/retrieval#conversational-retrieval-chain
|
||||
|
||||
To run this example, you will need to install the following packages:
|
||||
pip install langchain openai faiss-cpu tiktoken
|
||||
""" # noqa: F401
|
||||
|
||||
from operator import itemgetter
|
||||
from typing import List, Tuple
|
||||
|
||||
from fastapi import FastAPI
|
||||
from langchain.chains import ConversationalRetrievalChain
|
||||
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 pydantic import BaseModel, Field
|
||||
|
||||
from langserve import add_routes
|
||||
from langserve.pydantic_v1 import BaseModel, Field
|
||||
|
||||
_TEMPLATE = """Given the following conversation and a follow up question, rephrase the
|
||||
follow up question to be a standalone question, in its original language.
|
||||
|
||||
Chat History:
|
||||
{chat_history}
|
||||
Follow Up Input: {question}
|
||||
Standalone question:"""
|
||||
CONDENSE_QUESTION_PROMPT = PromptTemplate.from_template(_TEMPLATE)
|
||||
|
||||
ANSWER_TEMPLATE = """Answer the question based only on the following context:
|
||||
{context}
|
||||
|
||||
Question: {question}
|
||||
"""
|
||||
ANSWER_PROMPT = ChatPromptTemplate.from_template(ANSWER_TEMPLATE)
|
||||
|
||||
DEFAULT_DOCUMENT_PROMPT = PromptTemplate.from_template(template="{page_content}")
|
||||
|
||||
|
||||
def _combine_documents(
|
||||
docs, document_prompt=DEFAULT_DOCUMENT_PROMPT, document_separator="\n\n"
|
||||
):
|
||||
"""Combine documents into a single string."""
|
||||
doc_strings = [format_document(doc, document_prompt) for doc in docs]
|
||||
return document_separator.join(doc_strings)
|
||||
|
||||
|
||||
def _format_chat_history(chat_history: List[Tuple]) -> str:
|
||||
"""Format chat history into a string."""
|
||||
buffer = ""
|
||||
for dialogue_turn in chat_history:
|
||||
human = "Human: " + dialogue_turn[0]
|
||||
ai = "Assistant: " + dialogue_turn[1]
|
||||
buffer += "\n" + "\n".join([human, ai])
|
||||
return buffer
|
||||
|
||||
|
||||
vectorstore = FAISS.from_texts(
|
||||
["cats like fish", "dogs like sticks"], embedding=OpenAIEmbeddings()
|
||||
["harrison worked at kensho"], embedding=OpenAIEmbeddings()
|
||||
)
|
||||
retriever = vectorstore.as_retriever()
|
||||
|
||||
model = ChatOpenAI()
|
||||
|
||||
chain = ConversationalRetrievalChain.from_llm(model, retriever)
|
||||
_inputs = RunnableMap(
|
||||
standalone_question=RunnablePassthrough.assign(
|
||||
chat_history=lambda x: _format_chat_history(x["chat_history"])
|
||||
)
|
||||
| CONDENSE_QUESTION_PROMPT
|
||||
| ChatOpenAI(temperature=0)
|
||||
| StrOutputParser(),
|
||||
)
|
||||
_context = {
|
||||
"context": itemgetter("standalone_question") | retriever | _combine_documents,
|
||||
"question": lambda x: x["standalone_question"],
|
||||
}
|
||||
|
||||
|
||||
# User input
|
||||
@@ -32,9 +92,10 @@ class ChatHistory(BaseModel):
|
||||
question: str
|
||||
|
||||
|
||||
chain = ConversationalRetrievalChain.from_llm(model, retriever).with_types(
|
||||
input_type=ChatHistory
|
||||
conversational_qa_chain = (
|
||||
_inputs | _context | ANSWER_PROMPT | ChatOpenAI() | StrOutputParser()
|
||||
)
|
||||
chain = conversational_qa_chain.with_types(input_type=ChatHistory)
|
||||
|
||||
app = FastAPI(
|
||||
title="LangChain Server",
|
||||
@@ -45,7 +106,7 @@ app = FastAPI(
|
||||
# /invoke
|
||||
# /batch
|
||||
# /stream
|
||||
add_routes(app, chain)
|
||||
add_routes(app, chain, enable_feedback_endpoint=True)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
@@ -17,8 +17,8 @@ import base64
|
||||
from fastapi import FastAPI
|
||||
from langchain.document_loaders.blob_loaders import Blob
|
||||
from langchain.document_loaders.parsers.pdf import PDFMinerParser
|
||||
from langchain.pydantic_v1 import Field
|
||||
from langchain.schema.runnable import RunnableLambda
|
||||
from pydantic import Field
|
||||
|
||||
from langserve import CustomUserType, add_routes
|
||||
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Passthrough information\n",
|
||||
"\n",
|
||||
"An example that shows how to pass through additional info with the request, and get it back with the response."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain.prompts.chat import ChatPromptTemplate"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 9,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langserve import RemoteRunnable\n",
|
||||
"\n",
|
||||
"chain = RemoteRunnable(\"http://localhost:8000/v1/\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Let's create a prompt composed of a system message and a human message."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 10,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'output': AIMessage(content='`apple` translates to `mela` in Italian.'),\n",
|
||||
" 'info': {'info': {'user_id': 42, 'user_info': {'address': 42}}}}"
|
||||
]
|
||||
},
|
||||
"execution_count": 10,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"chain.invoke({'thing': 'apple', 'language': 'italian', 'info': {\"user_id\": 42, \"user_info\": {\"address\": 42}}})"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 11,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"{'info': {'info': {'user_id': 42, 'user_info': {'address': 42}}}}\n",
|
||||
"{'output': AIMessageChunk(content='')}\n",
|
||||
"{'output': AIMessageChunk(content='m')}\n",
|
||||
"{'output': AIMessageChunk(content='ela')}\n",
|
||||
"{'output': AIMessageChunk(content='')}\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"for chunk in chain.stream({'thing': 'apple', 'language': 'italian', 'info': {\"user_id\": 42, \"user_info\": {\"address\": 42}}}):\n",
|
||||
" print(chunk)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 12,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langserve import RemoteRunnable\n",
|
||||
"\n",
|
||||
"chain = RemoteRunnable(\"http://localhost:8000/v2/\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 13,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'output': AIMessage(content='`apple` translates to `mela` in Italian.'),\n",
|
||||
" 'info': {'user_id': 42, 'user_info': {'address': 42}}}"
|
||||
]
|
||||
},
|
||||
"execution_count": 13,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"chain.invoke({'thing': 'apple', 'language': 'italian', 'info': {\"user_id\": 42, \"user_info\": {\"address\": 42}}})"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.9.6"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 4
|
||||
}
|
||||
Executable
+86
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env python
|
||||
"""Example LangChain server passes through some of the inputs in the response."""
|
||||
|
||||
from typing import Any, Callable, Dict, List, Optional, TypedDict
|
||||
|
||||
from fastapi import FastAPI
|
||||
from langchain.chat_models import ChatOpenAI
|
||||
from langchain.prompts import ChatPromptTemplate
|
||||
from langchain.schema.runnable import RunnableMap, RunnablePassthrough
|
||||
|
||||
from langserve import add_routes
|
||||
|
||||
app = FastAPI(
|
||||
title="LangChain Server",
|
||||
version="1.0",
|
||||
description="Spin up a simple api server using Langchain's Runnable interfaces",
|
||||
)
|
||||
|
||||
|
||||
def _create_projection(
|
||||
*, include_keys: Optional[List] = None, exclude_keys: Optional[List[str]] = None
|
||||
) -> Callable[[dict], dict]:
|
||||
"""Create a projection function."""
|
||||
|
||||
def _project_dict(
|
||||
d: dict,
|
||||
) -> dict:
|
||||
"""Project dictionary."""
|
||||
keys = d.keys()
|
||||
if include_keys is not None:
|
||||
keys = set(keys) & set(include_keys)
|
||||
if exclude_keys is not None:
|
||||
keys = set(keys) - set(exclude_keys)
|
||||
return {k: d[k] for k in keys}
|
||||
|
||||
return _project_dict
|
||||
|
||||
|
||||
prompt = ChatPromptTemplate.from_messages(
|
||||
[("human", "translate `{thing}` to {language}")]
|
||||
)
|
||||
model = ChatOpenAI()
|
||||
|
||||
underlying_chain = prompt | model
|
||||
|
||||
wrapped_chain = RunnableMap(
|
||||
{
|
||||
"output": _create_projection(exclude_keys=["info"]) | underlying_chain,
|
||||
"info": _create_projection(include_keys=["info"]),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class Input(TypedDict):
|
||||
thing: str
|
||||
language: str
|
||||
info: Dict[str, Any]
|
||||
|
||||
|
||||
class Output(TypedDict):
|
||||
output: underlying_chain.output_schema
|
||||
info: Dict[str, Any]
|
||||
|
||||
|
||||
add_routes(
|
||||
app, wrapped_chain.with_types(input_type=Input, output_type=Output), path="/v1"
|
||||
)
|
||||
|
||||
|
||||
# Version 2
|
||||
# Uses RunnablePassthrough.assign
|
||||
wrapped_chain_2 = RunnablePassthrough.assign(output=underlying_chain) | {
|
||||
"output": lambda x: x["output"],
|
||||
"info": lambda x: x["info"],
|
||||
}
|
||||
|
||||
add_routes(
|
||||
app,
|
||||
wrapped_chain_2.with_types(input_type=Input, output_type=Output),
|
||||
path="/v2",
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(app, host="localhost", port=8000)
|
||||
+54
-31
@@ -6,16 +6,20 @@ from typing import Any, Dict, List, Tuple
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from langchain.chat_models.openai import ChatOpenAI
|
||||
from langchain.document_loaders.blob_loaders import Blob
|
||||
from langchain.document_loaders.parsers.pdf import PDFMinerParser
|
||||
from langchain.pydantic_v1 import BaseModel, Field
|
||||
from langchain.schema.messages import (
|
||||
AIMessage,
|
||||
BaseMessage,
|
||||
FunctionMessage,
|
||||
HumanMessage,
|
||||
)
|
||||
from langchain.schema.runnable import RunnableLambda
|
||||
from pydantic import BaseModel, Field
|
||||
from langchain_core.runnables import RunnableParallel
|
||||
|
||||
from langserve import CustomUserType
|
||||
from langserve.server import add_routes
|
||||
|
||||
app = FastAPI(
|
||||
@@ -35,8 +39,11 @@ app.add_middleware(
|
||||
expose_headers=["*"],
|
||||
)
|
||||
|
||||
# Example 1: Chat Widget
|
||||
# This shows how to create a chat widget.
|
||||
|
||||
class ChatHistory(BaseModel):
|
||||
|
||||
class ChatHistory(CustomUserType):
|
||||
chat_history: List[Tuple[str, str]] = Field(
|
||||
...,
|
||||
examples=[[("a", "aa")]],
|
||||
@@ -45,27 +52,43 @@ class ChatHistory(BaseModel):
|
||||
question: str
|
||||
|
||||
|
||||
def _format_to_messages(input: ChatHistory) -> List[BaseMessage]:
|
||||
"""Format the input to a list of messages."""
|
||||
history = input.chat_history
|
||||
user_input = input.question
|
||||
|
||||
messages = []
|
||||
|
||||
for human, ai in history:
|
||||
messages.append(HumanMessage(content=human))
|
||||
messages.append(AIMessage(content=ai))
|
||||
messages.append(HumanMessage(content=user_input))
|
||||
return messages
|
||||
|
||||
|
||||
model = ChatOpenAI()
|
||||
chat_model = RunnableParallel({"answer": (RunnableLambda(_format_to_messages) | model)})
|
||||
add_routes(
|
||||
app,
|
||||
chat_model.with_types(input_type=ChatHistory),
|
||||
config_keys=["configurable"],
|
||||
path="/chat",
|
||||
)
|
||||
|
||||
|
||||
# Example 2: Chat Widget with History
|
||||
# This one isn't hooked up toa model. It just shows that FunctionMessages can be used
|
||||
# surfaced as well in the playground.
|
||||
|
||||
|
||||
class ChatHistoryMessage(BaseModel):
|
||||
chat_history: List[BaseMessage] = Field(
|
||||
...,
|
||||
extra={"widget": {"type": "chat", "input": "location", "output": "output"}},
|
||||
extra={"widget": {"type": "chat", "input": "location"}},
|
||||
)
|
||||
location: str
|
||||
|
||||
|
||||
class FileProcessingRequest(BaseModel):
|
||||
file: bytes = Field(..., extra={"widget": {"type": "base64file"}})
|
||||
num_chars: int = 100
|
||||
|
||||
|
||||
def chat_with_bot(input: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Bot that repeats the question twice."""
|
||||
return {
|
||||
"answer": input["question"] * 2,
|
||||
"woof": "its so bad to woof, meow is better",
|
||||
}
|
||||
|
||||
|
||||
def chat_message_bot(input: Dict[str, Any]) -> List[BaseMessage]:
|
||||
"""Bot that repeats the question twice."""
|
||||
return [
|
||||
@@ -83,6 +106,21 @@ def chat_message_bot(input: Dict[str, Any]) -> List[BaseMessage]:
|
||||
]
|
||||
|
||||
|
||||
add_routes(
|
||||
app,
|
||||
RunnableLambda(chat_message_bot).with_types(input_type=ChatHistoryMessage),
|
||||
config_keys=["configurable"],
|
||||
path="/chat_message",
|
||||
)
|
||||
|
||||
# Example 3: File Processing Widget
|
||||
|
||||
|
||||
class FileProcessingRequest(BaseModel):
|
||||
file: bytes = Field(..., extra={"widget": {"type": "base64file"}})
|
||||
num_chars: int = 100
|
||||
|
||||
|
||||
def process_file(input: Dict[str, Any]) -> str:
|
||||
"""Extract the text from the first page of the PDF."""
|
||||
content = base64.decodebytes(input["file"])
|
||||
@@ -92,13 +130,6 @@ def process_file(input: Dict[str, Any]) -> str:
|
||||
return content[: input["num_chars"]]
|
||||
|
||||
|
||||
add_routes(
|
||||
app,
|
||||
RunnableLambda(chat_with_bot).with_types(input_type=ChatHistory),
|
||||
config_keys=["configurable"],
|
||||
path="/chat",
|
||||
)
|
||||
|
||||
add_routes(
|
||||
app,
|
||||
RunnableLambda(process_file).with_types(input_type=FileProcessingRequest),
|
||||
@@ -106,14 +137,6 @@ add_routes(
|
||||
path="/pdf",
|
||||
)
|
||||
|
||||
add_routes(
|
||||
app,
|
||||
RunnableLambda(chat_message_bot).with_types(input_type=ChatHistoryMessage),
|
||||
config_keys=["configurable"],
|
||||
path="/chat_message",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
|
||||
@@ -4,9 +4,16 @@ This is the ONLY public interface into the package. All other modules are
|
||||
to be considered private and subject to change without notice.
|
||||
"""
|
||||
|
||||
from langserve.api_handler import APIHandler
|
||||
from langserve.client import RemoteRunnable
|
||||
from langserve.schema import CustomUserType
|
||||
from langserve.server import add_routes
|
||||
from langserve.version import __version__
|
||||
|
||||
__all__ = ["RemoteRunnable", "add_routes", "__version__", "CustomUserType"]
|
||||
__all__ = [
|
||||
"RemoteRunnable",
|
||||
"APIHandler",
|
||||
"add_routes",
|
||||
"__version__",
|
||||
"CustomUserType",
|
||||
]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+17
-11
@@ -235,7 +235,8 @@ class RemoteRunnable(Runnable[Input, Output]):
|
||||
callback events returned by the server.
|
||||
"""
|
||||
_client_kwargs = client_kwargs or {}
|
||||
self.url = url
|
||||
# Enforce trailing slash
|
||||
self.url = url if url.endswith("/") else url + "/"
|
||||
self.sync_client = httpx.Client(
|
||||
base_url=url,
|
||||
timeout=timeout,
|
||||
@@ -365,11 +366,6 @@ class RemoteRunnable(Runnable[Input, Output]):
|
||||
|
||||
return outputs
|
||||
|
||||
def _enforce_trailing_slash(self, url: str) -> str:
|
||||
if url.endswith("/"):
|
||||
return url
|
||||
return url + "/"
|
||||
|
||||
def batch(
|
||||
self,
|
||||
inputs: List[Input],
|
||||
@@ -465,7 +461,7 @@ class RemoteRunnable(Runnable[Input, Output]):
|
||||
"config": _without_callbacks(config),
|
||||
"kwargs": kwargs,
|
||||
}
|
||||
endpoint = urljoin(self._enforce_trailing_slash(self.url), "stream")
|
||||
endpoint = urljoin(self.url, "stream")
|
||||
|
||||
try:
|
||||
from httpx_sse import connect_sse
|
||||
@@ -512,10 +508,13 @@ class RemoteRunnable(Runnable[Input, Output]):
|
||||
_raise_exception_from_data(
|
||||
sse.data, httpx.Request(method="POST", url=endpoint)
|
||||
)
|
||||
elif sse.event == "metadata":
|
||||
# Nothing to do for metadata for the regular remote client.
|
||||
continue
|
||||
elif sse.event == "end":
|
||||
break
|
||||
else:
|
||||
logger.error(
|
||||
_log_error_message_once(
|
||||
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}`."
|
||||
@@ -548,7 +547,7 @@ class RemoteRunnable(Runnable[Input, Output]):
|
||||
"config": _without_callbacks(config),
|
||||
"kwargs": kwargs,
|
||||
}
|
||||
endpoint = urljoin(self._enforce_trailing_slash(self.url), "stream")
|
||||
endpoint = urljoin(self.url, "stream")
|
||||
|
||||
try:
|
||||
from httpx_sse import aconnect_sse
|
||||
@@ -593,10 +592,13 @@ class RemoteRunnable(Runnable[Input, Output]):
|
||||
_raise_exception_from_data(
|
||||
sse.data, httpx.Request(method="POST", url=endpoint)
|
||||
)
|
||||
elif sse.event == "metadata":
|
||||
# Nothing to do for metadata for the regular remote client.
|
||||
continue
|
||||
elif sse.event == "end":
|
||||
break
|
||||
else:
|
||||
logger.error(
|
||||
_log_error_message_once(
|
||||
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}`."
|
||||
@@ -668,9 +670,13 @@ class RemoteRunnable(Runnable[Input, Output]):
|
||||
async for sse in event_source.aiter_sse():
|
||||
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.
|
||||
chunk_to_yield = RunLogPatch(*copy.deepcopy(data["ops"]))
|
||||
chunk = RunLogPatch(*data["ops"])
|
||||
|
||||
yield chunk
|
||||
yield chunk_to_yield
|
||||
if final_output:
|
||||
final_output += chunk
|
||||
else:
|
||||
|
||||
+39
-5
@@ -7,22 +7,53 @@ from typing import Sequence, Type
|
||||
from fastapi.responses import Response
|
||||
from langchain.schema.runnable import Runnable
|
||||
|
||||
try:
|
||||
from pydantic.v1 import BaseModel
|
||||
except ImportError:
|
||||
from pydantic import BaseModel
|
||||
from langserve.pydantic_v1 import BaseModel
|
||||
|
||||
|
||||
class PlaygroundTemplate(Template):
|
||||
delimiter = "____"
|
||||
|
||||
|
||||
def _get_mimetype(path: str) -> str:
|
||||
"""Get mimetype for file.
|
||||
|
||||
Custom implementation of mimetypes.guess_type that
|
||||
uses the file extension to determine the mimetype for some files.
|
||||
|
||||
This is necessary due to: https://bugs.python.org/issue43975
|
||||
Resolves issue: https://github.com/langchain-ai/langserve/issues/245
|
||||
|
||||
Args:
|
||||
path (str): Path to file
|
||||
|
||||
Returns:
|
||||
str: Mimetype of file
|
||||
"""
|
||||
try:
|
||||
file_extension = path.lower().split(".")[-1]
|
||||
except IndexError:
|
||||
return mimetypes.guess_type(path)[0]
|
||||
|
||||
if file_extension == "js":
|
||||
return "application/javascript"
|
||||
elif file_extension == "css":
|
||||
return "text/css"
|
||||
elif file_extension in ["htm", "html"]:
|
||||
return "text/html"
|
||||
|
||||
# If the file extension is not one of the specified ones,
|
||||
# use the default guess method
|
||||
mime_type = mimetypes.guess_type(path)[0]
|
||||
return mime_type
|
||||
|
||||
|
||||
async def serve_playground(
|
||||
runnable: Runnable,
|
||||
input_schema: Type[BaseModel],
|
||||
config_keys: Sequence[str],
|
||||
base_url: str,
|
||||
file_path: str,
|
||||
feedback_enabled: bool,
|
||||
) -> Response:
|
||||
"""Serve the playground."""
|
||||
local_file_path = os.path.abspath(
|
||||
@@ -42,7 +73,7 @@ async def serve_playground(
|
||||
|
||||
try:
|
||||
with open(local_file_path, encoding="utf-8") as f:
|
||||
mime_type = mimetypes.guess_type(local_file_path)[0]
|
||||
mime_type = _get_mimetype(local_file_path)
|
||||
if mime_type in ("text/html", "text/css", "application/javascript"):
|
||||
response = PlaygroundTemplate(f.read()).substitute(
|
||||
LANGSERVE_BASE_URL=base_url[1:]
|
||||
@@ -52,6 +83,9 @@ async def serve_playground(
|
||||
runnable.config_schema(include=config_keys).schema()
|
||||
),
|
||||
LANGSERVE_INPUT_SCHEMA=json.dumps(input_schema.schema()),
|
||||
LANGSERVE_FEEDBACK_ENABLED=json.dumps(
|
||||
"true" if feedback_enabled else "false"
|
||||
),
|
||||
)
|
||||
else:
|
||||
response = f.buffer.read()
|
||||
|
||||
File diff suppressed because one or more lines are too long
+255
File diff suppressed because one or more lines are too long
-247
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Vendored
+3
-2
@@ -5,8 +5,8 @@
|
||||
<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-7c3e1e1d.js"></script>
|
||||
<link rel="stylesheet" href="/____LANGSERVE_BASE_URL/assets/index-c6cde0dd.css">
|
||||
<script type="module" crossorigin src="/____LANGSERVE_BASE_URL/assets/index-6a0f524c.js"></script>
|
||||
<link rel="stylesheet" href="/____LANGSERVE_BASE_URL/assets/index-52e8ab2f.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
@@ -14,6 +14,7 @@
|
||||
try {
|
||||
window.CONFIG_SCHEMA = ____LANGSERVE_CONFIG_SCHEMA;
|
||||
window.INPUT_SCHEMA = ____LANGSERVE_INPUT_SCHEMA;
|
||||
window.FEEDBACK_ENABLED = ____LANGSERVE_FEEDBACK_ENABLED;
|
||||
} catch (error) {
|
||||
// pass
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
try {
|
||||
window.CONFIG_SCHEMA = ____LANGSERVE_CONFIG_SCHEMA;
|
||||
window.INPUT_SCHEMA = ____LANGSERVE_INPUT_SCHEMA;
|
||||
window.FEEDBACK_ENABLED = ____LANGSERVE_FEEDBACK_ENABLED;
|
||||
} catch (error) {
|
||||
// pass
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"lz-string": "^1.5.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"swr": "^2.2.4",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
"use-debounce": "^9.0.4",
|
||||
"vaul": "^0.7.3"
|
||||
|
||||
+178
-426
@@ -1,258 +1,55 @@
|
||||
import "./App.css";
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import defaults from "./utils/defaults";
|
||||
import { JsonForms } from "@jsonforms/react";
|
||||
import {
|
||||
materialAllOfControlTester,
|
||||
MaterialAllOfRenderer,
|
||||
MaterialObjectRenderer,
|
||||
materialOneOfControlTester,
|
||||
MaterialOneOfRenderer,
|
||||
} from "@jsonforms/material-renderers";
|
||||
import dayjs from "dayjs";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import relativeDate from "dayjs/plugin/relativeTime";
|
||||
import SendIcon from "./assets/SendIcon.svg?react";
|
||||
import ShareIcon from "./assets/ShareIcon.svg?react";
|
||||
import { compressToEncodedURIComponent } from "lz-string";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import {
|
||||
BooleanCell,
|
||||
DateCell,
|
||||
DateTimeCell,
|
||||
EnumCell,
|
||||
IntegerCell,
|
||||
NumberCell,
|
||||
SliderCell,
|
||||
TimeCell,
|
||||
booleanCellTester,
|
||||
dateCellTester,
|
||||
dateTimeCellTester,
|
||||
enumCellTester,
|
||||
integerCellTester,
|
||||
numberCellTester,
|
||||
sliderCellTester,
|
||||
textAreaCellTester,
|
||||
textCellTester,
|
||||
timeCellTester,
|
||||
vanillaRenderers,
|
||||
InputControl,
|
||||
} from "@jsonforms/vanilla-renderers";
|
||||
import { useSchemas } from "./useSchemas";
|
||||
import ShareIcon from "./assets/ShareIcon.svg?react";
|
||||
|
||||
import { useConfigSchema, useFeedback, useInputSchema } from "./useSchemas";
|
||||
import { useStreamLog } from "./useStreamLog";
|
||||
import { StreamCallback } from "./types";
|
||||
import { AppCallbackContext } from "./useStreamCallback";
|
||||
import {
|
||||
JsonFormsCore,
|
||||
RankedTester,
|
||||
rankWith,
|
||||
and,
|
||||
uiTypeIs,
|
||||
schemaMatches,
|
||||
schemaTypeIs,
|
||||
} from "@jsonforms/core";
|
||||
import CustomArrayControlRenderer, {
|
||||
materialArrayControlTester,
|
||||
} from "./components/CustomArrayControlRenderer";
|
||||
import CustomTextAreaCell from "./components/CustomTextAreaCell";
|
||||
import JsonTextAreaCell from "./components/JsonTextAreaCell";
|
||||
import { getStateFromUrl, ShareDialog } from "./components/ShareDialog";
|
||||
import {
|
||||
chatMessagesTester,
|
||||
ChatMessagesControlRenderer,
|
||||
} from "./components/ChatMessagesControlRenderer";
|
||||
import {
|
||||
ChatMessageTuplesControlRenderer,
|
||||
chatMessagesTupleTester,
|
||||
} from "./components/ChatMessageTuplesControlRenderer";
|
||||
import {
|
||||
fileBase64Tester,
|
||||
FileBase64ControlRenderer,
|
||||
} from "./components/FileBase64Tester";
|
||||
import { AppCallbackContext, useAppStreamCallbacks } from "./useStreamCallback";
|
||||
import { JsonSchema } from "@jsonforms/core";
|
||||
import { ShareDialog } from "./components/ShareDialog";
|
||||
import { IntermediateSteps } from "./components/IntermediateSteps";
|
||||
import { StreamOutput } from "./components/StreamOutput";
|
||||
import {
|
||||
customAnyOfTester,
|
||||
CustomAnyOfRenderer,
|
||||
} from "./components/CustomAnyOfRenderer";
|
||||
import { ConfigValue, SectionConfigure } from "./sections/SectionConfigure";
|
||||
import { InputValue, SectionInputs } from "./sections/SectionInputs";
|
||||
import { SubmitButton } from "./components/SubmitButton";
|
||||
import { useDebounce } from "use-debounce";
|
||||
import { cn } from "./utils/cn";
|
||||
import { CorrectnessFeedback } from "./components/feedback/CorrectnessFeedback";
|
||||
import { getStateFromUrl } from "./utils/url";
|
||||
|
||||
dayjs.extend(relativeDate);
|
||||
dayjs.extend(utc);
|
||||
function InputPlayground(props: {
|
||||
configSchema: { schema: JsonSchema; defaults: unknown };
|
||||
inputSchema: { schema: JsonSchema; defaults: unknown };
|
||||
|
||||
const isObjectWithPropertiesControl = rankWith(
|
||||
2,
|
||||
and(
|
||||
uiTypeIs("Control"),
|
||||
schemaTypeIs("object"),
|
||||
schemaMatches((schema) =>
|
||||
Object.prototype.hasOwnProperty.call(schema, "properties")
|
||||
)
|
||||
)
|
||||
);
|
||||
configData: ConfigValue;
|
||||
|
||||
const isObject = rankWith(1, and(uiTypeIs("Control"), schemaTypeIs("object")));
|
||||
const isElse = rankWith(1, and(uiTypeIs("Control")));
|
||||
startStream: (input: unknown, config: unknown) => void;
|
||||
stopStream: (() => void) | undefined;
|
||||
|
||||
export const renderers = [
|
||||
...vanillaRenderers,
|
||||
|
||||
// use material renderers to handle objects and json schema references
|
||||
// they should yield the rendering to simpler cells
|
||||
{ tester: isObjectWithPropertiesControl, renderer: MaterialObjectRenderer },
|
||||
{ tester: materialAllOfControlTester, renderer: MaterialAllOfRenderer },
|
||||
{ tester: materialOneOfControlTester, renderer: MaterialOneOfRenderer },
|
||||
|
||||
{ tester: customAnyOfTester, renderer: CustomAnyOfRenderer },
|
||||
|
||||
// custom renderers
|
||||
{ tester: materialArrayControlTester, renderer: CustomArrayControlRenderer },
|
||||
{ tester: isObject, renderer: InputControl },
|
||||
{ tester: chatMessagesTester, renderer: ChatMessagesControlRenderer },
|
||||
{
|
||||
tester: chatMessagesTupleTester,
|
||||
renderer: ChatMessageTuplesControlRenderer,
|
||||
},
|
||||
{ tester: fileBase64Tester, renderer: FileBase64ControlRenderer },
|
||||
];
|
||||
|
||||
const nestedArrayControlTester: RankedTester = rankWith(1, (_, jsonSchema) => {
|
||||
return jsonSchema.type === "array";
|
||||
});
|
||||
|
||||
export const cells = [
|
||||
{ tester: booleanCellTester, cell: BooleanCell },
|
||||
{ tester: dateCellTester, cell: DateCell },
|
||||
{ tester: dateTimeCellTester, cell: DateTimeCell },
|
||||
{ tester: enumCellTester, cell: EnumCell },
|
||||
{ tester: integerCellTester, cell: IntegerCell },
|
||||
{ tester: numberCellTester, cell: NumberCell },
|
||||
{ tester: sliderCellTester, cell: SliderCell },
|
||||
{ tester: textAreaCellTester, cell: CustomTextAreaCell },
|
||||
{ tester: textCellTester, cell: CustomTextAreaCell },
|
||||
{ tester: timeCellTester, cell: TimeCell },
|
||||
{ tester: nestedArrayControlTester, cell: CustomArrayControlRenderer },
|
||||
{ tester: isElse, cell: JsonTextAreaCell },
|
||||
];
|
||||
|
||||
function App() {
|
||||
const [isIframe] = useState(() => window.self !== window.top);
|
||||
|
||||
// it is possible that defaults are being applied _after_
|
||||
// the initial update message has been sent from the parent window
|
||||
// so we store the initial config data in a ref
|
||||
const initConfigData = useRef<JsonFormsCore["data"]>(null);
|
||||
|
||||
// store form state
|
||||
const [configData, setConfigData] = useState<
|
||||
Pick<JsonFormsCore, "data" | "errors"> & { defaults: boolean }
|
||||
>({ data: {}, errors: [], defaults: true });
|
||||
|
||||
const [inputData, setInputData] = useState<
|
||||
Pick<JsonFormsCore, "data" | "errors">
|
||||
>({ data: null, errors: [] });
|
||||
// fetch input and config schemas from the server
|
||||
const schemas = useSchemas(configData);
|
||||
// apply defaults defined in each schema
|
||||
useEffect(() => {
|
||||
if (schemas.config) {
|
||||
const state = getStateFromUrl(window.location.href);
|
||||
setConfigData({
|
||||
data:
|
||||
state.configFromUrl ??
|
||||
initConfigData.current ??
|
||||
defaults(schemas.config),
|
||||
errors: [],
|
||||
defaults: true,
|
||||
});
|
||||
|
||||
setInputData({ data: defaults(schemas.input), errors: [] });
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [schemas.config]);
|
||||
|
||||
// callbacks handling
|
||||
const callbacks = useRef<{
|
||||
onStart: Exclude<StreamCallback["onStart"], undefined>[];
|
||||
onSuccess: Exclude<StreamCallback["onSuccess"], undefined>[];
|
||||
onError: Exclude<StreamCallback["onError"], undefined>[];
|
||||
}>({ onStart: [], onSuccess: [], onError: [] });
|
||||
|
||||
// the runner
|
||||
const { startStream, stopStream, latest } = useStreamLog({
|
||||
onStart(...args) {
|
||||
for (const callback of callbacks.current.onStart) {
|
||||
callback(...args);
|
||||
}
|
||||
},
|
||||
onSuccess(...args) {
|
||||
for (const callback of callbacks.current.onSuccess) {
|
||||
callback(...args);
|
||||
}
|
||||
},
|
||||
onError(...args) {
|
||||
for (const callback of callbacks.current.onError) {
|
||||
callback(...args);
|
||||
}
|
||||
},
|
||||
children?: React.ReactNode;
|
||||
}) {
|
||||
const [inputData, setInputData] = useState<InputValue>({
|
||||
data: props.inputSchema.defaults,
|
||||
errors: [],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
window.parent?.postMessage({ type: "init" }, "*");
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
function listener(event: MessageEvent) {
|
||||
if (event.source === window.parent) {
|
||||
const message = event.data;
|
||||
if (typeof message === "object" && message != null) {
|
||||
switch (message.type) {
|
||||
case "update": {
|
||||
const value: { config: JsonFormsCore["data"] } = message.value;
|
||||
if (Object.keys(value.config).length > 0) {
|
||||
initConfigData.current = value.config;
|
||||
setConfigData({
|
||||
data: value.config,
|
||||
errors: [],
|
||||
defaults: false,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("message", listener);
|
||||
return () => window.removeEventListener("message", listener);
|
||||
}, []);
|
||||
|
||||
const isInputResetable = useMemo(() => {
|
||||
if (!schemas.input) return false;
|
||||
return (
|
||||
JSON.stringify(defaults(schemas.input)) !== JSON.stringify(inputData.data)
|
||||
);
|
||||
}, [schemas.input, inputData.data]);
|
||||
|
||||
function onSubmit() {
|
||||
const submitRef = useRef<(() => void) | null>(null);
|
||||
submitRef.current = () => {
|
||||
if (
|
||||
!stopStream &&
|
||||
(!!inputData.errors?.length || !!configData.errors?.length)
|
||||
!props.stopStream &&
|
||||
(!!inputData.errors?.length || !!props.configData.errors?.length)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (stopStream) {
|
||||
stopStream();
|
||||
if (props.stopStream) {
|
||||
props.stopStream();
|
||||
} else {
|
||||
startStream(inputData.data, configData.data);
|
||||
props.startStream(inputData.data, props.configData.data);
|
||||
}
|
||||
}
|
||||
|
||||
const submitRef = useRef<(() => void) | null>(null);
|
||||
submitRef.current = onSubmit;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("keydown", (e) => {
|
||||
@@ -264,207 +61,162 @@ function App() {
|
||||
}, []);
|
||||
|
||||
const isSendDisabled =
|
||||
!stopStream && (!!inputData.errors?.length || !!configData.errors?.length);
|
||||
|
||||
if (!schemas.config || !schemas.input) {
|
||||
return <></>;
|
||||
}
|
||||
!props.stopStream &&
|
||||
(!!inputData.errors?.length || !!props.configData.errors?.length);
|
||||
|
||||
return (
|
||||
<AppCallbackContext.Provider value={callbacks}>
|
||||
<div className="flex items-center flex-col text-ls-black bg-gradient-to-b from-[#F9FAFB] to-[#EFF8FF] min-h-[100dvh] dark:from-[#0C111C] dark:to-[#0C111C]">
|
||||
<div className="flex flex-col flex-grow gap-4 px-4 pt-6 max-w-[800px] w-full">
|
||||
<h1 className="text-2xl text-left">
|
||||
<strong>🦜 LangServe</strong> Playground
|
||||
</h1>
|
||||
<>
|
||||
<SectionInputs
|
||||
input={props.inputSchema.schema}
|
||||
value={inputData}
|
||||
onChange={(input) => setInputData(input)}
|
||||
/>
|
||||
|
||||
{Object.keys(schemas.config).length > 0 && (
|
||||
<div className="flex flex-col gap-3 [&:has(.content>.vertical-layout:first-child:last-child:empty)]:hidden">
|
||||
{!isIframe && (
|
||||
<h2 className="text-xl font-semibold">Configure</h2>
|
||||
)}
|
||||
{props.children}
|
||||
|
||||
<div className="content flex flex-col gap-3">
|
||||
<JsonForms
|
||||
schema={schemas.config}
|
||||
data={configData.data}
|
||||
renderers={renderers}
|
||||
cells={cells}
|
||||
onChange={({ data, errors }) =>
|
||||
data
|
||||
? setConfigData({ data, errors, defaults: false })
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<div className="flex-grow md:hidden" />
|
||||
|
||||
{!!configData.errors?.length && configData.data && (
|
||||
<div className="bg-background rounded-xl">
|
||||
<div className="bg-red-500/10 text-red-700 dark:text-red-300 rounded-xl p-3">
|
||||
<strong className="font-bold">Validation Errors</strong>
|
||||
<ul className="list-disc pl-5">
|
||||
{configData.errors?.map((e, i) => (
|
||||
<li key={i}>{e.message}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="gap-4 grid grid-cols-2 sticky -mx-4 px-4 py-4 bottom-0 bg-background md:static md:bg-transparent">
|
||||
<div className="md:hidden absolute inset-x-0 bottom-full h-5 bg-gradient-to-t from-black/5 to-black/0" />
|
||||
|
||||
<ShareDialog config={props.configData.data}>
|
||||
<button
|
||||
type="button"
|
||||
className="px-4 py-3 gap-3 font-medium border border-divider-700 rounded-full flex items-center justify-center hover:bg-divider-500/50 active:bg-divider-500 transition-colors"
|
||||
>
|
||||
<ShareIcon className="flex-shrink-0" /> <span>Share</span>
|
||||
</button>
|
||||
</ShareDialog>
|
||||
|
||||
<SubmitButton
|
||||
disabled={isSendDisabled}
|
||||
onSubmit={submitRef.current}
|
||||
isLoading={!!props.stopStream}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ConfigPlayground(props: {
|
||||
configSchema: {
|
||||
schema: JsonSchema;
|
||||
defaults: unknown;
|
||||
};
|
||||
}) {
|
||||
const urlState = getStateFromUrl(window.location.href);
|
||||
const [configData, setConfigData] = useState<ConfigValue>({
|
||||
data: urlState.configFromUrl ?? props.configSchema.defaults,
|
||||
errors: [],
|
||||
defaults: true,
|
||||
});
|
||||
|
||||
const feedback = useFeedback();
|
||||
|
||||
// input schema is derived from config data
|
||||
const [debouncedConfigData, debounceState] = useDebounce(
|
||||
configData.data,
|
||||
500
|
||||
);
|
||||
|
||||
const inputSchema = useInputSchema(
|
||||
debouncedConfigData !== props.configSchema.defaults
|
||||
? debouncedConfigData
|
||||
: undefined
|
||||
);
|
||||
|
||||
const { context, callbacks } = useAppStreamCallbacks();
|
||||
const { startStream, stopStream, latest } = useStreamLog(callbacks);
|
||||
|
||||
return (
|
||||
<AppCallbackContext.Provider value={context}>
|
||||
<SectionConfigure
|
||||
config={props.configSchema.schema}
|
||||
value={configData}
|
||||
onChange={setConfigData}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col flex-grow gap-4 w-full transition-opacity",
|
||||
(inputSchema.isLoading || debounceState.isPending()) &&
|
||||
"opacity-50 pointer-events-none"
|
||||
)}
|
||||
>
|
||||
{inputSchema.error != null ? (
|
||||
<div className="bg-background rounded-xl">
|
||||
<div className="bg-red-500/10 text-red-700 dark:text-red-300 rounded-xl p-3">
|
||||
{inputSchema.error.toString()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isIframe && (
|
||||
<div className="flex flex-col gap-3">
|
||||
<h2 className="text-xl font-semibold">Try it</h2>
|
||||
|
||||
<div className="p-4 border border-divider-700 flex flex-col gap-3 rounded-2xl bg-background">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-medium">Inputs</h3>
|
||||
{isInputResetable && (
|
||||
<button
|
||||
type="button"
|
||||
className="text-sm px-1 -mr-1 py-0.5 rounded-md hover:bg-divider-500/50 active:bg-divider-500 text-ls-gray-100"
|
||||
onClick={() =>
|
||||
setInputData({
|
||||
data: defaults(schemas.input),
|
||||
errors: [],
|
||||
})
|
||||
}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<JsonForms
|
||||
schema={schemas.input}
|
||||
data={inputData.data}
|
||||
renderers={renderers}
|
||||
cells={cells}
|
||||
onChange={({ data, errors }) =>
|
||||
setInputData({ data, errors })
|
||||
}
|
||||
/>
|
||||
{!!inputData.errors?.length && inputData.data && (
|
||||
<div className="bg-red-500/10 text-red-700 dark:text-red-300 rounded-xl p-3">
|
||||
<strong className="font-bold">Validation Errors</strong>
|
||||
<ul className="list-disc pl-5">
|
||||
{inputData.errors?.map((e, i) => (
|
||||
<li key={i}>{e.message}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{latest && (
|
||||
<div className="flex flex-col gap-3">
|
||||
<h2 className="text-xl font-semibold">Output</h2>
|
||||
<div className="p-4 border border-divider-700 flex flex-col gap-3 rounded-2xl bg-background text-lg whitespace-pre-wrap break-words">
|
||||
<StreamOutput streamed={latest.streamed_output} />
|
||||
</div>
|
||||
<IntermediateSteps latest={latest} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex-grow md:hidden" />
|
||||
|
||||
<div className="gap-4 grid grid-cols-2 sticky -mx-4 px-4 py-4 bottom-0 bg-background md:static md:bg-transparent">
|
||||
<div className="md:hidden absolute inset-x-0 bottom-full h-5 bg-gradient-to-t from-black/5 to-black/0" />
|
||||
|
||||
{isIframe ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="px-4 py-3 gap-3 font-medium border border-divider-700 rounded-full flex items-center justify-center hover:bg-divider-500/50 active:bg-divider-500 transition-colors"
|
||||
onClick={() =>
|
||||
window.parent?.postMessage({ type: "close" }, "*")
|
||||
}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="px-4 py-3 gap-3 font-medium border border-transparent rounded-full flex items-center justify-center bg-blue-500 hover:bg-blue-600 active:bg-blue-700 disabled:opacity-50 transition-colors"
|
||||
onClick={() => {
|
||||
const hash = compressToEncodedURIComponent(
|
||||
JSON.stringify(configData.data)
|
||||
);
|
||||
|
||||
const state = getStateFromUrl(window.location.href);
|
||||
const targetUrl = `${state.basePath}/c/${hash}`;
|
||||
window.parent?.postMessage(
|
||||
{
|
||||
type: "apply",
|
||||
value: { targetUrl, config: configData.data },
|
||||
},
|
||||
"*"
|
||||
);
|
||||
}}
|
||||
>
|
||||
<span className="text-white">Apply</span>
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ShareDialog config={configData.data}>
|
||||
<button
|
||||
type="button"
|
||||
className="px-4 py-3 gap-3 font-medium border border-divider-700 rounded-full flex items-center justify-center hover:bg-divider-500/50 active:bg-divider-500 transition-colors"
|
||||
>
|
||||
<ShareIcon className="flex-shrink-0" /> <span>Share</span>
|
||||
</button>
|
||||
</ShareDialog>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"px-4 py-3 gap-3 font-medium border border-transparent rounded-full flex items-center justify-center bg-blue-500 disabled:opacity-50 transition-colors",
|
||||
!isSendDisabled
|
||||
? "hover:bg-blue-600 active:bg-blue-700"
|
||||
: ""
|
||||
)}
|
||||
onClick={onSubmit}
|
||||
disabled={isSendDisabled}
|
||||
>
|
||||
{stopStream ? (
|
||||
<>
|
||||
<div role="status">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="w-5 h-5 animate-spin text-white fill-ls-blue"
|
||||
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>
|
||||
<span className="sr-only">Loading...</span>
|
||||
</div>
|
||||
<span className="text-white">Stop</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<SendIcon className="flex-shrink-0" />
|
||||
<span className="text-white">Start</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{inputSchema.data != null ? (
|
||||
<InputPlayground
|
||||
configSchema={props.configSchema}
|
||||
inputSchema={inputSchema.data}
|
||||
configData={configData}
|
||||
startStream={startStream}
|
||||
stopStream={stopStream}
|
||||
>
|
||||
{latest && (
|
||||
<div className="flex flex-col gap-3">
|
||||
<h2 className="text-xl font-semibold">Output</h2>
|
||||
<div className="p-4 border border-divider-700 flex flex-col gap-3 rounded-2xl bg-background text-lg whitespace-pre-wrap break-words relative group">
|
||||
<StreamOutput streamed={latest.streamed_output} />
|
||||
|
||||
{feedback.data && latest.id ? (
|
||||
<div className="absolute right-4 top-4 flex items-center gap-2 transition-opacity opacity-0 focus-within:opacity-100 group-hover:opacity-100">
|
||||
<CorrectnessFeedback
|
||||
key={latest.id}
|
||||
runId={latest.id}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<IntermediateSteps
|
||||
latest={latest}
|
||||
feedbackEnabled={!!feedback.data}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</InputPlayground>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</AppCallbackContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function Playground() {
|
||||
const configSchema = useConfigSchema();
|
||||
if (configSchema.isLoading) return null;
|
||||
|
||||
if (configSchema.error != null) {
|
||||
return (
|
||||
<div className="bg-background rounded-xl">
|
||||
<div className="bg-red-500/10 text-red-700 dark:text-red-300 rounded-xl p-3">
|
||||
{configSchema.error.toString()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (configSchema.data == null) return "No config schema found";
|
||||
return <ConfigPlayground configSchema={configSchema.data} />;
|
||||
}
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<div className="flex items-center flex-col text-ls-black bg-gradient-to-b from-[#F9FAFB] to-[#EFF8FF] min-h-[100dvh] dark:from-[#0C111C] dark:to-[#0C111C]">
|
||||
<div className="flex flex-col flex-grow gap-4 px-4 pt-6 max-w-[800px] w-full">
|
||||
<h1 className="text-2xl text-left">
|
||||
<strong>🦜 LangServe</strong> Playground
|
||||
</h1>
|
||||
<Playground />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,6 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M17 2V13M22 9.8V5.2C22 4.07989 22 3.51984 21.782 3.09202C21.5903 2.71569 21.2843 2.40973 20.908 2.21799C20.4802 2 19.9201 2 18.8 2H8.11802C6.65654 2 5.92579 2 5.33559 2.26743C4.81541 2.50314 4.37331 2.88242 4.06125 3.36072C3.70718 3.90339 3.59606 4.62564 3.37383 6.07012L2.85076 9.47012C2.55765 11.3753 2.4111 12.3279 2.69381 13.0691C2.94195 13.7197 3.40866 14.2637 4.01393 14.6079C4.70354 15 5.66734 15 7.59494 15H8.40001C8.96006 15 9.24009 15 9.454 15.109C9.64216 15.2049 9.79514 15.3578 9.89101 15.546C10 15.7599 10 16.0399 10 16.6V19.5342C10 20.896 11.104 22 12.4658 22C12.7907 22 13.085 21.8087 13.217 21.5119L16.5777 13.9502C16.7306 13.6062 16.807 13.4343 16.9278 13.3082C17.0346 13.1967 17.1657 13.1115 17.311 13.0592C17.4753 13 17.6634 13 18.0398 13H18.8C19.9201 13 20.4802 13 20.908 12.782C21.2843 12.5903 21.5903 12.2843 21.782 11.908C22 11.4802 22 10.9201 22 9.8Z"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,6 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M7 22V11M2 13V20C2 21.1046 2.89543 22 4 22H17.4262C18.907 22 20.1662 20.9197 20.3914 19.4562L21.4683 12.4562C21.7479 10.6389 20.3418 9 18.5032 9H15C14.4477 9 14 8.55228 14 8V4.46584C14 3.10399 12.896 2 11.5342 2C11.2093 2 10.915 2.1913 10.7831 2.48812L7.26394 10.4061C7.10344 10.7673 6.74532 11 6.35013 11H4C2.89543 11 2 11.8954 2 13Z"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 549 B |
@@ -11,7 +11,8 @@ import {
|
||||
import { AutosizeTextarea } from "./AutosizeTextarea";
|
||||
import { isJsonSchemaExtra } from "../utils/schema";
|
||||
import { useStreamCallback } from "../useStreamCallback";
|
||||
import { traverseNaiveJsonPath } from "../utils/path";
|
||||
import { getNormalizedJsonPath, traverseNaiveJsonPath } from "../utils/path";
|
||||
import { getMessageContent } from "../utils/messages";
|
||||
|
||||
type MessageTuple = [string, string];
|
||||
|
||||
@@ -53,9 +54,25 @@ export const ChatMessageTuplesControlRenderer = withJsonFormsControlProps(
|
||||
const widget = props.schema.extra.widget;
|
||||
if (!("input" in widget) && !("output" in widget)) return;
|
||||
|
||||
const human = traverseNaiveJsonPath(ctx.input, widget.input ?? "");
|
||||
const ai = traverseNaiveJsonPath(ctx.output, widget.output ?? "");
|
||||
props.handleChange(props.path, [...data, [human, ai]]);
|
||||
const inputPath = getNormalizedJsonPath(widget.input ?? "");
|
||||
const outputPath = getNormalizedJsonPath(widget.output ?? "");
|
||||
|
||||
const isSingleOutputKey =
|
||||
ctx.output != null &&
|
||||
Object.keys(ctx.output).length === 1 &&
|
||||
Object.keys(ctx.output)[0] === "output";
|
||||
|
||||
const human = traverseNaiveJsonPath(ctx.input, inputPath);
|
||||
let ai = traverseNaiveJsonPath(ctx.output, outputPath);
|
||||
|
||||
if (isSingleOutputKey) {
|
||||
ai = traverseNaiveJsonPath(ai, ["output", ...outputPath]) ?? ai;
|
||||
}
|
||||
|
||||
ai = getMessageContent(ai);
|
||||
if (typeof human === "string" && typeof ai === "string") {
|
||||
props.handleChange(props.path, [...data, [human, ai]]);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
} from "@jsonforms/core";
|
||||
import { AutosizeTextarea } from "./AutosizeTextarea";
|
||||
import { useStreamCallback } from "../useStreamCallback";
|
||||
import { traverseNaiveJsonPath } from "../utils/path";
|
||||
import { getNormalizedJsonPath, traverseNaiveJsonPath } from "../utils/path";
|
||||
import { isJsonSchemaExtra } from "../utils/schema";
|
||||
import * as ToggleGroup from "@radix-ui/react-toggle-group";
|
||||
|
||||
@@ -122,8 +122,20 @@ export const ChatMessagesControlRenderer = withJsonFormsControlProps(
|
||||
const widget = props.schema.extra.widget;
|
||||
if (!("input" in widget) && !("output" in widget)) return;
|
||||
|
||||
const human = traverseNaiveJsonPath(ctx.input, widget.input ?? "");
|
||||
const ai = traverseNaiveJsonPath(ctx.output, widget.output ?? "");
|
||||
const inputPath = getNormalizedJsonPath(widget.input ?? "");
|
||||
const outputPath = getNormalizedJsonPath(widget.output ?? "");
|
||||
|
||||
const human = traverseNaiveJsonPath(ctx.input, inputPath);
|
||||
let ai = traverseNaiveJsonPath(ctx.output, outputPath);
|
||||
|
||||
const isSingleOutputKey =
|
||||
ctx.output != null &&
|
||||
Object.keys(ctx.output).length === 1 &&
|
||||
Object.keys(ctx.output)[0] === "output";
|
||||
|
||||
if (isSingleOutputKey) {
|
||||
ai = traverseNaiveJsonPath(ai, ["output", ...outputPath]) ?? ai;
|
||||
}
|
||||
|
||||
const humanMsg = constructMessage(human, "human");
|
||||
const aiMsg = constructMessage(ai, "ai");
|
||||
@@ -296,12 +308,6 @@ export const ChatMessagesControlRenderer = withJsonFormsControlProps(
|
||||
message.additional_kwargs?.function_call.name ?? ""
|
||||
}
|
||||
onChange={(e) => {
|
||||
console.log(
|
||||
Paths.compose(
|
||||
msgPath,
|
||||
"additional_kwargs.function_call.name"
|
||||
)
|
||||
);
|
||||
props.handleChange(
|
||||
Paths.compose(
|
||||
msgPath,
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
JsonSchema,
|
||||
isAnyOfControl,
|
||||
} from "@jsonforms/core";
|
||||
import { renderers, cells } from "../App";
|
||||
import { renderers, cells } from "../renderers";
|
||||
|
||||
export const CustomAnyOfRenderer = withJsonFormsAnyOfProps((props) => {
|
||||
const anyOfRenderInfos = createCombinatorRenderInfos(
|
||||
|
||||
@@ -4,8 +4,12 @@ import ChevronRight from "../assets/ChevronRight.svg?react";
|
||||
import { RunState } from "../useStreamLog";
|
||||
import { cn } from "../utils/cn";
|
||||
import { str } from "../utils/str";
|
||||
import { CorrectnessFeedback } from "./feedback/CorrectnessFeedback";
|
||||
|
||||
export function IntermediateSteps(props: { latest: RunState }) {
|
||||
export function IntermediateSteps(props: {
|
||||
latest: RunState;
|
||||
feedbackEnabled: boolean;
|
||||
}) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const length = Object.values(props.latest.logs).length;
|
||||
const disabled = length === 0;
|
||||
@@ -41,9 +45,16 @@ export function IntermediateSteps(props: { latest: RunState }) {
|
||||
<strong className="text-sm font-medium">{log.name}</strong>
|
||||
<p className="text-sm">{dayjs.utc(log.start_time).fromNow()}</p>
|
||||
</div>
|
||||
<pre className="break-words whitespace-pre-wrap min-w-0 text-sm bg-ls-gray-400 rounded-lg p-3">
|
||||
{str(log.final_output) ?? "..."}
|
||||
</pre>
|
||||
<div className="bg-ls-gray-400 rounded-lg p-3 relative group">
|
||||
<pre className="break-words whitespace-pre-wrap min-w-0 text-sm">
|
||||
{str(log.final_output) ?? "..."}
|
||||
</pre>
|
||||
{props.feedbackEnabled && log.id ? (
|
||||
<div className="absolute right-3 top-3 flex items-center gap-2 transition-opacity opacity-0 focus-within:opacity-100 group-hover:opacity-100">
|
||||
<CorrectnessFeedback key={log.id} runId={log.id} />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -4,37 +4,11 @@ import CodeIcon from "../assets/CodeIcon.svg?react";
|
||||
import PadlockIcon from "../assets/PadlockIcon.svg?react";
|
||||
import CopyIcon from "../assets/CopyIcon.svg?react";
|
||||
import CheckCircleIcon from "../assets/CheckCircleIcon.svg?react";
|
||||
import {
|
||||
compressToEncodedURIComponent,
|
||||
decompressFromEncodedURIComponent,
|
||||
} from "lz-string";
|
||||
import { compressToEncodedURIComponent } from "lz-string";
|
||||
import { getStateFromUrl } from "../utils/url";
|
||||
|
||||
const URL_LENGTH_LIMIT = 2000;
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
function CopyButton(props: { value: string }) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const cbRef = useRef<number | null>(null);
|
||||
@@ -121,7 +95,6 @@ const result = await chain.invoke({ ... });
|
||||
<PadlockIcon className="mx-3" />
|
||||
<div className="overflow-auto whitespace-nowrap py-3 no-scrollbar text-ls-gray-100">
|
||||
{playgroundUrl.split("://")[1]}
|
||||
PadlockIcon
|
||||
</div>
|
||||
<CopyButton value={playgroundUrl} />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import SendIcon from "../assets/SendIcon.svg?react";
|
||||
import { cn } from "../utils/cn";
|
||||
|
||||
export function SubmitButton(props: {
|
||||
disabled: boolean;
|
||||
isLoading?: boolean;
|
||||
onSubmit: () => void;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"px-4 py-3 gap-3 font-medium border border-transparent rounded-full flex items-center justify-center bg-blue-500 disabled:opacity-50 transition-colors",
|
||||
!props.disabled ? "hover:bg-blue-600 active:bg-blue-700" : ""
|
||||
)}
|
||||
onClick={props.onSubmit}
|
||||
disabled={props.disabled}
|
||||
>
|
||||
{props.isLoading ? (
|
||||
<>
|
||||
<div role="status">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="w-5 h-5 animate-spin text-white fill-ls-blue"
|
||||
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>
|
||||
<span className="sr-only">Loading...</span>
|
||||
</div>
|
||||
<span className="text-white">Stop</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<SendIcon className="flex-shrink-0" />
|
||||
<span className="text-white">Start</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import ThumbsUpIcon from "../../assets/ThumbsUpIcon.svg?react";
|
||||
import ThumbsDownIcon from "../../assets/ThumbsDownIcon.svg?react";
|
||||
import CircleSpinIcon from "../../assets/CircleSpinIcon.svg?react";
|
||||
import { resolveApiUrl } from "../../utils/url";
|
||||
import { useState } from "react";
|
||||
import { cn } from "../../utils/cn";
|
||||
import useSWRMutation from "swr/mutation";
|
||||
|
||||
const useFeedbackMutation = (runId: string) => {
|
||||
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) throw new Error(`Failed request ${request.status}`);
|
||||
const json: {
|
||||
id: string;
|
||||
score: number;
|
||||
} = await request.json();
|
||||
|
||||
return json;
|
||||
}
|
||||
);
|
||||
|
||||
return { lastArg: mutation.isMutating ? lastArg : null, mutation };
|
||||
};
|
||||
|
||||
export function CorrectnessFeedback(props: { runId: string }) {
|
||||
const score = useFeedbackMutation(props.runId);
|
||||
|
||||
if (props.runId == null) return null;
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"border focus-within:border-ls-blue focus-within:outline-none bg-background rounded p-1 border-divider-700 hover:bg-divider-500/50 active:bg-divider-500",
|
||||
score.mutation.data?.score === 1 && "text-teal-500"
|
||||
)}
|
||||
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" />
|
||||
) : (
|
||||
<ThumbsUpIcon className="w-4 h-4" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"border focus-within:border-ls-blue focus-within:outline-none bg-background rounded p-1 border-divider-700 hover:bg-divider-500/50 active:bg-divider-500",
|
||||
score.mutation.data?.score === -1 && "text-red-500"
|
||||
)}
|
||||
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" />
|
||||
) : (
|
||||
<ThumbsDownIcon className="w-4 h-4" />
|
||||
)}
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,11 @@
|
||||
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 />);
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import {
|
||||
materialAllOfControlTester,
|
||||
MaterialAllOfRenderer,
|
||||
MaterialObjectRenderer,
|
||||
materialOneOfControlTester,
|
||||
MaterialOneOfRenderer,
|
||||
} from "@jsonforms/material-renderers";
|
||||
import {
|
||||
BooleanCell,
|
||||
DateCell,
|
||||
DateTimeCell,
|
||||
EnumCell,
|
||||
IntegerCell,
|
||||
NumberCell,
|
||||
SliderCell,
|
||||
TimeCell,
|
||||
booleanCellTester,
|
||||
dateCellTester,
|
||||
dateTimeCellTester,
|
||||
enumCellTester,
|
||||
integerCellTester,
|
||||
numberCellTester,
|
||||
sliderCellTester,
|
||||
textAreaCellTester,
|
||||
textCellTester,
|
||||
timeCellTester,
|
||||
vanillaRenderers,
|
||||
InputControl,
|
||||
} from "@jsonforms/vanilla-renderers";
|
||||
import {
|
||||
RankedTester,
|
||||
rankWith,
|
||||
and,
|
||||
uiTypeIs,
|
||||
schemaMatches,
|
||||
schemaTypeIs,
|
||||
} from "@jsonforms/core";
|
||||
import CustomArrayControlRenderer, {
|
||||
materialArrayControlTester,
|
||||
} from "./components/CustomArrayControlRenderer";
|
||||
import CustomTextAreaCell from "./components/CustomTextAreaCell";
|
||||
import JsonTextAreaCell from "./components/JsonTextAreaCell";
|
||||
import {
|
||||
chatMessagesTester,
|
||||
ChatMessagesControlRenderer,
|
||||
} from "./components/ChatMessagesControlRenderer";
|
||||
import {
|
||||
ChatMessageTuplesControlRenderer,
|
||||
chatMessagesTupleTester,
|
||||
} from "./components/ChatMessageTuplesControlRenderer";
|
||||
import {
|
||||
fileBase64Tester,
|
||||
FileBase64ControlRenderer,
|
||||
} from "./components/FileBase64Tester";
|
||||
import {
|
||||
customAnyOfTester,
|
||||
CustomAnyOfRenderer,
|
||||
} from "./components/CustomAnyOfRenderer";
|
||||
|
||||
const isObjectWithPropertiesControl = rankWith(
|
||||
2,
|
||||
and(
|
||||
uiTypeIs("Control"),
|
||||
schemaTypeIs("object"),
|
||||
schemaMatches((schema) =>
|
||||
Object.prototype.hasOwnProperty.call(schema, "properties")
|
||||
)
|
||||
)
|
||||
);
|
||||
const isObject = rankWith(1, and(uiTypeIs("Control"), schemaTypeIs("object")));
|
||||
const isElse = rankWith(1, and(uiTypeIs("Control")));
|
||||
|
||||
export const renderers = [
|
||||
...vanillaRenderers,
|
||||
|
||||
// use material renderers to handle objects and json schema references
|
||||
// they should yield the rendering to simpler cells
|
||||
{ tester: isObjectWithPropertiesControl, renderer: MaterialObjectRenderer },
|
||||
{ tester: materialAllOfControlTester, renderer: MaterialAllOfRenderer },
|
||||
{ tester: materialOneOfControlTester, renderer: MaterialOneOfRenderer },
|
||||
|
||||
{ tester: customAnyOfTester, renderer: CustomAnyOfRenderer },
|
||||
|
||||
// custom renderers
|
||||
{ tester: materialArrayControlTester, renderer: CustomArrayControlRenderer },
|
||||
{ tester: isObject, renderer: InputControl },
|
||||
{ tester: chatMessagesTester, renderer: ChatMessagesControlRenderer },
|
||||
{
|
||||
tester: chatMessagesTupleTester,
|
||||
renderer: ChatMessageTuplesControlRenderer,
|
||||
},
|
||||
{ tester: fileBase64Tester, renderer: FileBase64ControlRenderer },
|
||||
];
|
||||
const nestedArrayControlTester: RankedTester = rankWith(1, (_, jsonSchema) => {
|
||||
return jsonSchema.type === "array";
|
||||
});
|
||||
|
||||
export const cells = [
|
||||
{ tester: booleanCellTester, cell: BooleanCell },
|
||||
{ tester: dateCellTester, cell: DateCell },
|
||||
{ tester: dateTimeCellTester, cell: DateTimeCell },
|
||||
{ tester: enumCellTester, cell: EnumCell },
|
||||
{ tester: integerCellTester, cell: IntegerCell },
|
||||
{ tester: numberCellTester, cell: NumberCell },
|
||||
{ tester: sliderCellTester, cell: SliderCell },
|
||||
{ tester: textAreaCellTester, cell: CustomTextAreaCell },
|
||||
{ tester: textCellTester, cell: CustomTextAreaCell },
|
||||
{ tester: timeCellTester, cell: TimeCell },
|
||||
{ tester: nestedArrayControlTester, cell: CustomArrayControlRenderer },
|
||||
{ tester: isElse, cell: JsonTextAreaCell },
|
||||
];
|
||||
@@ -0,0 +1,50 @@
|
||||
import { JsonForms } from "@jsonforms/react";
|
||||
import { JsonFormsCore, JsonSchema } from "@jsonforms/core";
|
||||
import { renderers, cells } from "../renderers";
|
||||
|
||||
export type ConfigValue = Pick<JsonFormsCore, "data" | "errors"> & {
|
||||
defaults: boolean;
|
||||
};
|
||||
|
||||
export function SectionConfigure(props: {
|
||||
config: JsonSchema | undefined;
|
||||
value: ConfigValue;
|
||||
onChange: (value: ConfigValue) => void;
|
||||
}) {
|
||||
if (props.config == null || Object.keys(props.config).length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 [&:has(.content>.vertical-layout:first-child:last-child:empty)]:hidden">
|
||||
<h2 className="text-xl font-semibold">Configure</h2>
|
||||
|
||||
<div className="content flex flex-col gap-3">
|
||||
<JsonForms
|
||||
schema={props.config}
|
||||
data={props.value.data}
|
||||
renderers={renderers}
|
||||
cells={cells}
|
||||
onChange={({ data, errors }) => {
|
||||
if (data) {
|
||||
props.onChange({ data, errors, defaults: false });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{!!props.value.errors?.length && props.value.data && (
|
||||
<div className="bg-background rounded-xl">
|
||||
<div className="bg-red-500/10 text-red-700 dark:text-red-300 rounded-xl p-3">
|
||||
<strong className="font-bold">Validation Errors</strong>
|
||||
<ul className="list-disc pl-5">
|
||||
{props.value.errors?.map((e, i) => (
|
||||
<li key={i}>{e.message}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { useMemo } from "react";
|
||||
import defaults from "../utils/defaults";
|
||||
import { JsonForms } from "@jsonforms/react";
|
||||
import { JsonFormsCore, JsonSchema } from "@jsonforms/core";
|
||||
import { renderers, cells } from "../renderers";
|
||||
|
||||
export type InputValue = Pick<JsonFormsCore, "data" | "errors">;
|
||||
|
||||
export function SectionInputs(props: {
|
||||
input: JsonSchema | undefined;
|
||||
value: InputValue;
|
||||
onChange: (value: InputValue) => void;
|
||||
}) {
|
||||
const isInputResetable = useMemo(() => {
|
||||
if (!props.input) return false;
|
||||
return (
|
||||
JSON.stringify(defaults(props.input)) !== JSON.stringify(props.value.data)
|
||||
);
|
||||
}, [props.input, props.value.data]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<h2 className="text-xl font-semibold">Try it</h2>
|
||||
|
||||
<div className="p-4 border border-divider-700 flex flex-col gap-3 rounded-2xl bg-background">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-medium">Inputs</h3>
|
||||
{isInputResetable && (
|
||||
<button
|
||||
type="button"
|
||||
className="text-sm px-1 -mr-1 py-0.5 rounded-md hover:bg-divider-500/50 active:bg-divider-500 text-ls-gray-100"
|
||||
onClick={() =>
|
||||
props.onChange({
|
||||
data: defaults(props.input),
|
||||
errors: [],
|
||||
})
|
||||
}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<JsonForms
|
||||
schema={props.input}
|
||||
data={props.value.data}
|
||||
renderers={renderers}
|
||||
cells={cells}
|
||||
onChange={({ data, errors }) => props.onChange({ data, errors })}
|
||||
/>
|
||||
{!!props.value.errors?.length && props.value.data && (
|
||||
<div className="bg-red-500/10 text-red-700 dark:text-red-300 rounded-xl p-3">
|
||||
<strong className="font-bold">Validation Errors</strong>
|
||||
<ul className="list-disc pl-5">
|
||||
{props.value.errors?.map((e, i) => (
|
||||
<li key={i}>{e.message}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { resolveApiUrl } from "./utils/url";
|
||||
import { simplifySchema } from "./utils/simplifySchema";
|
||||
import { JsonFormsCore } from "@jsonforms/core";
|
||||
import { JsonSchema } from "@jsonforms/core";
|
||||
import { compressToEncodedURIComponent } from "lz-string";
|
||||
import { useDebounce } from "use-debounce";
|
||||
|
||||
import useSWR from "swr";
|
||||
import defaults from "./utils/defaults";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -11,66 +12,72 @@ declare global {
|
||||
CONFIG_SCHEMA?: any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
INPUT_SCHEMA?: any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
FEEDBACK_ENABLED?: any;
|
||||
}
|
||||
}
|
||||
|
||||
export function useSchemas(
|
||||
configData: Pick<JsonFormsCore, "data" | "errors"> & { defaults: boolean }
|
||||
) {
|
||||
const [schemas, setSchemas] = useState<{
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
config: null | any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
input: null | any;
|
||||
}>({
|
||||
config: null,
|
||||
input: null,
|
||||
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;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
async function save() {
|
||||
if (import.meta.env.DEV) {
|
||||
const [config, input] = await Promise.all([
|
||||
fetch(resolveApiUrl("/config_schema"))
|
||||
.then((r) => r.json())
|
||||
.then(simplifySchema),
|
||||
fetch(resolveApiUrl("/input_schema"))
|
||||
.then((r) => r.json())
|
||||
.then(simplifySchema),
|
||||
]);
|
||||
setSchemas({ config, input });
|
||||
} else {
|
||||
setSchemas({
|
||||
config: window.CONFIG_SCHEMA
|
||||
? await simplifySchema(window.CONFIG_SCHEMA)
|
||||
: null,
|
||||
input: window.INPUT_SCHEMA
|
||||
? await simplifySchema(window.INPUT_SCHEMA)
|
||||
: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
save();
|
||||
}, []);
|
||||
|
||||
const [debouncedConfigData] = useDebounce(configData, 500);
|
||||
|
||||
useEffect(() => {
|
||||
if (!debouncedConfigData.defaults) {
|
||||
fetch(
|
||||
resolveApiUrl(
|
||||
`/c/${compressToEncodedURIComponent(
|
||||
JSON.stringify(debouncedConfigData.data)
|
||||
)}/input_schema`
|
||||
)
|
||||
)
|
||||
.then((r) => r.json())
|
||||
.then(simplifySchema)
|
||||
.then((input) => setSchemas((current) => ({ ...current, input })))
|
||||
.catch(() => {}); // ignore errors, eg. due to incomplete config
|
||||
}
|
||||
}, [debouncedConfigData]);
|
||||
|
||||
return schemas;
|
||||
}
|
||||
|
||||
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 }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,35 @@ export const AppCallbackContext = createContext<MutableRefObject<{
|
||||
onError: Exclude<StreamCallback["onError"], undefined>[];
|
||||
}> | null>(null);
|
||||
|
||||
export function useAppStreamCallbacks() {
|
||||
// callbacks handling
|
||||
const context = useRef<{
|
||||
onStart: Exclude<StreamCallback["onStart"], undefined>[];
|
||||
onSuccess: Exclude<StreamCallback["onSuccess"], undefined>[];
|
||||
onError: Exclude<StreamCallback["onError"], undefined>[];
|
||||
}>({ onStart: [], onSuccess: [], onError: [] });
|
||||
|
||||
const callbacks: StreamCallback = {
|
||||
onStart(...args) {
|
||||
for (const callback of context.current.onStart) {
|
||||
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" | "onSuccess" | "onError"
|
||||
>(type: Type, callback: Exclude<StreamCallback[Type], undefined>) {
|
||||
|
||||
@@ -76,6 +76,7 @@ export function useStreamLog(callbacks: StreamCallback = {}) {
|
||||
setLatest(innerLatest);
|
||||
}
|
||||
},
|
||||
openWhenHidden: true,
|
||||
onclose() {
|
||||
setController(null);
|
||||
successRef.current?.({ input, output: innerLatest?.final_output });
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
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;
|
||||
}
|
||||
@@ -2,11 +2,18 @@ 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 = Array.isArray(path) ? path : [path];
|
||||
const queue = getNormalizedJsonPath(path);
|
||||
|
||||
let tmp: unknown = x;
|
||||
while (queue.length > 0) {
|
||||
const first = queue.shift()!;
|
||||
|
||||
@@ -1,5 +1,33 @@
|
||||
export function resolveApiUrl(path: string) {
|
||||
let prefix = window.location.pathname.split("/playground")[0];
|
||||
if (prefix.endsWith("/")) prefix = prefix.slice(0, -1);
|
||||
return new URL(prefix + path, window.location.origin);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ export default defineConfig({
|
||||
plugins: [svgr(), react()],
|
||||
server: {
|
||||
proxy: {
|
||||
"^/____LANGSERVE_BASE_URL.*/(config_schema|input_schema|stream_log)": {
|
||||
"^/____LANGSERVE_BASE_URL.*/(config_schema|input_schema|stream_log|feedback)(/[a-zA-Z0-9-]*)?$": {
|
||||
target: "http://127.0.0.1:8000",
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace("/____LANGSERVE_BASE_URL", ""),
|
||||
|
||||
@@ -1407,6 +1407,11 @@ chokidar@^3.5.3:
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||
|
||||
client-only@^0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
|
||||
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
|
||||
|
||||
clsx@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.0.0.tgz#12658f3fd98fafe62075595a5c30e43d18f3d00b"
|
||||
@@ -2620,6 +2625,14 @@ svg-parser@^2.0.4:
|
||||
resolved "https://registry.yarnpkg.com/svg-parser/-/svg-parser-2.0.4.tgz#fdc2e29e13951736140b76cb122c8ee6630eb6b5"
|
||||
integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==
|
||||
|
||||
swr@^2.2.4:
|
||||
version "2.2.4"
|
||||
resolved "https://registry.yarnpkg.com/swr/-/swr-2.2.4.tgz#03ec4c56019902fbdc904d78544bd7a9a6fa3f07"
|
||||
integrity sha512-njiZ/4RiIhoOlAaLYDqwz5qH/KZXVilRLvomrx83HjzCWTfa+InyfAjv05PSFxnmLzZkNO9ZfvgoqzAaEI4sGQ==
|
||||
dependencies:
|
||||
client-only "^0.0.1"
|
||||
use-sync-external-store "^1.2.0"
|
||||
|
||||
tailwind-merge@^1.14.0:
|
||||
version "1.14.0"
|
||||
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-1.14.0.tgz#e677f55d864edc6794562c63f5001f45093cdb8b"
|
||||
@@ -2751,6 +2764,11 @@ use-sidecar@^1.1.2:
|
||||
detect-node-es "^1.1.0"
|
||||
tslib "^2.0.0"
|
||||
|
||||
use-sync-external-store@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
|
||||
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
|
||||
|
||||
util-deprecate@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import pydantic
|
||||
|
||||
|
||||
def _get_pydantic_version() -> int:
|
||||
"""Get the pydantic major version."""
|
||||
return int(pydantic.__version__.split(".")[0])
|
||||
|
||||
|
||||
# Code is written to support both version 1 and 2
|
||||
PYDANTIC_MAJOR_VERSION = _get_pydantic_version()
|
||||
@@ -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
|
||||
+5
-6
@@ -2,13 +2,9 @@ from datetime import datetime
|
||||
from typing import Dict, List, Optional, Union
|
||||
from uuid import UUID
|
||||
|
||||
from langserve.pydantic import PYDANTIC_MAJOR_VERSION
|
||||
from pydantic import BaseModel # Floats between v1 and v2
|
||||
|
||||
if PYDANTIC_MAJOR_VERSION == 2:
|
||||
from pydantic.v1 import BaseModel as BaseModelV1
|
||||
else:
|
||||
from pydantic import BaseModel as BaseModelV1
|
||||
from pydantic import BaseModel
|
||||
from langserve.pydantic_v1 import BaseModel as BaseModelV1
|
||||
|
||||
|
||||
class CustomUserType(BaseModelV1):
|
||||
@@ -100,6 +96,9 @@ class Feedback(BaseFeedback):
|
||||
Represents feedback given on an individual run
|
||||
"""
|
||||
|
||||
id: UUID
|
||||
"""The unique ID of the feedback that was created."""
|
||||
|
||||
created_at: datetime
|
||||
"""The time the feedback was created."""
|
||||
|
||||
|
||||
+15
-22
@@ -11,11 +11,11 @@ any information about the exception. This is done to prevent leaking
|
||||
sensitive information from the server to the client.
|
||||
"""
|
||||
import abc
|
||||
import json
|
||||
import logging
|
||||
from functools import lru_cache
|
||||
from typing import Any, Dict, List, Union
|
||||
|
||||
import orjson
|
||||
from langchain.prompts.base import StringPromptValue
|
||||
from langchain.prompts.chat import ChatPromptValueConcrete
|
||||
from langchain.schema.agent import AgentAction, AgentActionMessageLog, AgentFinish
|
||||
@@ -39,14 +39,9 @@ from langchain.schema.output import (
|
||||
LLMResult,
|
||||
)
|
||||
|
||||
from langserve.pydantic_v1 import BaseModel, ValidationError
|
||||
from langserve.validation import CallbackEvent
|
||||
|
||||
try:
|
||||
from pydantic.v1 import BaseModel, ValidationError
|
||||
except ImportError:
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -88,14 +83,11 @@ class WellKnownLCObject(BaseModel):
|
||||
]
|
||||
|
||||
|
||||
# Custom JSON Encoder
|
||||
class _LangChainEncoder(json.JSONEncoder):
|
||||
"""Custom JSON Encoder that can encode pydantic objects as well."""
|
||||
|
||||
def default(self, obj) -> Any:
|
||||
if isinstance(obj, BaseModel):
|
||||
return obj.dict()
|
||||
return super().default(obj)
|
||||
def default(obj) -> Any:
|
||||
"""Default serialization for well known objects."""
|
||||
if isinstance(obj, BaseModel):
|
||||
return obj.dict()
|
||||
return super().default(obj)
|
||||
|
||||
|
||||
def _decode_lc_objects(value: Any) -> Any:
|
||||
@@ -154,11 +146,11 @@ class Serializer(abc.ABC):
|
||||
"""Convert the given object to a JSON serializable object."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def dumps(self, obj: Any) -> str:
|
||||
def dumps(self, obj: Any) -> bytes:
|
||||
"""Dump the given object as a JSON string."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def loads(self, s: str) -> Any:
|
||||
def loads(self, s: bytes) -> Any:
|
||||
"""Load the given JSON string."""
|
||||
|
||||
@abc.abstractmethod
|
||||
@@ -169,18 +161,19 @@ class Serializer(abc.ABC):
|
||||
class WellKnownLCSerializer(Serializer):
|
||||
def dumpd(self, obj: Any) -> Any:
|
||||
"""Convert the given object to a JSON serializable object."""
|
||||
return json.loads(json.dumps(obj, cls=_LangChainEncoder)) # :*(
|
||||
return orjson.loads(orjson.dumps(obj, default=default))
|
||||
|
||||
def dumps(self, obj: Any) -> str:
|
||||
def dumps(self, obj: Any) -> bytes:
|
||||
"""Dump the given object as a JSON string."""
|
||||
return json.dumps(obj, cls=_LangChainEncoder)
|
||||
return orjson.dumps(obj, default=default)
|
||||
|
||||
def loadd(self, obj: Any) -> Any:
|
||||
"""Load the given object."""
|
||||
return _decode_lc_objects(obj)
|
||||
|
||||
def loads(self, s: str) -> Any:
|
||||
def loads(self, s: bytes) -> Any:
|
||||
"""Load the given JSON string."""
|
||||
return self.loadd(json.loads(s))
|
||||
return self.loadd(orjson.loads(s))
|
||||
|
||||
|
||||
def _project_top_level(model: BaseModel) -> Dict[str, Any]:
|
||||
|
||||
+669
-999
File diff suppressed because it is too large
Load Diff
@@ -26,6 +26,7 @@ from langchain.schema import (
|
||||
Generation,
|
||||
RunInfo,
|
||||
)
|
||||
from typing_extensions import Type
|
||||
|
||||
from langserve.schema import BatchResponseMetadata, SingletonResponseMetadata
|
||||
|
||||
@@ -34,7 +35,6 @@ try:
|
||||
except ImportError:
|
||||
from pydantic import BaseModel, Field, create_model
|
||||
|
||||
from typing_extensions import Type, TypedDict
|
||||
|
||||
# Type that is either a python annotation or a pydantic model that can be
|
||||
# used to validate the input or output of a runnable.
|
||||
@@ -46,7 +46,7 @@ Validator = Union[Type[BaseModel], type]
|
||||
def create_invoke_request_model(
|
||||
namespace: str,
|
||||
input_type: Validator,
|
||||
config: TypedDict,
|
||||
config: Type[BaseModel],
|
||||
) -> Type[BaseModel]:
|
||||
"""Create a pydantic model for the invoke request."""
|
||||
invoke_request_type = create_model(
|
||||
@@ -77,7 +77,7 @@ def create_invoke_request_model(
|
||||
def create_stream_request_model(
|
||||
namespace: str,
|
||||
input_type: Validator,
|
||||
config: TypedDict,
|
||||
config: Type[BaseModel],
|
||||
) -> Type[BaseModel]:
|
||||
"""Create a pydantic model for the stream request."""
|
||||
stream_request_model = create_model(
|
||||
@@ -108,7 +108,7 @@ def create_stream_request_model(
|
||||
def create_batch_request_model(
|
||||
namespace: str,
|
||||
input_type: Validator,
|
||||
config: TypedDict,
|
||||
config: Type[BaseModel],
|
||||
) -> Type[BaseModel]:
|
||||
"""Create a pydantic model for the batch request."""
|
||||
batch_request_type = create_model(
|
||||
@@ -140,7 +140,7 @@ def create_batch_request_model(
|
||||
def create_stream_log_request_model(
|
||||
namespace: str,
|
||||
input_type: Validator,
|
||||
config: TypedDict,
|
||||
config: Type[BaseModel],
|
||||
) -> Type[BaseModel]:
|
||||
"""Create a pydantic model for the invoke request."""
|
||||
stream_log_request = create_model(
|
||||
|
||||
Generated
+1036
-798
File diff suppressed because it is too large
Load Diff
+17
-3
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langserve"
|
||||
version = "0.0.24"
|
||||
version = "0.0.38"
|
||||
description = ""
|
||||
readme = "README.md"
|
||||
authors = ["LangChain"]
|
||||
@@ -12,11 +12,12 @@ include = ["langserve/playground/dist/**/*"]
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.8.1"
|
||||
httpx = ">=0.23.0" # May be able to decrease this version
|
||||
fastapi = {version = ">=0.90.1", optional = true}
|
||||
fastapi = {version = ">=0.90.1,<1", optional = true}
|
||||
sse-starlette = {version = "^1.3.0", optional = true}
|
||||
httpx-sse = {version = ">=0.3.1", optional = true}
|
||||
pydantic = ">=1"
|
||||
langchain = ">=0.0.322"
|
||||
langchain = ">=0.0.333"
|
||||
orjson = ">=2"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
jupyterlab = "^3.6.1"
|
||||
@@ -37,6 +38,7 @@ pytest-asyncio = "^0.21.1"
|
||||
pytest-mock = "^3.11.1"
|
||||
pytest-socket = "^0.6.0"
|
||||
pytest-watch = "^4.2.0"
|
||||
pytest-timeout = "^2.2.0"
|
||||
|
||||
[tool.poetry.group.examples.dependencies]
|
||||
openai = "^0.28.0"
|
||||
@@ -83,3 +85,15 @@ omit = [
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
# --strict-markers will raise errors on unknown marks.
|
||||
# https://docs.pytest.org/en/7.1.x/how-to/mark.html#raising-errors-on-unknown-marks
|
||||
#
|
||||
# https://docs.pytest.org/en/7.1.x/reference/reference.html
|
||||
# --strict-config any warnings encountered while parsing the `pytest`
|
||||
# section of the configuration file raise errors.
|
||||
addopts = "--strict-markers --strict-config --durations=5 -vv"
|
||||
# Global timeout for all tests. There shuold be a good reason for a test to
|
||||
# take more than 5 seconds
|
||||
timeout = 5
|
||||
asyncio_mode = "auto"
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
|
||||
from langserve.callbacks import AsyncEventAggregatorCallback, replace_uuids
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_event_aggregator() -> None:
|
||||
"""Test that the event aggregator is aggregating events."""
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import pytest
|
||||
|
||||
from langserve.playground import _get_mimetype
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"file_extension, expected_mimetype",
|
||||
[
|
||||
("js", "application/javascript"),
|
||||
("css", "text/css"),
|
||||
("htm", "text/html"),
|
||||
("html", "text/html"),
|
||||
("txt", "text/plain"), # An example of an unknown extension using guess_type
|
||||
],
|
||||
)
|
||||
def test_get_mimetype(file_extension: str, expected_mimetype: str) -> None:
|
||||
# Create a filename with the given extension
|
||||
filename = f"test_file.{file_extension}"
|
||||
|
||||
# Call the _get_mimetype function with the test filename
|
||||
mimetype = _get_mimetype(filename)
|
||||
|
||||
# Check if the returned mimetype matches the expected one
|
||||
assert mimetype == expected_mimetype
|
||||
@@ -1,4 +1,6 @@
|
||||
import datetime
|
||||
import uuid
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
@@ -49,7 +51,7 @@ def test_serialization(data: Any) -> None:
|
||||
# Test encoding
|
||||
lc_serializer = WellKnownLCSerializer()
|
||||
|
||||
assert isinstance(lc_serializer.dumps(data), str)
|
||||
assert isinstance(lc_serializer.dumps(data), bytes)
|
||||
# Translate to python primitives and load back into object
|
||||
assert lc_serializer.loadd(lc_serializer.dumpd(data)) == data
|
||||
# Test simple equality (does not include pydantic class names)
|
||||
@@ -119,3 +121,65 @@ def _get_full_representation(data: Any) -> Any:
|
||||
def test_decode_events(data: Any, expected: Any) -> None:
|
||||
"""Test decoding events."""
|
||||
assert load_events(data) == expected
|
||||
|
||||
|
||||
class SimpleEnum(Enum):
|
||||
a = "a"
|
||||
b = "b"
|
||||
|
||||
|
||||
class SimpleModel(BaseModel):
|
||||
x: int
|
||||
y: SimpleEnum
|
||||
z: uuid.UUID
|
||||
dt: datetime.datetime
|
||||
d: datetime.date
|
||||
t: datetime.time
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"obj, expected",
|
||||
[
|
||||
({"key": "value"}, {"key": "value"}),
|
||||
([1, 2, 3], [1, 2, 3]),
|
||||
(123, 123),
|
||||
(uuid.UUID(int=1), "00000000-0000-0000-0000-000000000001"),
|
||||
(datetime.datetime(2020, 1, 1), "2020-01-01T00:00:00"),
|
||||
(datetime.date(2020, 1, 1), "2020-01-01"),
|
||||
(datetime.time(0, 0, 0), "00:00:00"),
|
||||
(datetime.time(0, 0, 0, 1), "00:00:00.000001"),
|
||||
(SimpleEnum.a, "a"),
|
||||
(
|
||||
SimpleModel(
|
||||
x=1,
|
||||
y=SimpleEnum.a,
|
||||
z=uuid.UUID(int=1),
|
||||
dt=datetime.datetime(2020, 1, 1),
|
||||
d=datetime.date(2020, 1, 1),
|
||||
t=datetime.time(0, 0, 0),
|
||||
),
|
||||
{
|
||||
"x": 1,
|
||||
"y": "a",
|
||||
"z": "00000000-0000-0000-0000-000000000001",
|
||||
"dt": "2020-01-01T00:00:00",
|
||||
"d": "2020-01-01",
|
||||
"t": "00:00:00",
|
||||
},
|
||||
),
|
||||
("string", "string"),
|
||||
(True, True),
|
||||
(None, None),
|
||||
],
|
||||
)
|
||||
def test_encoding_of_well_known_types(obj: Any, expected: str) -> None:
|
||||
"""Test encoding of well known types.
|
||||
|
||||
This tests verifies that our custom serializer is able to encode some well
|
||||
known types; e.g., uuid, datetime, date, time
|
||||
|
||||
It doesn't handle types like Decimal or frozenset etc just yet while we determine
|
||||
how to roll out a more complete solution.
|
||||
"""
|
||||
lc_serializer = WellKnownLCSerializer()
|
||||
assert lc_serializer.dumpd(obj) == expected
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@ from fastapi import Request
|
||||
from langchain.prompts import PromptTemplate
|
||||
from langchain.schema.runnable.utils import ConfigurableField
|
||||
|
||||
from langserve.server import _unpack_request_config
|
||||
from langserve.api_handler import _unpack_request_config
|
||||
|
||||
try:
|
||||
from pydantic.v1 import BaseModel, ValidationError
|
||||
@@ -140,7 +140,7 @@ def test_validation(test_case) -> None:
|
||||
model(**test_case)
|
||||
|
||||
|
||||
def test_invoke_request_with_runnables() -> None:
|
||||
async def test_invoke_request_with_runnables() -> None:
|
||||
"""Test that the invoke request model is created correctly."""
|
||||
runnable = PromptTemplate.from_template("say hello to {name}").configurable_fields(
|
||||
template=ConfigurableField(
|
||||
@@ -153,14 +153,15 @@ def test_invoke_request_with_runnables() -> None:
|
||||
Model = create_invoke_request_model("", runnable.input_schema, config)
|
||||
|
||||
assert (
|
||||
_unpack_request_config(
|
||||
await _unpack_request_config(
|
||||
Model(
|
||||
input={"name": "bob"},
|
||||
).config,
|
||||
keys=[],
|
||||
config_keys=[],
|
||||
model=config,
|
||||
request=MagicMock(Request),
|
||||
per_req_config_modifier=lambda x, y: x,
|
||||
server_config=None,
|
||||
)
|
||||
== {}
|
||||
)
|
||||
@@ -182,12 +183,13 @@ def test_invoke_request_with_runnables() -> None:
|
||||
"template": "goodbye {name}",
|
||||
}
|
||||
|
||||
assert _unpack_request_config(
|
||||
assert await _unpack_request_config(
|
||||
request.config,
|
||||
keys=["configurable"],
|
||||
config_keys=["configurable"],
|
||||
model=config,
|
||||
request=MagicMock(Request),
|
||||
per_req_config_modifier=lambda x, y: x,
|
||||
server_config=None,
|
||||
) == {
|
||||
"configurable": {"template": "goodbye {name}"},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user