Compare commits

..

125 Commits

Author SHA1 Message Date
Erick Friis aa3d7bb0a7 path bugfix 2023-10-23 16:35:32 -07:00
Erick Friis 26a4495a53 Add package routes (#87)
Co-authored-by: Eugene Yurtsev <eyurtsev@gmail.com>
2023-10-23 15:16:00 -07:00
Nuno Campos ff6f20d9e1 Remove usage of locals from example (#86)
Co-authored-by: Eugene Yurtsev <eyurtsev@gmail.com>
2023-10-23 13:33:52 -04:00
Eugene Yurtsev c448042d3d Start handling errors on stream/astream/astream_log (#83)
This PR introduces error handling for stream/astream/astream_log
endpoints.

The error handling is incomplete as client side errors aren't handled
yet.

1) When an exception is encountered while streaming the server sents an
error event in the stream with data:

- status code: int
- message: str

2) Client code is updated to handle this

In addition this PR, proposes that the client with log an error instead
of raising when encountering an unsupported event. This will allow us to
ship additional events without breaking older clients. Useful for sending
over callback events.
2023-10-20 15:43:58 -04:00
Eugene Yurtsev 0db97cf565 Update readme (#81)
Update readme
2023-10-20 09:16:31 -04:00
Eugene Yurtsev ea6b3867f6 Account for custom input and output types (#56)
Account for custom input and output types
2023-10-19 22:23:09 -04:00
Nuno Campos 3027ecab3d 0.0.15 (#77) 2023-10-19 20:07:13 +01:00
Nuno Campos 80108338db 0.0.14 (#74) 2023-10-19 20:03:04 +01:00
Nuno Campos a4497911fd Refetch input schema on config change aka. support configurable prompts (#66) 2023-10-19 20:01:41 +01:00
Nuno Campos b93ccf9b90 Add documentation for playground (#73) 2023-10-19 18:30:14 +01:00
Nuno Campos 9f7d672d20 Add back char (#72) 2023-10-19 17:59:47 +01:00
Nuno Campos 4ba226e40a Revert example (#71) 2023-10-19 17:43:46 +01:00
Nuno Campos c3f553bc3b 0.0.13 (#69) 2023-10-19 17:30:54 +01:00
Nuno Campos 4cbc20f6c4 Fix non-dict input (#70) 2023-10-19 17:30:41 +01:00
David Duong 5e00bfee38 Playground Cleanup (#63)
- Autoexpanding textareas
- Add missing chevrons
- Align dark mode styles, spacing for nested JSON

---------

Co-authored-by: Nuno Campos <nuno@boringbits.io>
2023-10-19 17:11:11 +01:00
Nuno Campos c4565e85b0 0.0.12 (#68) 2023-10-19 17:04:08 +01:00
Nuno Campos 65709a7966 Rebuild files (#67) 2023-10-19 17:01:34 +01:00
Jacob Lee 05afa7fb07 Lightmode fix, add borders to nested inputs (#64) 2023-10-19 16:59:05 +01:00
Eugene Yurtsev 23827b3f5c Version 0.0.11 (#62) 2023-10-19 11:01:50 -04:00
Nuno Campos c50ec01632 Merge pull request #19 from langchain-ai/nc/playground
Nc/playground
2023-10-19 15:58:36 +01:00
Nuno Campos 174f47f4f7 Update built files 2023-10-19 15:36:01 +01:00
Nuno Campos 6ef8a25f71 Update built files 2023-10-19 15:34:47 +01:00
Eugene Yurtsev db1016da62 Allow prerelease from any branch (#61)
Allow prerelease to test pypi from any branch
2023-10-19 10:32:03 -04:00
Eugene Yurtsev b0935f0266 Add publish to test pypi (#59)
Add release to test pypi
2023-10-19 10:22:27 -04:00
Tat Dat Duong 20f04581cc Fix dark mode for icons 2023-10-19 13:47:37 +02:00
Tat Dat Duong 5ee72a5f24 Initial work on cleaning up styles 2023-10-19 13:11:11 +02:00
Nuno Campos 14b9c5f745 Lint 2023-10-19 11:55:45 +01:00
Nuno Campos 020f4e7ae7 Add json cell 2023-10-19 11:52:04 +01:00
Nuno Campos 33cf1f322f Resolve all refs before passing to jsonforms 2023-10-19 11:26:00 +01:00
Nuno Campos d800c100bf Copy changes 2023-10-19 10:39:00 +01:00
Nuno Campos 063e10f755 Resolve maximum call stack size exceeded for schemas with root $ref 2023-10-19 10:31:16 +01:00
Tat Dat Duong 001c924e4e Cleanup styles 2023-10-19 10:40:38 +02:00
jacoblee93 37f4f3c599 Update test case 2023-10-19 10:40:38 +02:00
jacoblee93 a54d7df974 Adds custom array and text controls 2023-10-19 10:40:38 +02:00
Eugene Yurtsev 4878d832cf Minor fix in contribution guidelines (#55)
Minor fix
2023-10-19 10:40:38 +02:00
Eugene Yurtsev 24d96b7cac Add CONTRIBUTING.md guidelines (#54)
Add contributing.md. Inserting this in the root of the project, so it's
easier to find. If folks feel strongly can put in in .github.
2023-10-19 10:40:38 +02:00
Nuno Campos 05f11af6e9 Update default to match langchain (#51)
Co-authored-by: jacoblee93 <jacoblee93@gmail.com>
Co-authored-by: Eugene Yurtsev <eyurtsev@gmail.com>
2023-10-19 10:40:38 +02:00
Eugene Yurtsev 144eb0717e Version 0.0.10 bump (#50)
Bump
2023-10-19 10:40:38 +02:00
Tat Dat Duong bd5d8cda86 Fix dark theme for errors, move validation stuff to the top-right 2023-10-19 09:04:38 +02:00
Tat Dat Duong ca06ab2e32 Refresh check icon 2023-10-19 08:45:06 +02:00
Tat Dat Duong 3df9fe8f28 Add better validation error styles 2023-10-19 08:35:57 +02:00
Tat Dat Duong d37fce3f57 Add separator 2023-10-19 08:09:30 +02:00
Nuno Campos 62106399dc Do not apply defaults for input schema 2023-10-18 21:24:05 +01:00
Nuno Campos cc8d69eae1 Now in playground too 2023-10-18 20:19:24 +01:00
Nuno Campos 20f2f0135a oops 2023-10-18 20:11:18 +01:00
Nuno Campos 7225efe27b Use input type 2023-10-18 20:08:35 +01:00
Nuno Campos a211178fa6 New build 2023-10-18 19:31:12 +01:00
Tat Dat Duong c4e1a3154b New build 2023-10-18 18:50:12 +02:00
Tat Dat Duong ee451b143c Handle value sync 2023-10-18 17:47:56 +02:00
Tat Dat Duong dc23a3c27c Add initial version of embeddable SDK 2023-10-18 17:00:47 +02:00
Tat Dat Duong acef4d78e7 Add iframe handling 2023-10-18 17:00:47 +02:00
Nuno Campos 4208e5de26 Add new built files 2023-10-18 15:15:18 +01:00
Nuno Campos 9d4d56adab Fix serving of non-text files 2023-10-18 15:14:44 +01:00
Tat Dat Duong 73ff692be7 Hack the overdrag 2023-10-18 15:04:56 +02:00
Tat Dat Duong fd01b78c01 Pointer events, use correct color 2023-10-18 14:53:24 +02:00
Tat Dat Duong cc6484de5c Add code snippets 2023-10-18 14:49:28 +02:00
Tat Dat Duong 1bf88e8904 Cleanup share 2023-10-18 14:33:59 +02:00
Tat Dat Duong 0a8538c743 Cleanup styles 2023-10-18 14:22:44 +02:00
Tat Dat Duong 43a0de08fe Replace icon, use share button 2023-10-18 13:56:46 +02:00
Nuno Campos bd884736ba apply config in schemas endpoints 2023-10-18 10:50:35 +01:00
Eugene Yurtsev 6eeb274875 Minor fix in contribution guidelines (#55)
Minor fix
2023-10-17 21:46:54 -04:00
Eugene Yurtsev 779b8e1cc9 Add CONTRIBUTING.md guidelines (#54)
Add contributing.md. Inserting this in the root of the project, so it's
easier to find. If folks feel strongly can put in in .github.
2023-10-17 21:45:10 -04:00
Nuno Campos ada5d103b4 Update default to match langchain (#51)
Co-authored-by: jacoblee93 <jacoblee93@gmail.com>
Co-authored-by: Eugene Yurtsev <eyurtsev@gmail.com>
2023-10-17 21:20:44 -04:00
Nuno Campos 9b5fe1471c Add built files 2023-10-17 22:41:41 +01:00
Nuno Campos c0ade4707e Update url, fix defaults 2023-10-17 22:41:12 +01:00
Eugene Yurtsev fb0f9a0dc3 Version 0.0.10 bump (#50)
Bump
2023-10-17 14:42:27 -04:00
Tat Dat Duong e47494cf5b Further style changes 2023-10-17 20:08:38 +02:00
Tat Dat Duong dfab212e5b Temporary change 2023-10-17 20:01:12 +02:00
Nuno Campos 5b2ee137a2 Merge branch 'main' into nc/playground 2023-10-17 18:59:53 +01:00
Nuno Campos 786b7e3a93 Update path and api docs 2023-10-17 18:57:50 +01:00
Eugene Yurtsev 972b99ddbf Temporary workaround for generating unique model names (#47)
If two schema different schemas share the same name, fastapi openapi
documentation fails.

This is a temporary work-around for openapi spec generation purposes. It
will work only for top level model names.

We've bumped into this issue while hosting 2 runnables that start with a
PromptTemplate (same name for the schema), but have different template
variables (so the schema are different).

We'll need to find a better solution to handle nested models. This
should be good for now as it'll allow users to deploy the common case
correctly.

---------

Co-authored-by: Erick Friis <erickfriis@gmail.com>
2023-10-17 13:52:30 -04:00
Nuno Campos eb0e97f8a1 Lint 2023-10-17 18:29:26 +01:00
Nuno Campos 4b2c89386f Lock 2023-10-17 18:28:12 +01:00
Nuno Campos 85320dc2cb hide from schema 2023-10-17 18:23:42 +01:00
Tat Dat Duong ce9161d02c Copy to clipboard, parse the contents 2023-10-17 18:16:27 +01:00
Tat Dat Duong 2d9e756fa6 Add share button 2023-10-17 18:16:27 +01:00
Tat Dat Duong d2b9570bd3 Fix dark mode 2023-10-17 18:16:21 +01:00
Tat Dat Duong fd8cfc327b Rough draft of styles 2023-10-17 18:16:02 +01:00
Tat Dat Duong 5f1b9c800e Use 8000 for every example, resolve playground from every path 2023-10-17 18:16:02 +01:00
Tat Dat Duong 8c9142948e Add makefile command 2023-10-17 18:16:02 +01:00
Tat Dat Duong 44e8d7bc4d Move playground to langserve folder 2023-10-17 18:16:02 +01:00
Tat Dat Duong 30a0f70bdf Remove unused asset 2023-10-17 18:16:02 +01:00
Tat Dat Duong ae51653a9c Force every input to be multiline 2023-10-17 18:16:02 +01:00
Tat Dat Duong e3d2c5f698 Fix types for schema 2023-10-17 18:16:02 +01:00
Nuno Campos a150be31c2 Try to fix reload on error 2023-10-17 18:16:02 +01:00
Nuno Campos 7d4bcdc18a Lint 2023-10-17 18:16:02 +01:00
Nuno Campos 8c3aba7475 Lint 2023-10-17 18:15:40 +01:00
Nuno Campos b315ed781b Update lock file 2023-10-17 18:15:40 +01:00
Nuno Campos e5dd75508c Add /playground endpoint that serves an playground UI with support for configuration and intermediate steps 2023-10-17 18:15:22 +01:00
Nuno Campos 32d4ad89e3 Add endpoints scoped to a config hash encoded in url (#49) 2023-10-17 18:11:45 +01:00
Eugene Yurtsev 1902209c35 Bump to version 0.0.9 (#48)
Bump to version 0.0.9
2023-10-16 21:31:10 -04:00
Peter Krenesky e819e9c905 Add auth options for RemoteRunnable (#46)
This PR adds support for most authentication scenarios to
`RemoteRunnable`. It does this by exposing auth kwargs available in the
`httpx` clients.

From the [httpx
source](https://github.com/encode/httpx/blob/master/httpx/_client.py#L582):
* **auth** - *(optional)* An authentication class to use when sending
requests.
* **headers** - *(optional)* Dictionary of HTTP headers to include when
sending requests.
* **cookies** - *(optional)* Dictionary of Cookie items to include when
sending requests.
* **verify** - *(optional)* SSL certificates (a.k.a CA bundle) used to
verify the identity of requested hosts. Either `True` (default CA
bundle), a path to an SSL certificate file, an `ssl.SSLContext`, or
`False` (which will disable verification).
* **cert** - *(optional)* An SSL certificate used by the requested host
to authenticate the client. Either a path to an SSL certificate file, or
two-tuple of (certificate file, key file), or a three-tuple of
(certificate file, key file, password).

---------

Co-authored-by: Eugene Yurtsev <eyurtsev@gmail.com>
2023-10-16 21:29:03 -04:00
Eugene Yurtsev 3819dfba72 Version 0.0.8 release (#43)
Version 0.0.8 release
2023-10-12 13:10:06 -04:00
Eugene Yurtsev 334f087709 Bumping to langchain 0.0.313 (#42)
Bump to langchain 0.0.313
2023-10-12 13:08:19 -04:00
Eugene Yurtsev 4cbc39765e Add client examples (#41)
Add client examples
2023-10-12 09:56:36 -04:00
Eugene Yurtsev a2966fa661 Bump min version of langchain (#40)
Update lock file
2023-10-12 09:56:28 -04:00
Eugene Yurtsev 5216b34b82 Add langsmith instructions links (#39)
Add langsmith instructions
2023-10-12 09:25:35 -04:00
Nuno Campos 034b9d1a29 Add deployment instructions (#37) 2023-10-12 09:21:03 -04:00
Nuno Campos b3e791838a Update README.md (#36) 2023-10-12 09:20:50 -04:00
Eugene Yurtsev 887565db26 Link to JS Remote Runnable in README (#35)
Add link to JS remote runnable in readme
2023-10-11 21:45:24 -04:00
Eugene Yurtsev bb8ac0f7b8 Add Agent Action/Finish messages to well known types (#32)
Add agent related messages to well known types
2023-10-11 21:39:33 -04:00
Harrison Chase c44e3bf200 add requests to notebook (#34) 2023-10-11 21:38:49 -04:00
Harrison Chase 06fec4868b add agent examples (#33) 2023-10-11 17:06:01 -07:00
Harrison Chase 906e62e5a5 add warning for legacy chains (#31) 2023-10-11 17:58:52 -04:00
Harrison Chase 00fdca8d11 add conversational retrieval chain (#30) 2023-10-11 16:57:27 -04:00
Eugene Yurtsev 0993834cda Document LangChain CLI (#29)
Document bootstrap using langchain cli
2023-10-11 16:37:40 -04:00
Harrison Chase 3849342876 Delete examples/retrieval/Untitled.ipynb (#28) 2023-10-11 16:30:05 -04:00
Eugene Yurtsev d2dc4a9292 Add more docs, disable **kwargs (#26)
* Disable kwargs (since we're not yet doing validation on them)
* Add more in code docs
2023-10-11 15:48:39 -04:00
Eugene Yurtsev 5506675152 Add more documentation to README (#27)
Add more documentation to readme
2023-10-11 15:44:14 -04:00
Eugene Yurtsev 5f3042861a Add tracing information (#24)
Add some debug information for tracing by default server side.
2023-10-11 15:12:36 -04:00
Eugene Yurtsev 6ec7ceadc2 Add in code documentation (#25)
Add some in code documentation
2023-10-11 14:55:33 -04:00
Eugene Yurtsev 90ec2e1032 Add output type schema to InvokeResponse and BatchResponse (#23)
Add output type information to invoke and batch responses
2023-10-10 16:28:57 -04:00
Eugene Yurtsev 54c89a2ff7 Create release v0.0.7 (#22)
Release version 0.0.7
2023-10-06 15:22:47 -04:00
Eugene Yurtsev 59390016d8 Bump langchain, update api (#20)
update code to reflect langchain changes
2023-10-05 22:05:32 -04:00
Eugene Yurtsev 08c3dd9231 Fix lint, remove unused deps (#18)
fix lint
2023-10-04 17:18:53 -04:00
Eugene Yurtsev d3dd8e0567 Simplify examples (#17)
Simplify some examples
2023-10-04 15:52:33 -04:00
Eugene Yurtsev 9462a8ac32 Version 0.0.6 (#16)
Version 0.0.6
2023-10-04 13:02:15 -04:00
Eugene Yurtsev a4ac5b6f40 Add tests for configurable runnables (#15)
Add tests for configuration
2023-10-04 12:59:09 -04:00
Eugene Yurtsev f973c6af3c Add input/output/config endpoints to server and test out configurability of runnables (#9)
Code is in a working state. Potentially some minor kinks. Dependent on
features on master, so should not be landed prior to master being
released
2023-10-04 11:31:15 -04:00
Predrag Gruevski 48baf90ca1 Add SECURITY.md file that documents the repo security policies. (#14)
Adding this file populates the GitHub Security tab's security policy
field, letting users know where to report issues.
2023-10-04 11:00:42 -04:00
Eugene Yurtsev e1da4040bd Update semver syntax (#13)
Poetry seems to be having issues with dep resolution when using former syntax
2023-10-03 15:49:50 -04:00
Eugene Yurtsev 298ae9605f Version 0.0.4 (#12)
Pick up changes from langchain for serializing chunked messages
2023-10-03 15:16:06 -04:00
Eugene Yurtsev 001e165b8f Bump langchain min version and add fix for chunked messages (#11)
Bump langchain to pick up fix for chunked messages
2023-10-03 15:03:44 -04:00
Eugene Yurtsev 72607daec3 Relax constraints (#8)
Relax constraints
2023-10-02 16:27:38 -04:00
Eugene Yurtsev 9b87d317f7 Update README.md (#7)
Update readme
2023-10-02 14:30:26 -04:00
91 changed files with 22035 additions and 1283 deletions
+49
View File
@@ -0,0 +1,49 @@
name: test-release
on:
workflow_call:
inputs:
working-directory:
required: true
type: string
description: "From which folder this pipeline executes"
env:
POETRY_VERSION: "1.5.1"
jobs:
publish_to_test_pypi:
runs-on: ubuntu-latest
permissions:
# This permission is used for trusted publishing:
# https://blog.pypi.org/posts/2023-04-20-introducing-trusted-publishers/
#
# Trusted publishing has to also be configured on PyPI for each package:
# https://docs.pypi.org/trusted-publishers/adding-a-publisher/
id-token: write
defaults:
run:
working-directory: ${{ inputs.working-directory }}
steps:
- uses: actions/checkout@v3
- name: Set up Python + Poetry ${{ env.POETRY_VERSION }}
uses: "./.github/actions/poetry_setup"
with:
python-version: "3.10"
poetry-version: ${{ env.POETRY_VERSION }}
working-directory: ${{ inputs.working-directory }}
cache-key: release
- name: Build project for distribution
run: poetry build
- name: Check Version
id: check-version
run: |
echo version=$(poetry version --short) >> $GITHUB_OUTPUT
- name: Publish package to TestPyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/
verbose: true
print-hash: true
+1
View File
@@ -11,6 +11,7 @@ on:
- '.github/workflows/_test.yml'
- '.github/workflows/langserve_ci.yml'
- 'langserve/**'
- 'examples/**'
- 'pyproject.toml'
- 'Makefile'
workflow_dispatch: # Allows to trigger the workflow manually in GitHub UI
@@ -0,0 +1,13 @@
---
name: Test Release
on:
workflow_dispatch: # Allows to trigger the workflow manually in GitHub UI
jobs:
release:
uses:
./.github/workflows/_test_release.yml
with:
working-directory: .
secrets: inherit
+2
View File
@@ -158,3 +158,5 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
.envrc
+51
View File
@@ -0,0 +1,51 @@
# Contributing
## Contributor License Agreement
We are grateful to the contributors who help evolve LangServe and dedicate their time to the project. As the primary sponsor of LangServe, LangChain, Inc. aims to build products in the open that benefit thousands of developers while allowing us to build a sustainable business. For all code contributions to LangServe, we ask that contributors complete and sign a Contributor License Agreement (“CLA”). The agreement between contributors and the project is explicit, so LangServe users can be confident in the legal status of the source code and their right to use it.The CLA does not change the terms of the underlying license, LangServe License, used by our software.
Before you can contribute to LangServe, a bot will comment on the PR asking you to agree to the CLA if you haven't already. Agreeing to the CLA is required before code can be merged and only needs to happen on the first contribution to the project. All subsequent contributions will fall under the same CLA.
## 🗺️ Guidelines
### Dependency Management: Poetry and other env/dependency managers
This project uses [Poetry](https://python-poetry.org/) v1.6.1+ as a dependency manager.
### Local Development Dependencies
Install langserve development requirements (for running langchain, running examples, linting, formatting, tests, and coverage):
```sh
poetry install --with test,dev
```
Then verify that tests pass:
```sh
make test
```
### Formatting and Linting
Run these locally before submitting a PR; the CI system will check also.
#### Code Formatting
Formatting for this project is done via a combination of [Black](https://black.readthedocs.io/en/stable/) and [ruff](https://docs.astral.sh/ruff/rules/).
To run formatting for this project:
```sh
make format
```
#### Linting
Linting for this project is done via a combination of [Black](https://black.readthedocs.io/en/stable/), [ruff](https://docs.astral.sh/ruff/rules/), and [mypy](http://mypy-lang.org/).
To run linting for this project:
```sh
make lint
```
+9
View File
@@ -3,6 +3,12 @@
# Default target executed when no arguments are given to make.
all: help
build-playground:
cd ./langserve/playground && yarn build
build: build-playground
poetry build
######################
# TESTING AND COVERAGE
######################
@@ -13,6 +19,9 @@ TEST_FILE ?= tests/unit_tests/
test:
poetry run pytest --disable-socket --allow-unix-socket $(TEST_FILE)
test_watch:
poetry run ptw . -- $(TEST_FILE)
######################
# LINTING AND FORMATTING
######################
+153 -35
View File
@@ -1,9 +1,42 @@
# LangServe 🦜️🔗
# LangServe 🦜️🔗
## Overview
`LangServe` is a library that allows developers to host their Langchain runnables /
call into them remotely from a runnable interface.
`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.
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
- 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)
### 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)
## 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` installed
and also `typer`. (`pip install langchain typer` or `pip install "langchain[cli]"`)
```sh
langchain ../path/to/directory
```
And follow the instructions...
## Examples
@@ -11,13 +44,15 @@ For more examples, see the [examples](./examples) directory.
### Server
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
#!/usr/bin/env python
from fastapi import FastAPI
from langchain.prompts import ChatPromptTemplate
from langchain.chat_models import ChatAnthropic, ChatOpenAI
from langserve import add_routes
from typing_extensions import TypedDict
app = FastAPI(
@@ -26,35 +61,25 @@ app = FastAPI(
description="A simple api server using Langchain's Runnable interfaces",
)
# Serve Open AI and Anthropic models
LLMInput = Union[List[Union[SystemMessage, HumanMessage, str]], str]
add_routes(
app,
ChatOpenAI(),
path="/openai",
input_type=LLMInput,
config_keys=[],
)
add_routes(
app,
ChatAnthropic(),
path="/anthropic",
input_type=LLMInput,
config_keys=[],
)
# Serve a joke chain
class ChainInput(TypedDict):
"""The input to the chain."""
topic: str
"""The topic of the joke."""
model = ChatAnthropic()
prompt = ChatPromptTemplate.from_template("tell me a joke about {topic}")
add_routes(app, prompt | model, path="/chain", input_type=ChainInput)
add_routes(
app,
prompt | model,
path="/chain",
)
if __name__ == "__main__":
import uvicorn
@@ -62,9 +87,26 @@ if __name__ == "__main__":
uvicorn.run(app, host="localhost", port=8000)
```
### Docs
If you've deployed the server above, you can view the generated OpenAPI docs using:
```sh
curl localhost:8000/docs
```
make sure to **add** the `/docs` suffix.
Below will return a 404 until you define a `@app.get("/")`
```sh
localhost:8000
```
### Client
Python SDK
```python
from langchain.schema import SystemMessage, HumanMessage
@@ -82,18 +124,18 @@ joke_chain.invoke({"topic": "parrots"})
await joke_chain.ainvoke({"topic": "parrots"})
prompt = [
SystemMessage(content='Act like either a cat or a parrot.'),
SystemMessage(content='Act like either a cat or a parrot.'),
HumanMessage(content='Hello!')
]
# Supports astream
async for msg in anthropic.astream(prompt):
print(msg, end="", flush=True)
prompt = ChatPromptTemplate.from_messages(
[("system", "Tell me a long story about {topic}")]
)
# Can define custom chains
chain = prompt | RunnableMap({
"openai": openai,
@@ -103,22 +145,98 @@ chain = prompt | RunnableMap({
chain.batch([{ "topic": "parrots" }, { "topic": "cats" }])
```
## Installation
In TypeScript (requires LangChain.js version 0.0.166 or later):
```bash
# pip install langserve[all] -- has not been published to pypi yet
```typescript
import { RemoteRunnable } from "langchain/runnables/remote";
const chain = new RemoteRunnable({
url: `http://localhost:8000/chain/invoke/`,
});
const result = await chain.invoke({
topic: "cats",
});
```
or use `client` extra for client code, and `server` extra for server code.
Python using `requests`:
## Features
```python
import requests
response = requests.post(
"http://localhost:8000/chain/invoke/",
json={'input': {'topic': 'cats'}}
)
response.json()
```
- Deploy runnables with FastAPI
- Client can use remote runnables almost as if they were local
- Supports async
- Supports batch
- Supports stream
You can also use `curl`:
### Limitations
```sh
curl --location --request POST 'http://localhost:8000/chain/invoke/' \
--header 'Content-Type: application/json' \
--data-raw '{
"input": {
"topic": "cats"
}
}'
```
- Chain callbacks cannot be passed from the client to the server
## Endpoints
The following code:
```python
...
add_routes(
app,
runnable,
path="/my_runnable",
)
```
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
- `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
## 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.
## 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.
## 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.
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/).
## Deployment
### Deploy to GCP
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
```
+6
View File
@@ -0,0 +1,6 @@
# Security Policy
## Reporting a Vulnerability
Please report security vulnerabilities by email to `security@langchain.dev`.
This email is an alias to a subset of our maintainers, and will ensure the issue is promptly triaged and acted upon as needed.
+144
View File
@@ -0,0 +1,144 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Client\n",
"\n",
"Demo of a client interacting with a remote agent. "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"You can interact with this via API directly"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'output': {'output': 'Eugene thinks that cats like fish.'}}"
]
},
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"import requests\n",
"\n",
"inputs = {\"input\": {\"input\": \"what does eugene think of cats?\"}}\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": 3,
"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": 4,
"metadata": {
"tags": []
},
"outputs": [
{
"data": {
"text/plain": [
"{'output': 'Hello! How can I assist you today?'}"
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"await remote_runnable.ainvoke({\"input\": \"hi!\"})"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {
"tags": []
},
"outputs": [
{
"data": {
"text/plain": [
"{'output': 'Eugene thinks that cats like fish.'}"
]
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"remote_runnable.invoke({\"input\": \"what does eugene think of cats?\"})"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.1"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
+82
View File
@@ -0,0 +1,82 @@
#!/usr/bin/env python
"""Example LangChain server exposes a conversational retrieval chain."""
from fastapi import FastAPI
from langchain.agents import AgentExecutor, tool
from langchain.agents.format_scratchpad import format_to_openai_functions
from langchain.agents.output_parsers import OpenAIFunctionsAgentOutputParser
from langchain.chat_models import ChatOpenAI
from langchain.embeddings import OpenAIEmbeddings
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain.tools.render import format_tool_to_openai_function
from langchain.vectorstores import FAISS
from pydantic import BaseModel
from langserve import add_routes
vectorstore = FAISS.from_texts(
["cats like fish", "dogs like sticks"], embedding=OpenAIEmbeddings()
)
retriever = vectorstore.as_retriever()
@tool
def get_eugene_thoughts(query: str) -> list:
"""Returns Eugene's thoughts on a topic."""
return retriever.get_relevant_documents(query)
tools = [get_eugene_thoughts]
prompt = ChatPromptTemplate.from_messages(
[
("system", "You are a helpful assistant."),
("user", "{input}"),
MessagesPlaceholder(variable_name="agent_scratchpad"),
]
)
llm = ChatOpenAI()
llm_with_tools = llm.bind(functions=[format_tool_to_openai_function(t) for t in tools])
agent = (
{
"input": lambda x: x["input"],
"agent_scratchpad": lambda x: format_to_openai_functions(
x["intermediate_steps"]
),
}
| prompt
| llm_with_tools
| OpenAIFunctionsAgentOutputParser()
)
agent_executor = AgentExecutor(agent=agent, tools=tools)
app = FastAPI(
title="LangChain Server",
version="1.0",
description="Spin up a simple api server using Langchain's Runnable interfaces",
)
# We need to add these input/output schemas because the current AgentExecutor
# is lacking in schemas.
class Input(BaseModel):
input: str
class Output(BaseModel):
output: str
# Adds routes to the app for using the chain under:
# /invoke
# /batch
# /stream
add_routes(app, agent_executor, input_type=Input, output_type=Output)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="localhost", port=8000)
+39 -11
View File
@@ -10,17 +10,45 @@
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {
"tags": []
},
"outputs": [],
"cell_type": "markdown",
"metadata": {},
"source": [
"from langchain.prompts.chat import (\n",
" HumanMessagePromptTemplate,\n",
" SystemMessagePromptTemplate,\n",
")"
"You can interact with this via API directly"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'output': {'content': \"Why don't scientists trust atoms when playing sports? \\n\\nBecause they make up everything!\",\n",
" 'additional_kwargs': {},\n",
" 'type': 'ai',\n",
" 'example': False}}"
]
},
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"import requests\n",
"\n",
"inputs = {\"input\": {\"topic\": \"sports\"}}\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)"
]
},
{
@@ -155,7 +183,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.4"
"version": "3.10.1"
}
},
"nbformat": 4,
+42 -8
View File
@@ -1,15 +1,33 @@
#!/usr/bin/env python
"""Example LangChain server exposes a chain composed of a prompt and an LLM."""
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from langchain.chat_models import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
from typing_extensions import TypedDict
from langchain.prompts import PromptTemplate
# from typing_extensions import TypedDict
from langchain.pydantic_v1 import BaseModel
from langchain.schema.output_parser import StrOutputParser
from langchain.schema.runnable import ConfigurableField
from langserve import add_routes
model = ChatOpenAI()
prompt = ChatPromptTemplate.from_template("tell me a joke about {topic}")
chain = prompt | model
model = ChatOpenAI(temperature=0.5).configurable_alternatives(
ConfigurableField(id="llm", name="LLM"),
high_temp=ChatOpenAI(temperature=0.9),
low_temp=ChatOpenAI(temperature=0.1, max_tokens=1),
default_key="medium_temp",
)
prompt = PromptTemplate.from_template(
"tell me a joke about {topic}."
).configurable_fields(
template=ConfigurableField(
id="prompt",
name="Prompt",
description="The prompt to use. Must contain {topic}",
)
)
chain = prompt | model | StrOutputParser()
app = FastAPI(
title="LangChain Server",
@@ -17,15 +35,31 @@ app = FastAPI(
description="Spin up a simple api server using Langchain's Runnable interfaces",
)
# Set all CORS enabled origins
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
expose_headers=["*"],
)
class ChainInput(TypedDict):
# The input type is automatically inferred from the runnable
# interface; however, if you want to override it, you can do so
# by passing in the input_type argument to add_routes.
class ChainInput(BaseModel):
"""The input to the chain."""
topic: str
"""The topic of the joke."""
add_routes(app, chain, input_type=ChainInput)
add_routes(app, chain, input_type=ChainInput, config_keys=["configurable"])
# Alternatively, you can rely on langchain's type inference
# to infer the input type from the runnable interface.
# add_routes(app, chain)
if __name__ == "__main__":
import uvicorn
@@ -0,0 +1,146 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Client\n",
"\n",
"Demo of a client interacting with a remote conversational retrieval chain. "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"You can interact with this via API directly"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'output': {'answer': 'Cats like fish.'}}"
]
},
"execution_count": 2,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"import requests\n",
"\n",
"inputs = {\"input\": {\"question\": \"what do cats like?\", \"chat_history\": \"\"}}\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": 6,
"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": 7,
"metadata": {
"tags": []
},
"outputs": [
{
"data": {
"text/plain": [
"{'answer': 'Cats like fish.'}"
]
},
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"await remote_runnable.ainvoke({\"question\": \"what do cats like?\", \"chat_history\": \"\"})"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {
"tags": []
},
"outputs": [
{
"data": {
"text/plain": [
"{'answer': 'Cats like fish.'}"
]
},
"execution_count": 10,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"await remote_runnable.ainvoke(\n",
" {\"question\": \"what do cats like?\", \"chat_history\": [(\"hi\", \"hi\")]}\n",
")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.1"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env python
"""Example LangChain server exposes a conversational retrieval chain."""
from fastapi import FastAPI
from langchain.chains import ConversationalRetrievalChain
from langchain.chat_models import ChatOpenAI
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import FAISS
from langserve import add_routes
vectorstore = FAISS.from_texts(
["cats like fish", "dogs like sticks"], embedding=OpenAIEmbeddings()
)
retriever = vectorstore.as_retriever()
model = ChatOpenAI()
chain = ConversationalRetrievalChain.from_llm(model, retriever)
app = FastAPI(
title="LangChain Server",
version="1.0",
description="Spin up a simple api server using Langchain's Runnable interfaces",
)
# Adds routes to the app for using the chain under:
# /invoke
# /batch
# /stream
add_routes(app, chain)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="localhost", port=8000)
+4 -9
View File
@@ -229,7 +229,7 @@
"metadata": {},
"outputs": [],
"source": [
"from langchain.schema.runnable import PutLocalVar, GetLocalVar"
"from langchain.schema.runnable import RunnablePassthrough"
]
},
{
@@ -266,13 +266,8 @@
")\n",
"\n",
"\n",
"chain = (\n",
" comedian_chain\n",
" | PutLocalVar(\"joke\")\n",
" | {\"joke\": GetLocalVar(\"joke\")}\n",
" | joke_classifier_chain\n",
" | PutLocalVar(\"classification\")\n",
" | {\"joke\": GetLocalVar(\"joke\"), \"classification\": GetLocalVar(\"classification\")}\n",
"chain = {\"joke\": comedian_chain} | RunnablePassthrough.assign(\n",
" classification=joke_classifier_chain\n",
")"
]
},
@@ -316,7 +311,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.4"
"version": "3.10.1"
}
},
"nbformat": 4,
-9
View File
@@ -1,11 +1,8 @@
#!/usr/bin/env python
"""Example LangChain server exposes multiple runnables (LLMs in this case)."""
from typing import List, Union
from fastapi import FastAPI
from langchain.chat_models import ChatAnthropic, ChatOpenAI
from langchain.prompts.chat import ChatPromptValue
from langchain.schema.messages import HumanMessage, SystemMessage
from langserve import add_routes
@@ -15,21 +12,15 @@ app = FastAPI(
description="Spin up a simple api server using Langchain's Runnable interfaces",
)
LLMInput = Union[List[Union[SystemMessage, HumanMessage, str]], str, ChatPromptValue]
add_routes(
app,
ChatOpenAI(),
path="/openai",
input_type=LLMInput,
config_keys=[],
)
add_routes(
app,
ChatAnthropic(),
path="/anthropic",
input_type=LLMInput,
config_keys=[],
)
if __name__ == "__main__":
-33
View File
@@ -1,33 +0,0 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "797be11c-0205-4f99-bbc8-f944ea19436f",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.4"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
+45 -3
View File
@@ -9,6 +9,48 @@
"Demo of a client interacting with a remote retriever. "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"You can interact with this via API directly"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'output': [{'page_content': 'dogs like sticks',\n",
" 'metadata': {},\n",
" 'type': 'Document'},\n",
" {'page_content': 'cats like fish', 'metadata': {}, 'type': 'Document'}]}"
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"import requests\n",
"\n",
"inputs = {\"input\": \"tree\"}\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": 1,
@@ -39,8 +81,8 @@
{
"data": {
"text/plain": [
"[Document(page_content='dogs like sticks', metadata={}),\n",
" Document(page_content='cats like fish', metadata={})]"
"[Document(page_content='dogs like sticks'),\n",
" Document(page_content='cats like fish')]"
]
},
"execution_count": 2,
@@ -182,7 +224,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.4"
"version": "3.10.1"
}
},
"nbformat": 4,
+2 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python
"""Example LangChain server exposes a retrieval."""
"""Example LangChain server exposes a retriever."""
from fastapi import FastAPI
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import FAISS
@@ -20,7 +20,7 @@ app = FastAPI(
# /invoke
# /batch
# /stream
add_routes(app, retriever, input_type=str)
add_routes(app, retriever)
if __name__ == "__main__":
import uvicorn
+2 -10
View File
@@ -1,15 +1,7 @@
"""Main entrypoint into package."""
from importlib import metadata
from langserve.client import RemoteRunnable
from langserve.server import add_routes
from langserve.version import __version__
__all__ = ["RemoteRunnable", "add_routes"]
try:
__version__ = metadata.version(__package__)
except metadata.PackageNotFoundError:
# Case where package metadata is not available.
__version__ = ""
del metadata # optional, avoids polluting the results of dir(__package__)
__all__ = ["RemoteRunnable", "add_routes", "__version__"]
+110 -8
View File
@@ -1,12 +1,25 @@
from __future__ import annotations
import asyncio
import json
import logging
import weakref
from concurrent.futures import ThreadPoolExecutor
from typing import Any, AsyncIterator, Iterator, List, Optional, Sequence, Union
from functools import lru_cache
from typing import (
Any,
AsyncIterator,
Dict,
Iterator,
List,
Optional,
Sequence,
Union,
)
from urllib.parse import urljoin
import httpx
from httpx._types import AuthTypes, CertTypes, CookieTypes, HeaderTypes, VerifyTypes
from langchain.callbacks.tracers.log_stream import RunLogPatch
from langchain.load.dump import dumpd
from langchain.schema.runnable import Runnable
@@ -20,6 +33,8 @@ from langchain.schema.runnable.utils import Input, Output
from langserve.serialization import simple_dumpd, simple_loads
logger = logging.getLogger(__name__)
def _without_callbacks(config: Optional[RunnableConfig]) -> RunnableConfig:
"""Evict callbacks from the config since those are definitely not supported."""
@@ -27,6 +42,12 @@ def _without_callbacks(config: Optional[RunnableConfig]) -> RunnableConfig:
return {k: v for k, v in _config.items() if k != "callbacks"}
@lru_cache(maxsize=1_000) # Will accommodate up to 100 different error messages
def _log_error_message_once(error_message: str) -> None:
"""Log an error message once."""
logger.error(error_message)
def _raise_for_status(response: httpx.Response) -> None:
"""Re-raise with a more informative message.
@@ -84,6 +105,26 @@ def _close_clients(sync_client: httpx.Client, async_client: httpx.AsyncClient) -
asyncio.run(async_client.aclose())
def _raise_exception_from_data(data: str, request: httpx.Request) -> None:
"""Raise an httpx exception from the given error event data."""
try:
decoded_data = json.loads(data)
except json.JSONDecodeError:
raise httpx.HTTPStatusError(
message="invalid json in error event sent from server",
request=request,
response=httpx.Response(status_code=500, text=data),
)
raise httpx.HTTPStatusError(
message=decoded_data["message"],
request=request,
response=httpx.Response(
status_code=decoded_data["status_code"],
text=decoded_data["message"],
),
)
class RemoteRunnable(Runnable[Input, Output]):
"""A RemoteRunnable is a runnable that is executed on a remote server.
@@ -102,16 +143,48 @@ class RemoteRunnable(Runnable[Input, Output]):
url: str,
*,
timeout: Optional[float] = None,
auth: Optional[AuthTypes] = None,
headers: Optional[HeaderTypes] = None,
cookies: Optional[CookieTypes] = None,
verify: VerifyTypes = True,
cert: Optional[CertTypes] = None,
client_kwargs: Optional[Dict[str, Any]] = None,
) -> None:
"""Initialize the client.
Args:
url: The url of the server
timeout: The timeout for requests
auth: Authentication class for requests
headers: Headers to send with requests
cookies: Cookies to send with requests
verify: Whether to verify SSL certificates
cert: SSL certificate to use for requests
client_kwargs: If provided will be unpacked as kwargs to both the sync
and async httpx clients
"""
_client_kwargs = client_kwargs or {}
self.url = url
self.sync_client = httpx.Client(base_url=url, timeout=timeout)
self.async_client = httpx.AsyncClient(base_url=url, timeout=timeout)
self.sync_client = httpx.Client(
base_url=url,
timeout=timeout,
auth=auth,
headers=headers,
cookies=cookies,
verify=verify,
cert=cert,
**_client_kwargs,
)
self.async_client = httpx.AsyncClient(
base_url=url,
timeout=timeout,
auth=auth,
headers=headers,
cookies=cookies,
verify=verify,
cert=cert,
**_client_kwargs,
)
# Register cleanup handler once RemoteRunnable is garbage collected
weakref.finalize(self, _close_clients, self.sync_client, self.async_client)
@@ -292,10 +365,19 @@ class RemoteRunnable(Runnable[Input, Output]):
final_output += chunk
else:
final_output = chunk
elif sse.event == "error":
# This can only be a server side error
_raise_exception_from_data(
sse.data, httpx.Request(method="POST", url=endpoint)
)
elif sse.event == "end":
break
else:
raise NotImplementedError(f"Unknown event {sse.event}")
logger.error(
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}`."
)
except BaseException as e:
run_manager.on_chain_error(e)
raise
@@ -343,11 +425,20 @@ class RemoteRunnable(Runnable[Input, Output]):
final_output += chunk
else:
final_output = chunk
elif sse.event == "error":
# This can only be a server side error
_raise_exception_from_data(
sse.data, httpx.Request(method="POST", url=endpoint)
)
elif sse.event == "end":
break
else:
raise NotImplementedError(f"Unknown event {sse.event}")
logger.error(
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}`."
)
except BaseException as e:
await run_manager.on_chain_error(e)
raise
@@ -368,6 +459,7 @@ class RemoteRunnable(Runnable[Input, Output]):
**kwargs: Optional[Any],
) -> AsyncIterator[RunLogPatch]:
"""Stream all output from a runnable, as reported to the callback system.
This includes all inner runs of LLMs, Retrievers, Tools, etc.
Output is streamed as Log objects, which include a list of
@@ -392,6 +484,7 @@ class RemoteRunnable(Runnable[Input, Output]):
"input": simple_dumpd(input),
"config": _without_callbacks(config),
"kwargs": kwargs,
"diff": True,
"include_names": include_names,
"include_types": include_types,
"include_tags": include_tags,
@@ -414,16 +507,25 @@ class RemoteRunnable(Runnable[Input, Output]):
if sse.event == "data":
data = simple_loads(sse.data)
chunk = RunLogPatch(*data["ops"])
yield chunk
yield chunk
if final_output:
final_output += chunk
else:
final_output = chunk
elif sse.event == "error":
# This can only be a server side error
_raise_exception_from_data(
sse.data, httpx.Request(method="POST", url=endpoint)
)
elif sse.event == "end":
break
else:
raise NotImplementedError(f"Unknown event {sse.event}")
_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}`."
)
except BaseException as e:
await run_manager.on_chain_error(e)
raise
+432
View File
@@ -0,0 +1,432 @@
"""
Copyright © 2017 Marcel Dancak <dancakm@gmail.com>
This work is free. You can redistribute it and/or modify it under the
terms of the Do What The Fuck You Want To Public License, Version 2,
as published by Sam Hocevar. See the COPYING file for more details.
Adapted from https://github.com/marcel-dancak/lz-string-python/blob/master/lzstring.py
"""
import math
keyStrBase64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="
keyStrUriSafe = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$"
baseReverseDic = {}
class Object(object):
def __init__(self, **kwargs):
for k, v in kwargs.items():
setattr(self, k, v)
def getBaseValue(alphabet, character):
if alphabet not in baseReverseDic:
baseReverseDic[alphabet] = {}
for index, i in enumerate(alphabet):
baseReverseDic[alphabet][i] = index
return baseReverseDic[alphabet][character]
def _compress(uncompressed, bitsPerChar, getCharFromInt):
if uncompressed is None:
return ""
context_dictionary = {}
context_dictionaryToCreate = {}
context_c = ""
context_wc = ""
context_w = ""
context_enlargeIn = 2 # Compensate for the first entry which should not count
context_dictSize = 3
context_numBits = 2
context_data = []
context_data_val = 0
context_data_position = 0
for ii in range(len(uncompressed)):
context_c = uncompressed[ii]
if context_c not in context_dictionary:
context_dictionary[context_c] = context_dictSize
context_dictSize += 1
context_dictionaryToCreate[context_c] = True
context_wc = context_w + context_c
if context_wc in context_dictionary:
context_w = context_wc
else:
if context_w in context_dictionaryToCreate:
if ord(context_w[0]) < 256:
for i in range(context_numBits):
context_data_val = context_data_val << 1
if context_data_position == bitsPerChar - 1:
context_data_position = 0
context_data.append(getCharFromInt(context_data_val))
context_data_val = 0
else:
context_data_position += 1
value = ord(context_w[0])
for i in range(8):
context_data_val = (context_data_val << 1) | (value & 1)
if context_data_position == bitsPerChar - 1:
context_data_position = 0
context_data.append(getCharFromInt(context_data_val))
context_data_val = 0
else:
context_data_position += 1
value = value >> 1
else:
value = 1
for i in range(context_numBits):
context_data_val = (context_data_val << 1) | value
if context_data_position == bitsPerChar - 1:
context_data_position = 0
context_data.append(getCharFromInt(context_data_val))
context_data_val = 0
else:
context_data_position += 1
value = 0
value = ord(context_w[0])
for i in range(16):
context_data_val = (context_data_val << 1) | (value & 1)
if context_data_position == bitsPerChar - 1:
context_data_position = 0
context_data.append(getCharFromInt(context_data_val))
context_data_val = 0
else:
context_data_position += 1
value = value >> 1
context_enlargeIn -= 1
if context_enlargeIn == 0:
context_enlargeIn = math.pow(2, context_numBits)
context_numBits += 1
del context_dictionaryToCreate[context_w]
else:
value = context_dictionary[context_w]
for i in range(context_numBits):
context_data_val = (context_data_val << 1) | (value & 1)
if context_data_position == bitsPerChar - 1:
context_data_position = 0
context_data.append(getCharFromInt(context_data_val))
context_data_val = 0
else:
context_data_position += 1
value = value >> 1
context_enlargeIn -= 1
if context_enlargeIn == 0:
context_enlargeIn = math.pow(2, context_numBits)
context_numBits += 1
# Add wc to the dictionary.
context_dictionary[context_wc] = context_dictSize
context_dictSize += 1
context_w = str(context_c)
# Output the code for w.
if context_w != "":
if context_w in context_dictionaryToCreate:
if ord(context_w[0]) < 256:
for i in range(context_numBits):
context_data_val = context_data_val << 1
if context_data_position == bitsPerChar - 1:
context_data_position = 0
context_data.append(getCharFromInt(context_data_val))
context_data_val = 0
else:
context_data_position += 1
value = ord(context_w[0])
for i in range(8):
context_data_val = (context_data_val << 1) | (value & 1)
if context_data_position == bitsPerChar - 1:
context_data_position = 0
context_data.append(getCharFromInt(context_data_val))
context_data_val = 0
else:
context_data_position += 1
value = value >> 1
else:
value = 1
for i in range(context_numBits):
context_data_val = (context_data_val << 1) | value
if context_data_position == bitsPerChar - 1:
context_data_position = 0
context_data.append(getCharFromInt(context_data_val))
context_data_val = 0
else:
context_data_position += 1
value = 0
value = ord(context_w[0])
for i in range(16):
context_data_val = (context_data_val << 1) | (value & 1)
if context_data_position == bitsPerChar - 1:
context_data_position = 0
context_data.append(getCharFromInt(context_data_val))
context_data_val = 0
else:
context_data_position += 1
value = value >> 1
context_enlargeIn -= 1
if context_enlargeIn == 0:
context_enlargeIn = math.pow(2, context_numBits)
context_numBits += 1
del context_dictionaryToCreate[context_w]
else:
value = context_dictionary[context_w]
for i in range(context_numBits):
context_data_val = (context_data_val << 1) | (value & 1)
if context_data_position == bitsPerChar - 1:
context_data_position = 0
context_data.append(getCharFromInt(context_data_val))
context_data_val = 0
else:
context_data_position += 1
value = value >> 1
context_enlargeIn -= 1
if context_enlargeIn == 0:
context_enlargeIn = math.pow(2, context_numBits)
context_numBits += 1
# Mark the end of the stream
value = 2
for i in range(context_numBits):
context_data_val = (context_data_val << 1) | (value & 1)
if context_data_position == bitsPerChar - 1:
context_data_position = 0
context_data.append(getCharFromInt(context_data_val))
context_data_val = 0
else:
context_data_position += 1
value = value >> 1
# Flush the last char
while True:
context_data_val = context_data_val << 1
if context_data_position == bitsPerChar - 1:
context_data.append(getCharFromInt(context_data_val))
break
else:
context_data_position += 1
return "".join(context_data)
def _decompress(length, resetValue, getNextValue):
dictionary = {}
enlargeIn = 4
dictSize = 4
numBits = 3
entry = ""
result = []
data = Object(val=getNextValue(0), position=resetValue, index=1)
for i in range(3):
dictionary[i] = i
bits = 0
maxpower = math.pow(2, 2)
power = 1
while power != maxpower:
resb = data.val & data.position
data.position >>= 1
if data.position == 0:
data.position = resetValue
data.val = getNextValue(data.index)
data.index += 1
bits |= power if resb > 0 else 0
power <<= 1
next = bits
if next == 0:
bits = 0
maxpower = math.pow(2, 8)
power = 1
while power != maxpower:
resb = data.val & data.position
data.position >>= 1
if data.position == 0:
data.position = resetValue
data.val = getNextValue(data.index)
data.index += 1
bits |= power if resb > 0 else 0
power <<= 1
c = chr(bits)
elif next == 1:
bits = 0
maxpower = math.pow(2, 16)
power = 1
while power != maxpower:
resb = data.val & data.position
data.position >>= 1
if data.position == 0:
data.position = resetValue
data.val = getNextValue(data.index)
data.index += 1
bits |= power if resb > 0 else 0
power <<= 1
c = chr(bits)
elif next == 2:
return ""
# print(bits)
dictionary[3] = c
w = c
result.append(c)
counter = 0
while True:
counter += 1
if data.index > length:
return ""
bits = 0
maxpower = math.pow(2, numBits)
power = 1
while power != maxpower:
resb = data.val & data.position
data.position >>= 1
if data.position == 0:
data.position = resetValue
data.val = getNextValue(data.index)
data.index += 1
bits |= power if resb > 0 else 0
power <<= 1
c = bits
if c == 0:
bits = 0
maxpower = math.pow(2, 8)
power = 1
while power != maxpower:
resb = data.val & data.position
data.position >>= 1
if data.position == 0:
data.position = resetValue
data.val = getNextValue(data.index)
data.index += 1
bits |= power if resb > 0 else 0
power <<= 1
dictionary[dictSize] = chr(bits)
dictSize += 1
c = dictSize - 1
enlargeIn -= 1
elif c == 1:
bits = 0
maxpower = math.pow(2, 16)
power = 1
while power != maxpower:
resb = data.val & data.position
data.position >>= 1
if data.position == 0:
data.position = resetValue
data.val = getNextValue(data.index)
data.index += 1
bits |= power if resb > 0 else 0
power <<= 1
dictionary[dictSize] = chr(bits)
dictSize += 1
c = dictSize - 1
enlargeIn -= 1
elif c == 2:
return "".join(result)
if enlargeIn == 0:
enlargeIn = math.pow(2, numBits)
numBits += 1
if c in dictionary:
entry = dictionary[c]
else:
if c == dictSize:
entry = w + w[0]
else:
return None
result.append(entry)
# Add w+entry[0] to the dictionary.
dictionary[dictSize] = w + entry[0]
dictSize += 1
enlargeIn -= 1
w = entry
if enlargeIn == 0:
enlargeIn = math.pow(2, numBits)
numBits += 1
class LZString:
@staticmethod
def compress(uncompressed):
return _compress(uncompressed, 16, chr)
@staticmethod
def compressToUTF16(uncompressed):
if uncompressed is None:
return ""
return _compress(uncompressed, 15, lambda a: chr(a + 32)) + " "
@staticmethod
def compressToBase64(uncompressed):
if uncompressed is None:
return ""
res = _compress(uncompressed, 6, lambda a: keyStrBase64[a])
# To produce valid Base64
end = len(res) % 4
print(end)
if end > 0:
res += "=" * (4 - end)
return res
@staticmethod
def compressToEncodedURIComponent(uncompressed):
if uncompressed is None:
return ""
return _compress(uncompressed, 6, lambda a: keyStrUriSafe[a])
@staticmethod
def decompress(compressed):
if compressed is None:
return ""
if compressed == "":
return None
return _decompress(len(compressed), 32768, lambda index: ord(compressed[index]))
@staticmethod
def decompressFromUTF16(compressed):
if compressed is None:
return ""
if compressed == "":
return None
return _decompress(
len(compressed), 16384, lambda index: ord(compressed[index]) - 32
)
@staticmethod
def decompressFromBase64(compressed):
if compressed is None:
return ""
if compressed == "":
return None
return _decompress(
len(compressed),
32,
lambda index: getBaseValue(keyStrBase64, compressed[index]),
)
@staticmethod
def decompressFromEncodedURIComponent(compressed):
if compressed is None:
return ""
if compressed == "":
return None
compressed = compressed.replace(" ", "+")
return _decompress(
len(compressed),
32,
lambda index: getBaseValue(keyStrUriSafe, compressed[index]),
)
+104
View File
@@ -0,0 +1,104 @@
import importlib
import logging
from pathlib import Path
from typing import Generator, TypedDict, Union
from fastapi import APIRouter, FastAPI
from tomli import load
from langserve.server import add_routes
class LangServeExport(TypedDict):
"""
Fields from pyproject.toml that are relevant to LangServe
Attributes:
module: The module to import from, tool.langserve.export_module
attr: The attribute to import from the module, tool.langserve.export_attr
package_name: The name of the package, tool.poetry.name
"""
module: str
attr: str
package_name: str
def get_langserve_export(filepath: Path) -> LangServeExport:
with open(filepath, "rb") as f:
data = load(f)
try:
module = data["tool"]["langserve"]["export_module"]
attr = data["tool"]["langserve"]["export_attr"]
package_name = data["tool"]["poetry"]["name"]
except KeyError as e:
raise KeyError("Invalid LangServe PyProject.toml") from e
return LangServeExport(module=module, attr=attr, package_name=package_name)
EXCLUDE_PATHS = set(["__pycache__", ".venv", ".git", ".github"])
def _include_path(path: Path) -> bool:
"""
Skip paths that are in EXCLUDE_PATHS or start with an underscore.
"""
for part in path.parts:
if part in EXCLUDE_PATHS:
return False
if part.startswith("_"):
return False
return True
def list_packages(path: str = "packages") -> Generator[Path, None, None]:
"""
Yields Path objects for each folder that contains a pyproject.toml file within a
path. Use this to find packages to add to the server.
See `add_package_routes` below for an example of how to use this.
"""
# traverse path for routes to host (any directory holding a pyproject.toml file)
package_root = Path(path)
for pyproject_path in package_root.glob("**/pyproject.toml"):
if not _include_path(pyproject_path):
continue
yield pyproject_path.parent
def add_package_route(
app: Union[FastAPI, APIRouter], package_path: Path, mount_path: str
) -> None:
try:
pyproject_path = package_path / "pyproject.toml"
# get langserve export
package = get_langserve_export(pyproject_path)
package_name = package["package_name"]
# import module
mod = importlib.import_module(package["module"])
except KeyError:
logging.warning(
f"Skipping {package_path} because it is not a valid LangServe "
"package (see pyproject.toml)"
)
return
except ImportError as e:
logging.warning(f"Error: {e}")
logging.warning(f"Try fixing with `poetry add --editable {package_path}`")
logging.warning(
"To remove packages, use `poe` instead of `poetry`: "
f"`poe remove {package_name}`"
)
return
# get attr
chain = getattr(mod, package["attr"])
add_routes(app, chain, path=mount_path)
def add_package_routes(app: Union[FastAPI, APIRouter], path: str = "packages") -> None:
# traverse path for routes to host (any directory holding a pyproject.toml file)
for package_path in list_packages(path):
mount_path_relative = package_path.relative_to(Path(path))
mount_path = f"/{mount_path_relative}"
add_package_route(app, package_path, mount_path)
+47
View File
@@ -0,0 +1,47 @@
import json
import mimetypes
import os
from string import Template
from typing import List, Type
from fastapi.responses import Response
from langchain.schema.runnable import Runnable
try:
from pydantic.v1 import BaseModel
except ImportError:
from pydantic import BaseModel
class PlaygroundTemplate(Template):
delimiter = "____"
async def serve_playground(
runnable: Runnable,
input_schema: Type[BaseModel],
config_keys: List[str],
base_url: str,
file_path: str,
) -> Response:
local_file_path = os.path.join(
os.path.dirname(__file__),
"./playground/dist",
file_path or "index.html",
)
with open(local_file_path) as f:
mime_type = mimetypes.guess_type(local_file_path)[0]
if mime_type in ("text/html", "text/css", "application/javascript"):
res = PlaygroundTemplate(f.read()).substitute(
LANGSERVE_BASE_URL=base_url[1:]
if base_url.startswith("/")
else base_url,
LANGSERVE_CONFIG_SCHEMA=json.dumps(
runnable.config_schema(include=config_keys).schema()
),
LANGSERVE_INPUT_SCHEMA=json.dumps(input_schema.schema()),
)
else:
res = f.buffer.read()
return Response(res, media_type=mime_type)
+18
View File
@@ -0,0 +1,18 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}
+26
View File
@@ -0,0 +1,26 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.yarn
+27
View File
@@ -0,0 +1,27 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
- Configure the top-level `parserOptions` property like this:
```js
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: ['./tsconfig.json', './tsconfig.node.json'],
tsconfigRootDir: __dirname,
},
```
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

+23
View File
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/____LANGSERVE_BASE_URL/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Playground</title>
<script type="module" crossorigin src="/____LANGSERVE_BASE_URL/assets/index-9fe7f71c.js"></script>
<link rel="stylesheet" href="/____LANGSERVE_BASE_URL/assets/index-5008c8a8.css">
</head>
<body>
<div id="root"></div>
<script>
try {
window.CONFIG_SCHEMA = ____LANGSERVE_CONFIG_SCHEMA;
window.INPUT_SCHEMA = ____LANGSERVE_INPUT_SCHEMA;
} catch (error) {
// pass
}
</script>
</body>
</html>
+21
View File
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Playground</title>
</head>
<body>
<div id="root"></div>
<script>
try {
window.CONFIG_SCHEMA = ____LANGSERVE_CONFIG_SCHEMA;
window.INPUT_SCHEMA = ____LANGSERVE_INPUT_SCHEMA;
} catch (error) {
// pass
}
</script>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+52
View File
@@ -0,0 +1,52 @@
{
"name": "langserve-playground",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@jsonforms/core": "^3.1.0",
"@jsonforms/material-renderers": "^3.1.0",
"@jsonforms/react": "^3.1.0",
"@jsonforms/vanilla-renderers": "^3.1.0",
"@microsoft/fetch-event-source": "^2.0.1",
"@mui/icons-material": "^5.14.11",
"@mui/material": "^5.14.11",
"@mui/x-date-pickers": "^6.16.0",
"clsx": "^2.0.0",
"dayjs": "^1.11.10",
"fast-json-patch": "^3.1.1",
"json-schema-defaults": "^0.4.0",
"lodash": "^4.17.21",
"lz-string": "^1.5.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tailwind-merge": "^1.14.0",
"use-debounce": "^9.0.4",
"vaul": "^0.7.3"
},
"devDependencies": {
"@types/lodash": "^4.14.200",
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"@vitejs/plugin-react": "^4.0.3",
"autoprefixer": "^10.4.16",
"eslint": "^8.45.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3",
"postcss": "^8.4.31",
"tailwindcss": "^3.3.3",
"typescript": "^5.0.2",
"vite": "^4.4.5",
"vite-plugin-svgr": "^4.1.0"
}
}
+6
View File
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

+90
View File
@@ -0,0 +1,90 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
input,
textarea,
select {
background: transparent;
}
/* clash between MUI and Tailwind */
input:focus,
textarea:focus,
select:focus {
box-shadow: none;
outline: none;
}
:root {
--popover: 0 0% 100%;
--background: 0 0% 100%;
--divider-500: 210 40% 96.1%; /* slate-100 */
--divider-700: 214.3 31.8% 91.4%; /* slate-200 */
--ls-blue: 211.5 91.8% 61.8%;
--ls-black: 222.2 47.4% 11.2%; /* slate-900 */
--ls-gray-100: 215.4 16.3% 46.9%; /* slate-500 */
--ls-gray-200: 212.7 26.8% 83.9%; /* slate-300 */
--ls-gray-300: 214.3 31.8% 91.4%; /* slate-200 */
--ls-gray-400: 210 40% 96.1%; /* slate-100 */
}
@media (prefers-color-scheme: dark) {
:root {
--popover: 240 11.6% 8.4%;
--background: 240 11.6% 8.4%;
--divider-500: 217.2 32.6% 17.5%; /* slate-800 */
--divider-700: 215.3 25% 26.7%; /* slate-700 */
--ls-blue: 211.5 91.8% 61.8%;
--ls-black: 0 0% 100%; /* white */
--ls-gray-100: 215 20.2% 65.1%; /* slate-400 */
--ls-gray-200: 215.4 16.3% 46.9%; /* slate-500 */
--ls-gray-300: 215.3 25% 26.7%; /* slate-700 */
--ls-gray-400: 217.2 32.6% 17.5%; /* slate-800 */
}
}
}
.control {
@apply flex flex-col border border-divider-700 rounded-lg p-3 gap-1 relative bg-background transition-all outline-ls-blue/20;
@apply focus-within:border-ls-blue focus-within:outline focus-within:outline-4 focus-within:outline-ls-blue/20;
}
.control > label,
.control h6 {
@apply text-xs uppercase font-semibold text-ls-gray-100;
}
.control div .MuiGrid-item {
@apply pt-0;
}
.control > select {
@apply -ml-1;
}
.control > .input-description,
.control > .validation {
@apply absolute right-3 top-3 text-xs;
}
.group-layout {
@apply flex flex-col gap-4 bg-background p-4 border border-divider-700 rounded-lg;
}
.no-scrollbar {
scrollbar-width: none;
}
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.vertical-layout {
@apply flex flex-col gap-4;
}
+375
View File
@@ -0,0 +1,375 @@
import "./App.css";
import React, { useEffect, useRef, useState } from "react";
import defaults from "json-schema-defaults";
import { JsonForms } from "@jsonforms/react";
import {
materialAllOfControlTester,
MaterialAllOfRenderer,
materialAnyOfControlTester,
MaterialAnyOfRenderer,
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 ChevronRight from "./assets/ChevronRight.svg?react";
import { compressToEncodedURIComponent } from "lz-string";
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 { RunState, useStreamLog } from "./useStreamLog";
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 { cn } from "./utils/cn";
import { getStateFromUrl, ShareDialog } from "./components/ShareDialog";
dayjs.extend(relativeDate);
dayjs.extend(utc);
function str(o: unknown): React.ReactNode {
return typeof o === "object"
? JSON.stringify(o, null, 2)
: (o as React.ReactNode);
}
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")));
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: materialAnyOfControlTester, renderer: MaterialAnyOfRenderer },
{ tester: materialOneOfControlTester, renderer: MaterialOneOfRenderer },
// custom renderers
{ tester: materialArrayControlTester, renderer: CustomArrayControlRenderer },
{ tester: isObject, renderer: InputControl },
];
const nestedArrayControlTester: RankedTester = rankWith(1, (_, jsonSchema) => {
return jsonSchema.type === "array";
});
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 IntermediateSteps(props: { latest: RunState }) {
const [expanded, setExpanded] = useState(false);
return (
<div className="flex flex-col border border-divider-700 rounded-2xl bg-background">
<button
className="font-medium text-left p-4 flex items-center justify-between"
onClick={() => setExpanded((open) => !open)}
>
<span>Intermediate steps</span>
<ChevronRight
className={cn("transition-all", expanded && "rotate-90")}
/>
</button>
{expanded && (
<div className="flex flex-col gap-5 p-4 pt-0 divide-solid divide-y divide-divider-700 rounded-b-xl">
{Object.values(props.latest.logs).map((log) => (
<div
className="gap-3 flex-col min-w-0 flex bg-background pt-3 first-of-type:pt-0"
key={log.id}
>
<div className="flex items-center justify-between">
<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>
))}
</div>
)}
</div>
);
}
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: null, errors: [] });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [schemas.config]);
// the runner
const { startStream, stopStream, latest } = useStreamLog();
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);
}, []);
return schemas.config && schemas.input ? (
<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>
<div className="flex flex-col gap-3">
{!isIframe && <h2 className="text-xl font-semibold">Configure</h2>}
<JsonForms
schema={schemas.config}
data={configData.data}
renderers={renderers}
cells={cells}
onChange={({ data, errors }) =>
data
? setConfigData({ data, errors, defaults: false })
: undefined
}
/>
{!!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>
{!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">
<h3 className="font-medium">Inputs</h3>
<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">
{latest.streamed_output.map(str).join("") || "..."}
</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="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={() => {
stopStream
? stopStream()
: startStream(inputData.data, configData.data);
}}
disabled={
!stopStream &&
(!!inputData.errors?.length || !!configData.errors?.length)
}
>
{stopStream ? (
<span className="text-white">Stop</span>
) : (
<>
<SendIcon className="flex-shrink-0" />
<span className="text-white">Start</span>
</>
)}
</button>
</>
)}
</div>
</div>
</div>
) : null;
}
export default App;
@@ -0,0 +1,6 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M12 5.45455C8.38505 5.45455 5.45455 8.38505 5.45455 12C5.45455 15.615 8.38505 18.5455 12 18.5455C15.615 18.5455 18.5455 15.615 18.5455 12C18.5455 8.38505 15.615 5.45455 12 5.45455ZM4 12C4 7.58172 7.58172 4 12 4C16.4183 4 20 7.58172 20 12C20 16.4183 16.4183 20 12 20C7.58172 20 4 16.4183 4 12ZM15.787 9.30392C16.071 9.58794 16.071 10.0484 15.787 10.3324L11.4233 14.6961C11.1393 14.9801 10.6788 14.9801 10.3948 14.6961L8.21301 12.5143C7.929 12.2303 7.929 11.7697 8.21301 11.4857C8.49703 11.2017 8.95751 11.2017 9.24153 11.4857L10.9091 13.1533L14.7585 9.30392C15.0425 9.01991 15.503 9.01991 15.787 9.30392Z"
fill="currentColor" />
</svg>

After

Width:  |  Height:  |  Size: 792 B

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

After

Width:  |  Height:  |  Size: 505 B

@@ -0,0 +1,6 @@
<svg width="19" height="13" viewBox="0 0 19 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M6.75657 0.91107C7.08201 1.23651 7.08201 1.76414 6.75657 2.08958L2.34583 6.50033L6.75657 10.9111C7.08201 11.2365 7.08201 11.7641 6.75657 12.0896C6.43114 12.415 5.9035 12.415 5.57806 12.0896L0.578062 7.08958C0.252625 6.76414 0.252625 6.23651 0.578062 5.91107L5.57806 0.91107C5.9035 0.585633 6.43114 0.585633 6.75657 0.91107ZM12.2447 0.91107C12.5702 0.585633 13.0978 0.585633 13.4232 0.91107L18.4232 5.91107C18.7487 6.23651 18.7487 6.76414 18.4232 7.08958L13.4232 12.0896C13.0978 12.415 12.5702 12.415 12.2447 12.0896C11.9193 11.7641 11.9193 11.2365 12.2447 10.9111L16.6555 6.50033L12.2447 2.08958C11.9193 1.76414 11.9193 1.23651 12.2447 0.91107Z"
fill="currentColor" fill-opacity="0.9" />
</svg>

After

Width:  |  Height:  |  Size: 852 B

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

After

Width:  |  Height:  |  Size: 1.3 KiB

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

After

Width:  |  Height:  |  Size: 2.3 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 fill-rule="evenodd" clip-rule="evenodd"
d="M11.9999 2.51489C12.5522 2.51489 12.9999 2.96261 12.9999 3.51489V11.0002L20.4852 11.0002C21.0375 11.0002 21.4852 11.4479 21.4852 12.0002C21.4852 12.5525 21.0375 13.0002 20.4852 13.0002H12.9999V20.4855C12.9999 21.0377 12.5522 21.4855 11.9999 21.4855C11.4476 21.4855 10.9999 21.0377 10.9999 20.4855V13.0002H3.51465C2.96236 13.0002 2.51465 12.5525 2.51465 12.0002C2.51465 11.4479 2.96236 11.0002 3.51465 11.0002L10.9999 11.0002V3.51489C10.9999 2.96261 11.4476 2.51489 11.9999 2.51489Z"
fill="currentColor" />
</svg>

After

Width:  |  Height:  |  Size: 670 B

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

After

Width:  |  Height:  |  Size: 1.2 KiB

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

After

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

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,51 @@
import { cn } from "../utils/cn";
const COMMON_CLS = cn(
"text-lg col-[1] row-[1] m-0 resize-none overflow-hidden whitespace-pre-wrap break-words border-none bg-transparent p-0"
);
export function AutosizeTextarea(props: {
id?: string;
value?: string | null | undefined;
placeholder?: string;
className?: string;
onChange?: (e: string) => void;
onFocus?: () => void;
onBlur?: () => void;
onKeyDown?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
autoFocus?: boolean;
readOnly?: boolean;
cursorPointer?: boolean;
disabled?: boolean;
}) {
return (
<div className={cn("grid w-full", props.className)}>
<textarea
id={props.id}
className={cn(
COMMON_CLS,
"text-transparent caret-black dark:caret-slate-200"
)}
disabled={props.disabled}
value={props.value ?? ""}
rows={1}
onChange={(e) => {
const target = e.target as HTMLTextAreaElement;
props.onChange?.(target.value);
}}
onFocus={props.onFocus}
onBlur={props.onBlur}
placeholder={props.placeholder}
readOnly={props.readOnly}
autoFocus={props.autoFocus && !props.readOnly}
onKeyDown={props.onKeyDown}
/>
<div
aria-hidden
className={cn(COMMON_CLS, "pointer-events-none select-none")}
>
{props.value}{" "}
</div>
</div>
);
}
@@ -0,0 +1,92 @@
// From https://github.com/eclipsesource/jsonforms/blob/44070b325121ad7173082fdf33be079f42ef96c4/packages/material/src/complex/MaterialArrayControlRenderer.tsx
/*
The MIT License
Copyright (c) 2017-2019 EclipseSource Munich
https://github.com/eclipsesource/jsonforms
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
import { useCallback, useState } from "react";
import {
ArrayLayoutProps,
RankedTester,
isObjectArrayControl,
isObjectArrayWithNesting,
isPrimitiveArrayControl,
or,
rankWith,
} from "@jsonforms/core";
import { withJsonFormsArrayLayoutProps } from "@jsonforms/react";
import { MaterialTableControl } from "./CustomArrayControlRenderer/MaterialTableControl";
import { Hidden } from "@mui/material";
import { DeleteDialog } from "./CustomArrayControlRenderer/DeleteDialog";
export const MaterialArrayControlRenderer = (props: ArrayLayoutProps) => {
const [open, setOpen] = useState(false);
const [path, setPath] = useState<string | undefined>(undefined);
const [rowData, setRowData] = useState<number | undefined>(undefined);
const { removeItems, visible } = props;
const openDeleteDialog = useCallback(
(p: string, rowIndex: number) => {
setOpen(true);
setPath(p);
setRowData(rowIndex);
},
[setOpen, setPath, setRowData]
);
const deleteCancel = useCallback(() => setOpen(false), [setOpen]);
const deleteConfirm = useCallback(() => {
const p = path?.substring(0, path.lastIndexOf("."));
if (p != null && rowData != null) removeItems?.(p, [rowData])();
setOpen(false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [setOpen, path, rowData]);
const deleteClose = useCallback(() => setOpen(false), [setOpen]);
return (
<div className="control">
<Hidden xsUp={!visible}>
<MaterialTableControl {...props} openDeleteDialog={openDeleteDialog} />
<DeleteDialog
open={open}
onCancel={deleteCancel}
onConfirm={deleteConfirm}
onClose={deleteClose}
acceptText={props.translations.deleteDialogAccept!}
declineText={props.translations.deleteDialogDecline!}
title={props.translations.deleteDialogTitle!}
message={props.translations.deleteDialogMessage!}
/>
</Hidden>
</div>
);
};
// eslint-disable-next-line react-refresh/only-export-components
export const materialArrayControlTester: RankedTester = rankWith(
999,
or(isObjectArrayControl, isPrimitiveArrayControl, isObjectArrayWithNesting)
);
// eslint-disable-next-line react-refresh/only-export-components
export default withJsonFormsArrayLayoutProps(MaterialArrayControlRenderer);
@@ -0,0 +1,84 @@
/*
The MIT License
Copyright (c) 2017-2019 EclipseSource Munich
https://github.com/eclipsesource/jsonforms
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
import React from "react";
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
} from "@mui/material";
export interface DeleteDialogProps {
open: boolean;
onClose(): void;
onConfirm(): void;
onCancel(): void;
title: string;
message: string;
acceptText: string;
declineText: string;
}
export interface WithDeleteDialogSupport {
openDeleteDialog(path: string, data: number): void;
}
export const DeleteDialog = React.memo(function DeleteDialog({
open,
onClose,
onConfirm,
onCancel,
title,
message,
acceptText,
declineText,
}: DeleteDialogProps) {
return (
<Dialog
open={open}
keepMounted
onClose={onClose}
aria-labelledby="alert-dialog-confirmdelete-title"
aria-describedby="alert-dialog-confirmdelete-description"
>
<DialogTitle id="alert-dialog-confirmdelete-title">{title}</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-confirmdelete-description">
{message}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={onCancel} color="primary">
{declineText}
</Button>
<Button onClick={onConfirm} color="primary">
{acceptText}
</Button>
</DialogActions>
</Dialog>
);
});
@@ -0,0 +1,502 @@
/*
The MIT License
Copyright (c) 2017-2019 EclipseSource Munich
https://github.com/eclipsesource/jsonforms
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
import isEmpty from "lodash/isEmpty";
import union from "lodash/union";
import {
DispatchCell,
JsonFormsStateContext,
useJsonForms,
} from "@jsonforms/react";
import startCase from "lodash/startCase";
import range from "lodash/range";
import React, { Fragment, useMemo } from "react";
import TrashIcon from "../../assets/TrashIcon.svg?react";
import {
FormHelperText,
Grid,
Hidden,
IconButton,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
Typography,
} from "@mui/material";
import {
ArrayLayoutProps,
ControlElement,
errorsAt,
formatErrorMessage,
JsonSchema,
Paths,
Resolve,
JsonFormsRendererRegistryEntry,
JsonFormsCellRendererRegistryEntry,
encode,
ArrayTranslations,
} from "@jsonforms/core";
import ArrowDownward from "@mui/icons-material/ArrowDownward";
import ArrowUpward from "@mui/icons-material/ArrowUpward";
import { WithDeleteDialogSupport } from "./DeleteDialog";
import NoBorderTableCell from "./NoBorderTableCell";
import TableToolbar from "./TableToolbar";
import merge from "lodash/merge";
// we want a cell that doesn't automatically span
const styles = {
fixedCell: {
width: "150px",
height: "50px",
paddingLeft: 0,
paddingRight: 0,
textAlign: "center",
},
fixedCellSmall: {
width: "50px",
height: "50px",
paddingLeft: 0,
paddingRight: 0,
textAlign: "center",
},
};
const generateCells = (
Cell: React.ComponentType<OwnPropsOfNonEmptyCell | TableHeaderCellProps>,
schema: JsonSchema,
rowPath: string,
enabled: boolean,
cells?: JsonFormsCellRendererRegistryEntry[]
) => {
if (schema.type === "object") {
return getValidColumnProps(schema).map((prop) => {
const cellPath = Paths.compose(rowPath, prop);
const props = {
propName: prop,
schema,
title: schema.properties?.[prop]?.title ?? startCase(prop),
rowPath,
cellPath,
enabled,
cells,
};
return <Cell key={cellPath} {...props} />;
});
} else {
// primitives
const props = {
schema,
rowPath,
cellPath: rowPath,
enabled,
};
return <Cell key={rowPath} {...props} />;
}
};
const getValidColumnProps = (scopedSchema: JsonSchema) => {
if (
scopedSchema.type === "object" &&
typeof scopedSchema.properties === "object"
) {
return Object.keys(scopedSchema.properties).filter(
(prop) => scopedSchema.properties?.[prop].type !== "array"
);
}
// primitives
return [""];
};
export interface EmptyTableProps {
numColumns: number;
translations: ArrayTranslations;
}
const EmptyTable = ({ numColumns, translations }: EmptyTableProps) => (
<TableRow>
<NoBorderTableCell colSpan={numColumns}>
<Typography align="center">{translations.noDataMessage}</Typography>
</NoBorderTableCell>
</TableRow>
);
interface TableHeaderCellProps {
title: string;
}
const TableHeaderCell = React.memo(function TableHeaderCell({
title,
}: TableHeaderCellProps) {
return (
<TableCell
sx={{
color: "hsl(var(--ls-gray-100))",
borderBottomColor: "hsl(var(--divider-700))",
px: 0,
py: 1,
}}
>
{title}
</TableCell>
);
});
interface NonEmptyCellProps extends OwnPropsOfNonEmptyCell {
rootSchema: JsonSchema;
errors: string;
path: string;
enabled: boolean;
}
interface OwnPropsOfNonEmptyCell {
rowPath: string;
propName?: string;
schema: JsonSchema;
enabled: boolean;
renderers?: JsonFormsRendererRegistryEntry[];
cells?: JsonFormsCellRendererRegistryEntry[];
}
const ctxToNonEmptyCellProps = (
ctx: JsonFormsStateContext,
ownProps: OwnPropsOfNonEmptyCell
): NonEmptyCellProps => {
const path =
ownProps.rowPath +
(ownProps.schema.type === "object" ? "." + ownProps.propName : "");
const errors = formatErrorMessage(
union(
errorsAt(
path,
ownProps.schema,
(p) => p === path
)(ctx.core?.errors ?? []).map((error) => error.message) as string[]
)
);
return {
rowPath: ownProps.rowPath,
propName: ownProps.propName,
schema: ownProps.schema,
rootSchema: ctx.core?.schema ?? {},
errors,
path,
enabled: ownProps.enabled,
cells: ownProps.cells || ctx.cells,
renderers: ownProps.renderers || ctx.renderers,
};
};
const controlWithoutLabel = (scope: string): ControlElement => ({
type: "Control",
scope: scope,
label: false,
});
interface NonEmptyCellComponentProps {
path: string;
propName?: string;
schema: JsonSchema;
rootSchema: JsonSchema;
errors: string;
enabled: boolean;
renderers?: JsonFormsRendererRegistryEntry[];
cells?: JsonFormsCellRendererRegistryEntry[];
isValid: boolean;
}
const NonEmptyCellComponent = React.memo(function NonEmptyCellComponent({
path,
propName,
schema,
rootSchema,
errors,
enabled,
renderers,
cells,
isValid,
}: NonEmptyCellComponentProps) {
return (
<NoBorderTableCell sx={{ color: "hsl(var(--ls-black))" }}>
{schema.properties ? (
<DispatchCell
schema={Resolve.schema(
schema,
`#/properties/${encode(propName!)}`,
rootSchema
)}
uischema={controlWithoutLabel(`#/properties/${encode(propName!)}`)}
path={path}
enabled={enabled}
renderers={renderers}
cells={cells}
/>
) : (
<DispatchCell
schema={schema}
uischema={controlWithoutLabel("#")}
path={path}
enabled={enabled}
renderers={renderers}
cells={cells}
/>
)}
<FormHelperText error={!isValid}>{!isValid && errors}</FormHelperText>
</NoBorderTableCell>
);
});
const NonEmptyCell = (ownProps: OwnPropsOfNonEmptyCell) => {
const ctx = useJsonForms();
const emptyCellProps = ctxToNonEmptyCellProps(ctx, ownProps);
const isValid = isEmpty(emptyCellProps.errors);
return <NonEmptyCellComponent {...emptyCellProps} isValid={isValid} />;
};
interface NonEmptyRowProps {
childPath: string;
schema: JsonSchema;
rowIndex: number;
moveUpCreator: (path: string, position: number) => () => void;
moveDownCreator: (path: string, position: number) => () => void;
enableUp: boolean;
enableDown: boolean;
showSortButtons: boolean;
enabled: boolean;
cells?: JsonFormsCellRendererRegistryEntry[];
path: string;
translations: ArrayTranslations;
}
const NonEmptyRowComponent = ({
childPath,
schema,
rowIndex,
openDeleteDialog,
moveUpCreator,
moveDownCreator,
enableUp,
enableDown,
showSortButtons,
enabled,
cells,
path,
translations,
}: NonEmptyRowProps & WithDeleteDialogSupport) => {
const moveUp = useMemo(
() => moveUpCreator(path, rowIndex),
[moveUpCreator, path, rowIndex]
);
const moveDown = useMemo(
() => moveDownCreator(path, rowIndex),
[moveDownCreator, path, rowIndex]
);
return (
<TableRow key={childPath} hover>
{generateCells(NonEmptyCell as any, schema, childPath, enabled, cells)}
{enabled ? (
<NoBorderTableCell
style={showSortButtons ? styles.fixedCell : styles.fixedCellSmall}
>
<Grid
container
direction="row"
justifyContent="flex-end"
alignItems="center"
>
{showSortButtons ? (
<Fragment>
<Grid item>
<IconButton
aria-label={translations.upAriaLabel}
onClick={moveUp}
disabled={!enableUp}
size="large"
>
<ArrowUpward />
</IconButton>
</Grid>
<Grid item>
<IconButton
aria-label={translations.downAriaLabel}
onClick={moveDown}
disabled={!enableDown}
size="large"
>
<ArrowDownward />
</IconButton>
</Grid>
</Fragment>
) : null}
<Grid item>
<IconButton
aria-label={translations.removeAriaLabel}
onClick={() => openDeleteDialog(childPath, rowIndex)}
size="large"
sx={{ p: 1 }}
>
<TrashIcon className="text-ls-black" />
</IconButton>
</Grid>
</Grid>
</NoBorderTableCell>
) : null}
</TableRow>
);
};
export const NonEmptyRow = React.memo(NonEmptyRowComponent);
interface TableRowsProp {
data: number;
path: string;
schema: JsonSchema;
uischema: ControlElement;
config?: any;
enabled: boolean;
cells?: JsonFormsCellRendererRegistryEntry[];
moveUp?(path: string, toMove: number): () => void;
moveDown?(path: string, toMove: number): () => void;
translations: ArrayTranslations;
}
const TableRows = ({
data,
path,
schema,
openDeleteDialog,
moveUp,
moveDown,
uischema,
config,
enabled,
cells,
translations,
}: TableRowsProp & WithDeleteDialogSupport) => {
const isEmptyTable = data === 0;
if (isEmptyTable) {
return (
<EmptyTable
numColumns={getValidColumnProps(schema).length + 1}
translations={translations}
/>
);
}
const appliedUiSchemaOptions = merge({}, config, uischema.options);
return (
<React.Fragment>
{range(data).map((index: number) => {
const childPath = Paths.compose(path, `${index}`);
return (
<NonEmptyRow
key={childPath}
childPath={childPath}
rowIndex={index}
schema={schema}
openDeleteDialog={openDeleteDialog}
moveUpCreator={moveUp ?? (() => () => {})}
moveDownCreator={moveDown ?? (() => () => {})}
enableUp={index !== 0}
enableDown={index !== data - 1}
showSortButtons={
appliedUiSchemaOptions.showSortButtons ||
appliedUiSchemaOptions.showArrayTableSortButtons
}
enabled={enabled}
cells={cells}
path={path}
translations={translations}
/>
);
})}
</React.Fragment>
);
};
export class MaterialTableControl extends React.Component<
ArrayLayoutProps & WithDeleteDialogSupport,
any
> {
addItem = (path: string, value: any) => this.props.addItem(path, value);
render() {
const {
label,
path,
schema,
rootSchema,
uischema,
errors,
openDeleteDialog,
visible,
enabled,
cells,
translations,
} = this.props;
const controlElement = uischema as ControlElement;
const isObjectSchema = schema.type === "object";
const headerCells: any = isObjectSchema
? generateCells(TableHeaderCell as any, schema, path, enabled, cells)
: undefined;
return (
<Hidden xsUp={!visible}>
<Table>
<TableHead sx={{ borderBottomColor: "hsl(var(--divider-700))" }}>
<TableToolbar
errors={errors}
label={label}
addItem={this.addItem}
numColumns={isObjectSchema ? headerCells.length : 1}
path={path}
uischema={controlElement}
schema={schema}
rootSchema={rootSchema}
enabled={enabled}
translations={translations}
/>
{isObjectSchema && (
<TableRow>
{headerCells}
{enabled ? (
<TableCell
sx={{ borderBottomColor: "hsl(var(--divider-700))" }}
/>
) : null}
</TableRow>
)}
</TableHead>
<TableBody>
<TableRows
{...this.props}
openDeleteDialog={this.props.openDeleteDialog ?? openDeleteDialog}
translations={this.props.translations ?? translations}
/>
</TableBody>
</Table>
</Hidden>
);
}
}
@@ -0,0 +1,39 @@
/*
The MIT License
Copyright (c) 2017-2019 EclipseSource Munich
https://github.com/eclipsesource/jsonforms
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
import { styled } from "@mui/material/styles";
import { TableCell } from "@mui/material";
const StyledTableCell = styled(TableCell)({
borderBottom: "none",
fill: "white",
color: "inherit",
padding: 0,
});
const NoBorderTableCell = ({ children, ...otherProps }: any) => (
<StyledTableCell {...otherProps}>{children}</StyledTableCell>
);
export default NoBorderTableCell;
@@ -0,0 +1,101 @@
/*
The MIT License
Copyright (c) 2017-2019 EclipseSource Munich
https://github.com/eclipsesource/jsonforms
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
import React from "react";
import {
ControlElement,
createDefaultValue,
JsonSchema,
ArrayTranslations,
} from "@jsonforms/core";
import { IconButton, TableRow, Tooltip } from "@mui/material";
import PlusIcon from "../../assets/PlusIcon.svg?react";
import ValidationIcon from "./ValidationIcon";
import NoBorderTableCell from "./NoBorderTableCell";
export interface MaterialTableToolbarProps {
numColumns: number;
errors: string;
label: string;
path: string;
uischema: ControlElement;
schema: JsonSchema;
rootSchema: JsonSchema;
enabled: boolean;
translations: ArrayTranslations;
addItem(path: string, value: any): () => void;
}
const fixedCellSmall = {
paddingLeft: 0,
paddingRight: 0,
};
const TableToolbar = React.memo(function TableToolbar({
numColumns,
errors,
label,
path,
addItem,
schema,
enabled,
translations,
}: MaterialTableToolbarProps) {
return (
<TableRow>
<NoBorderTableCell colSpan={numColumns} sx={{ verticalAlign: "top" }}>
<div className="flex items-center gap-2">
{label && (
<span className="text-xs uppercase font-semibold text-ls-gray-100">
{label}
</span>
)}
{errors.length !== 0 && (
<ValidationIcon id="tooltip-validation" errorMessages={errors} />
)}
</div>
</NoBorderTableCell>
{enabled ? (
<NoBorderTableCell align="right" style={fixedCellSmall}>
<Tooltip
id="tooltip-add"
title={translations.addTooltip}
placement="bottom"
>
<IconButton
aria-label={translations.addAriaLabel}
onClick={addItem(path, createDefaultValue(schema))}
size="large"
sx={{ p: 1 }}
>
<PlusIcon className="text-ls-black" />
</IconButton>
</Tooltip>
</NoBorderTableCell>
) : null}
</TableRow>
);
});
export default TableToolbar;
@@ -0,0 +1,49 @@
/*
The MIT License
Copyright (c) 2017-2019 EclipseSource Munich
https://github.com/eclipsesource/jsonforms
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
import React from "react";
import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
import { Badge, Tooltip, styled } from "@mui/material";
const StyledBadge = styled(Badge)(({ theme }: any) => ({
color: theme.palette.error.main,
}));
export interface ValidationProps {
errorMessages: string;
id: string;
}
const ValidationIcon: React.FC<ValidationProps> = ({ errorMessages, id }) => {
return (
<Tooltip id={id} title={errorMessages}>
<StyledBadge badgeContent={errorMessages.split("\n").length}>
<ErrorOutlineIcon color="inherit" />
</StyledBadge>
</Tooltip>
);
};
export default ValidationIcon;
@@ -0,0 +1,67 @@
// From https://github.com/eclipsesource/jsonforms/blob/master/packages/vanilla-renderers/src/cells/TextAreaCell.tsx
/*
The MIT License
Copyright (c) 2017-2019 EclipseSource Munich
https://github.com/eclipsesource/jsonforms
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
import {
CellProps,
isMultiLineControl,
RankedTester,
rankWith,
} from "@jsonforms/core";
import { withJsonFormsCellProps } from "@jsonforms/react";
import {
withVanillaCellProps,
type VanillaRendererProps,
} from "@jsonforms/vanilla-renderers";
import merge from "lodash/merge";
import { cn } from "../utils/cn";
import { AutosizeTextarea } from "./AutosizeTextarea";
export const TextAreaCell = (props: CellProps & VanillaRendererProps) => {
const { data, className, id, enabled, config, uischema, path, handleChange } =
props;
const appliedUiSchemaOptions = merge({}, config, uischema.options);
return (
<AutosizeTextarea
value={data || ""}
onChange={(value) => handleChange(path, value === "" ? undefined : value)}
className={cn("w-full text-lg", className)}
id={id}
disabled={!enabled}
autoFocus={appliedUiSchemaOptions.focus}
placeholder={appliedUiSchemaOptions.placeholder}
/>
);
};
/**
* Tester for a multi-line string control.
* @type {RankedTester}
*/
// eslint-disable-next-line react-refresh/only-export-components
export const textAreaCellTester: RankedTester = rankWith(2, isMultiLineControl);
// eslint-disable-next-line react-refresh/only-export-components
export default withJsonFormsCellProps(withVanillaCellProps(TextAreaCell));
@@ -0,0 +1,85 @@
// From https://github.com/eclipsesource/jsonforms/blob/master/packages/vanilla-renderers/src/cells/TextAreaCell.tsx
/*
The MIT License
Copyright (c) 2017-2019 EclipseSource Munich
https://github.com/eclipsesource/jsonforms
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
import {
CellProps,
isMultiLineControl,
RankedTester,
rankWith,
} from "@jsonforms/core";
import { withJsonFormsCellProps } from "@jsonforms/react";
import {
withVanillaCellProps,
type VanillaRendererProps,
} from "@jsonforms/vanilla-renderers";
import merge from "lodash/merge";
import { AutosizeTextarea } from "./AutosizeTextarea";
import { cn } from "../utils/cn";
function tryJsonParse(str: string) {
try {
return JSON.parse(str);
} catch {
return str;
}
}
function tryJsonStringify(obj: unknown): string | unknown {
try {
return JSON.stringify(obj);
} catch {
return obj;
}
}
export const TextAreaCell = (props: CellProps & VanillaRendererProps) => {
const { data, className, id, enabled, config, uischema, path, handleChange } =
props;
const appliedUiSchemaOptions = merge({}, config, uischema.options);
return (
<AutosizeTextarea
value={typeof data === "object" ? tryJsonStringify(data) : data ?? ""}
onChange={(value) =>
handleChange(path, value === "" ? undefined : tryJsonParse(value))
}
className={cn("w-full text-lg", className)}
id={id}
disabled={!enabled}
autoFocus={appliedUiSchemaOptions.focus}
placeholder={appliedUiSchemaOptions.placeholder}
/>
);
};
/**
* Tester for a multi-line string control.
* @type {RankedTester}
*/
// eslint-disable-next-line react-refresh/only-export-components
export const textAreaCellTester: RankedTester = rankWith(2, isMultiLineControl);
// eslint-disable-next-line react-refresh/only-export-components
export default withJsonFormsCellProps(withVanillaCellProps(TextAreaCell));
@@ -0,0 +1,164 @@
import { Drawer } from "vaul";
import { ReactNode, useEffect, useMemo, useRef, useState } from "react";
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";
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);
function toggleCopied() {
setCopied(true);
if (cbRef.current != null) window.clearTimeout(cbRef.current);
cbRef.current = window.setTimeout(() => setCopied(false), 1500);
}
useEffect(() => {
return () => {
if (cbRef.current != null) {
window.clearTimeout(cbRef.current);
}
};
}, []);
return (
<button
className="px-3 py-1"
onClick={() => {
navigator.clipboard.writeText(props.value).then(toggleCopied);
}}
>
{copied ? <CheckCircleIcon /> : <CopyIcon />}
</button>
);
}
export function ShareDialog(props: { config: unknown; children: ReactNode }) {
const hash = useMemo(() => {
return compressToEncodedURIComponent(JSON.stringify(props.config));
}, [props.config]);
const state = getStateFromUrl(window.location.href);
// get base URL
const targetUrl = `${state.basePath}/c/${hash}`;
// .../c/[hash]/playground
const playgroundUrl = `${targetUrl}/playground`;
// cURL, JS: .../c/[hash]/invoke
// Python: .../c/[hash]
const invokeUrl = `${targetUrl}/invoke`;
const pythonSnippet = `
from langserve import RemoteRunnable
chain = RemoteRunnable("${targetUrl}")
chain.invoke({ ... })
`;
const typescriptSnippet = `
import { RemoteRunnable } from "langchain/runnables/remote";
const chain = new RemoteRunnable({ url: \`${invokeUrl}\` });
const result = await chain.invoke({ ... });
`;
return (
<Drawer.Root>
<Drawer.Trigger asChild>{props.children}</Drawer.Trigger>
<Drawer.Portal>
<Drawer.Overlay className="fixed inset-0 bg-black/40" />
<Drawer.Content className="flex justify-center items-center mt-24 fixed bottom-0 left-0 right-0 text-ls-black !pointer-events-none after:!bg-background">
<div className="p-4 bg-background max-w-[calc(800px-2rem)] rounded-t-2xl border border-divider-500 border-b-background pointer-events-auto">
<h3 className="text-xl font-medium">Share</h3>
<hr className="border-divider-500 my-4 -mx-4" />
<div className="flex flex-col gap-3">
{playgroundUrl.length < URL_LENGTH_LIMIT && (
<div className="flex flex-col gap-2 p-3 rounded-2xl dark:bg-[#2C2C2E] bg-gray-100">
<div className="flex items-center gap-3">
<div className="w-10 h-10 flex items-center justify-center text-center text-sm bg-background rounded-xl">
🦜
</div>
<span className="font-semibold">Playground</span>
</div>
<div className="grid grid-cols-[auto,1fr,auto] dark:bg-[#111111] bg-white rounded-xl text-sm items-center">
<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>
</div>
)}
<div className="flex flex-col gap-2 p-3 rounded-2xl dark:bg-[#2C2C2E] bg-gray-100">
<div className="flex items-center gap-3">
<div className="w-10 h-10 flex items-center justify-center text-center text-sm bg-background rounded-xl">
<CodeIcon />
</div>
<span className="font-semibold">Get the code</span>
</div>
{targetUrl.length < URL_LENGTH_LIMIT && (
<div className="grid grid-cols-[1fr,auto] dark:bg-[#111111] bg-white rounded-xl text-sm items-center">
<div className="overflow-auto whitespace-nowrap px-3 py-3 no-scrollbar text-ls-gray-100">
Python SDK
</div>
<CopyButton value={pythonSnippet.trim()} />
</div>
)}
{invokeUrl.length < URL_LENGTH_LIMIT && (
<div className="grid grid-cols-[1fr,auto] dark:bg-[#111111] bg-white rounded-xl text-sm items-center">
<div className="overflow-auto whitespace-nowrap px-3 py-3 no-scrollbar text-ls-gray-100">
TypeScript SDK
</div>
<CopyButton value={typescriptSnippet.trim()} />
</div>
)}
</div>
</div>
</div>
</Drawer.Content>
</Drawer.Portal>
</Drawer.Root>
);
}
+4
View File
@@ -0,0 +1,4 @@
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
ReactDOM.createRoot(document.getElementById("root")!).render(<App />);
@@ -0,0 +1,5 @@
declare module "json-schema-defaults" {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function defaults(schema: any): any;
export = defaults;
}
+71
View File
@@ -0,0 +1,71 @@
import { useEffect, useState } from "react";
import { resolveApiUrl } from "./utils/url";
import { simplifySchema } from "./utils/simplifySchema";
import { JsonFormsCore } from "@jsonforms/core";
import { compressToEncodedURIComponent } from "lz-string";
import { useDebounce } from "use-debounce";
declare global {
interface Window {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
CONFIG_SCHEMA?: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
INPUT_SCHEMA?: any;
}
}
export function useSchemas(
configData: Pick<JsonFormsCore, "data" | "errors"> & { defaults: boolean }
) {
const [schemas, setSchemas] = useState({
config: null,
input: null,
});
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;
}
+84
View File
@@ -0,0 +1,84 @@
import { useCallback, useReducer, useState } from "react";
import { applyPatch, Operation } from "fast-json-patch";
import { fetchEventSource } from "@microsoft/fetch-event-source";
import { resolveApiUrl } from "./utils/url";
export interface LogEntry {
// ID of the sub-run.
id: string;
// Name of the object being run.
name: string;
// Type of the object being run, eg. prompt, chain, llm, etc.
type: string;
// List of tags for the run.
tags: string[];
// Key-value pairs of metadata for the run.
metadata: { [key: string]: unknown };
// ISO-8601 timestamp of when the run started.
start_time: string;
// List of LLM tokens streamed by this run, if applicable.
streamed_output_str: string[];
// Final output of this run.
// Only available after the run has finished successfully.
final_output?: unknown;
// ISO-8601 timestamp of when the run ended.
// Only available after the run has finished.
end_time?: string;
}
export interface RunState {
// ID of the run.
id: string;
// List of output chunks streamed by Runnable.stream()
streamed_output: unknown[];
// Final output of the run, usually the result of aggregating (`+`) streamed_output.
// Only available after the run has finished successfully.
final_output?: unknown;
// Map of run names to sub-runs. If filters were supplied, this list will
// contain only the runs that matched the filters.
logs: { [name: string]: LogEntry };
}
function reducer(state: RunState | null, action: Operation[]) {
return applyPatch(state, action, true, false).newDocument;
}
export function useStreamLog() {
const [latest, updateLatest] = useReducer(reducer, null);
const [controller, setController] = useState<AbortController | null>(null);
const startStream = useCallback(async (input: unknown, config: unknown) => {
const controller = new AbortController();
setController(controller);
await fetchEventSource(resolveApiUrl("/stream_log").toString(), {
signal: controller.signal,
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ input, config }),
onmessage(msg) {
if (msg.event === "data") {
updateLatest(JSON.parse(msg.data)?.ops);
}
},
onclose() {
setController(null);
},
onerror(error) {
setController(null);
throw error;
},
});
}, []);
const stopStream = useCallback(() => {
controller?.abort();
setController(null);
}, [controller]);
return {
startStream,
stopStream: controller ? stopStream : undefined,
latest,
};
}
+7
View File
@@ -0,0 +1,7 @@
import clsx from "clsx";
import { ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
+4
View File
@@ -0,0 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
export const JsonRefs: {
resolveRefs(schema: any): Promise<{ resolved: any }>;
};
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,8 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { JsonRefs } from "./json-refs";
// jsonforms doesn't support schemas with root $ref
// so we resolve root $ref and replace it with the actual schema
export function simplifySchema(schema: any) {
return JsonRefs.resolveRefs(schema).then((r: any) => r.resolved);
}
+8
View File
@@ -0,0 +1,8 @@
export function resolveApiUrl(path: string) {
if (import.meta.env.DEV) {
return new URL(path, "http://127.0.0.1:8000");
}
const prefix = window.location.pathname.split("/playground")[0];
return new URL(prefix + path, window.location.origin);
}
+2
View File
@@ -0,0 +1,2 @@
/// <reference types="vite/client" />
/// <reference types="vite-plugin-svgr/client" />
+31
View File
@@ -0,0 +1,31 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {
colors: {
popover: {
DEFAULT: "hsl(var(--popover))",
},
background: {
DEFAULT: "hsl(var(--background))",
},
ls: {
blue: "hsl(211.5, 91.8%, 61.8%)",
black: "hsl(var(--ls-black))",
gray: {
100: "hsl(var(--ls-gray-100))",
200: "hsl(var(--ls-gray-200))",
300: "hsl(var(--ls-gray-300))",
400: "hsl(var(--ls-gray-400))",
},
},
divider: {
500: "hsl(var(--divider-500))",
700: "hsl(var(--divider-700))",
},
},
},
},
plugins: [],
};
+25
View File
@@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
+10
View File
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
+9
View File
@@ -0,0 +1,9 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import svgr from "vite-plugin-svgr";
// https://vitejs.dev/config/
export default defineConfig({
base: "/____LANGSERVE_BASE_URL/",
plugins: [svgr(), react()],
});
File diff suppressed because it is too large Load Diff
+15 -2
View File
@@ -8,6 +8,7 @@ from typing import Any, Union
from langchain.prompts.base import StringPromptValue
from langchain.prompts.chat import ChatPromptValueConcrete
from langchain.schema.agent import AgentAction, AgentActionMessageLog, AgentFinish
from langchain.schema.document import Document
from langchain.schema.messages import (
AIMessage,
@@ -21,11 +22,20 @@ from langchain.schema.messages import (
SystemMessage,
SystemMessageChunk,
)
from pydantic import BaseModel, ValidationError
try:
from pydantic.v1 import BaseModel, ValidationError
except ImportError:
from pydantic import BaseModel, ValidationError
class WellKnownLCObject(BaseModel):
"""A well known LangChain object."""
"""A well known LangChain object.
A pydantic model that defines what constitutes a well known LangChain object.
All well-known objects are allowed to be serialized and de-serialized.
"""
__root__: Union[
Document,
@@ -41,6 +51,9 @@ class WellKnownLCObject(BaseModel):
AIMessageChunk,
StringPromptValue,
ChatPromptValueConcrete,
AgentAction,
AgentFinish,
AgentActionMessageLog,
]
+458 -92
View File
@@ -1,9 +1,18 @@
"""FastAPI integration for langchain runnables.
This code contains integration for langchain runnables with FastAPI.
The main entry point is the `add_routes` function which adds the routes to an existing
FastAPI app or APIRouter.
"""
import json
import re
import weakref
from inspect import isclass
from typing import (
Any,
AsyncIterator,
Dict,
List,
Literal,
Mapping,
Sequence,
@@ -11,19 +20,28 @@ from typing import (
Union,
)
from fastapi import HTTPException, Request
from langchain.callbacks.tracers.log_stream import RunLogPatch
from langchain.load.serializable import Serializable
from langchain.schema.runnable import Runnable
from langchain.schema.runnable.config import merge_configs
from typing_extensions import Annotated
from langserve.lzstring import LZString
from langserve.version import __version__
try:
from pydantic.v1 import BaseModel, create_model
except ImportError:
from pydantic import BaseModel, create_model
from pydantic import BaseModel, Field, create_model
from langserve.playground import serve_playground
from langserve.serialization import simple_dumpd, simple_dumps
from langserve.validation import (
create_batch_request_model,
create_batch_response_model,
create_invoke_request_model,
create_runnable_config_model,
create_invoke_response_model,
create_stream_log_request_model,
create_stream_request_model,
)
@@ -35,62 +53,155 @@ except ImportError:
APIRouter = FastAPI = Any
def _project_dict(d: Mapping, keys: Sequence[str]) -> Dict[str, Any]:
"""Project the given keys from the given dict."""
return {k: d[k] for k in keys if k in d}
def _config_from_hash(config_hash: str) -> Dict[str, Any]:
try:
if not config_hash:
return {}
uncompressed = LZString.decompressFromEncodedURIComponent(config_hash)
parsed = json.loads(uncompressed)
if isinstance(parsed, dict):
return parsed
else:
raise HTTPException(400, "Invalid config hash")
except Exception:
raise HTTPException(400, "Invalid config hash")
class InvokeResponse(BaseModel):
"""Response from invoking a runnable.
A container is used to allow adding additional fields in the future.
"""
output: Any
"""The output of the runnable.
An object that can be serialized to JSON using LangChain serialization.
"""
class BatchResponse(BaseModel):
"""Response from batch invoking runnables.
A container is used to allow adding additional fields in the future.
"""
output: List[Any]
"""The output of the runnable.
An object that can be serialized to JSON using LangChain serialization.
"""
def _unpack_config(
*configs: Union[BaseModel, Mapping, str],
keys: Sequence[str],
model: Type[BaseModel],
) -> Dict[str, Any]:
"""Merge configs, and project the given keys from the merged dict."""
config_dicts = []
for config in configs:
if isinstance(config, str):
config_dicts.append(model(**_config_from_hash(config)).dict())
elif isinstance(config, BaseModel):
config_dicts.append(config.dict())
else:
config_dicts.append(config)
config = merge_configs(*config_dicts)
return {k: config[k] for k in keys if k in config}
def _unpack_input(validated_model: BaseModel) -> Any:
"""Unpack the decoded input from the validated model."""
if hasattr(validated_model, "__root__"):
return validated_model.__root__
model = validated_model.__root__
else:
return validated_model
model = validated_model
if isinstance(model, BaseModel) and not isinstance(model, Serializable):
# If the model is a pydantic model, but not a Serializable, then
# it was created by the server as part of validation and isn't expected
# to be accepted by the runnables as input as a pydantic model,
# instead we need to convert it into a corresponding python dict.
return model.dict()
return model
def _rename_pydantic_model(model: Type[BaseModel], name: str) -> Type[BaseModel]:
"""Rename the given pydantic model to the given name."""
return create_model(
name,
__config__=model.__config__,
**{
fieldname: (
field.annotation,
Field(
field.default,
title=fieldname,
description=field.field_info.description,
),
)
for fieldname, field in model.__fields__.items()
},
)
# This is a global registry of models to avoid creating the same model
# multiple times.
# Duplicated model names break fastapi's openapi generation.
_MODEL_REGISTRY = {}
_SEEN_NAMES = set()
def _resolve_input_type(input_type: Union[Type, BaseModel]) -> BaseModel:
if isclass(input_type) and issubclass(input_type, BaseModel):
input_type_ = input_type
def _replace_non_alphanumeric_with_underscores(s: str) -> str:
"""Replace non-alphanumeric characters with underscores."""
return re.sub(r"[^a-zA-Z0-9]", "_", s)
def _resolve_model(
type_: Union[Type, BaseModel], default_name: str, namespace: str
) -> Type[BaseModel]:
"""Resolve the input type to a BaseModel."""
if isclass(type_) and issubclass(type_, BaseModel):
model = type_
else:
input_type_ = create_model("Input", __root__=(input_type, ...))
model = create_model(default_name, __root__=(type_, ...))
hash_ = input_type_.schema_json()
hash_ = model.schema_json()
if model.__name__ in _SEEN_NAMES and hash_ not in _MODEL_REGISTRY:
# If the model name has been seen before, but the model itself is different
# generate a new name for the model.
model_to_use = _rename_pydantic_model(model, f"{namespace}{model.__name__}")
hash_ = model_to_use.schema_json()
else:
model_to_use = model
if hash_ not in _MODEL_REGISTRY:
_MODEL_REGISTRY[hash_] = input_type_
_SEEN_NAMES.add(model_to_use.__name__)
_MODEL_REGISTRY[hash_] = model_to_use
return _MODEL_REGISTRY[hash_]
def _add_namespace_to_model(namespace: str, model: Type[BaseModel]) -> Type[BaseModel]:
"""Prefix the name of the given model with the given namespace.
Code is used to help avoid name collisions when hosting multiple runnables
that may use the same underlying models.
Args:
namespace: The namespace to use for the model.
model: The model to create a unique name for.
Returns:
A new model with name prepended with the given namespace.
"""
model_with_unique_name = _rename_pydantic_model(
model,
f"{namespace}{model.__name__}",
)
model_with_unique_name.update_forward_refs()
return model_with_unique_name
def _add_tracing_info_to_metadata(config: Dict[str, Any], request: Request) -> None:
"""Add information useful for tracing and debugging purposes.
Args:
config: The config to expand with tracing information.
request: The request to use for expanding the metadata.
"""
metadata = config["metadata"] if "metadata" in config else {}
info = {
"__useragent": request.headers.get("user-agent"),
"__langserve_version": __version__,
}
metadata.update(info)
config["metadata"] = metadata
_APP_SEEN = weakref.WeakSet()
# PUBLIC API
@@ -100,10 +211,22 @@ def add_routes(
*,
path: str = "",
input_type: Union[Type, Literal["auto"], BaseModel] = "auto",
output_type: Union[Type, Literal["auto"], BaseModel] = "auto",
config_keys: Sequence[str] = (),
) -> None:
"""Register the routes on the given FastAPI app or APIRouter.
The following routes are added per runnable under the specified `path`:
* /invoke - for invoking a runnable with a single input
* /batch - for invoking a runnable with multiple inputs
* /stream - for streaming the output of a runnable
* /stream_log - for streaming intermediate outputs for a runnable
* /input_schema - for returning the input schema of the runnable
* /output_schema - for returning the output schema of the runnable
* /config_schema - for returning the config schema of the runnable
Args:
app: The FastAPI app or APIRouter to which routes should be added.
runnable: The runnable to wrap, must not be stateful.
@@ -111,6 +234,9 @@ def add_routes(
input_type: type to use for input validation.
Default is "auto" which will use the InputType of the runnable.
User is free to provide a custom type annotation.
output_type: type to use for output validation.
Default is "auto" which will use the OutputType of the runnable.
User is free to provide a custom type annotation.
config_keys: list of config keys that will be accepted, by default
no config keys are accepted.
"""
@@ -123,107 +249,347 @@ def add_routes(
"Use `pip install sse_starlette` to install."
)
if input_type == "auto":
input_type_ = _resolve_input_type(runnable.input_schema)
else:
input_type_ = _resolve_input_type(input_type)
if hasattr(app, "openapi_tags") and app not in _APP_SEEN:
_APP_SEEN.add(app)
app.openapi_tags = [
*(getattr(app, "openapi_tags", []) or []),
{
"name": "default",
},
{
"name": "config",
"description": "Endpoints with a default configuration set by `config_hash` path parameter.", # noqa: E501
},
]
namespace = path or ""
model_namespace = path.strip("/").replace("/", "_")
model_namespace = _replace_non_alphanumeric_with_underscores(path.strip("/"))
config = create_runnable_config_model(model_namespace, config_keys)
InvokeRequest = create_invoke_request_model(model_namespace, input_type_, config)
BatchRequest = create_batch_request_model(model_namespace, input_type_, config)
# Stream request is the same as invoke request, but with a different response type
StreamRequest = create_stream_request_model(model_namespace, input_type_, config)
StreamLogRequest = create_stream_log_request_model(
model_namespace, input_type_, config
input_type_ = _resolve_model(
runnable.input_schema if input_type == "auto" else input_type,
"Input",
model_namespace,
)
output_type_ = _resolve_model(
runnable.output_schema if output_type == "auto" else output_type,
"Output",
model_namespace,
)
ConfigPayload = _add_namespace_to_model(
model_namespace, runnable.config_schema(include=config_keys)
)
InvokeRequest = create_invoke_request_model(
model_namespace, input_type_, ConfigPayload
)
BatchRequest = create_batch_request_model(
model_namespace, input_type_, ConfigPayload
)
StreamRequest = create_stream_request_model(
model_namespace, input_type_, ConfigPayload
)
StreamLogRequest = create_stream_log_request_model(
model_namespace, input_type_, ConfigPayload
)
# Generate the response models
InvokeResponse = create_invoke_response_model(model_namespace, output_type_)
BatchResponse = create_batch_response_model(model_namespace, output_type_)
@app.post(
f"{namespace}/invoke",
namespace + "/c/{config_hash}/invoke",
response_model=InvokeResponse,
tags=["config"],
)
@app.post(f"{namespace}/invoke", response_model=InvokeResponse)
async def invoke(
request: Annotated[InvokeRequest, InvokeRequest]
invoke_request: Annotated[InvokeRequest, InvokeRequest],
request: Request,
config_hash: str = "",
) -> InvokeResponse:
"""Invoke the runnable with the given input and config."""
# Request is first validated using InvokeRequest which takes into account
# config_keys as well as input_type.
# After validation, the input is loaded using LangChain's load function.
config = _project_dict(request.config, config_keys)
config = _unpack_config(
config_hash, invoke_request.config, keys=config_keys, model=ConfigPayload
)
_add_tracing_info_to_metadata(config, request)
output = await runnable.ainvoke(
_unpack_input(request.input), config=config, **request.kwargs
_unpack_input(invoke_request.input), config=config
)
return InvokeResponse(output=simple_dumpd(output))
#
@app.post(
namespace + "/c/{config_hash}/batch",
response_model=BatchResponse,
tags=["config"],
)
@app.post(f"{namespace}/batch", response_model=BatchResponse)
async def batch(request: Annotated[BatchRequest, BatchRequest]) -> BatchResponse:
async def batch(
batch_request: Annotated[BatchRequest, BatchRequest],
request: Request,
config_hash: str = "",
) -> BatchResponse:
"""Invoke the runnable with the given inputs and config."""
if isinstance(request.config, list):
config = [_project_dict(config, config_keys) for config in request.config]
if isinstance(batch_request.config, list):
config = [
_unpack_config(
config_hash, config, keys=config_keys, model=ConfigPayload
)
for config in batch_request.config
]
for c in config:
_add_tracing_info_to_metadata(c, request)
else:
config = _project_dict(request.config, config_keys)
inputs = [_unpack_input(input_) for input_ in request.inputs]
output = await runnable.abatch(inputs, config=config, **request.kwargs)
config = _unpack_config(
config_hash, batch_request.config, keys=config_keys, model=ConfigPayload
)
_add_tracing_info_to_metadata(config, request)
inputs = [_unpack_input(input_) for input_ in batch_request.inputs]
output = await runnable.abatch(inputs, config=config)
return BatchResponse(output=simple_dumpd(output))
@app.post(namespace + "/c/{config_hash}/stream", tags=["config"])
@app.post(f"{namespace}/stream")
async def stream(
request: Annotated[StreamRequest, StreamRequest],
stream_request: Annotated[StreamRequest, StreamRequest],
request: Request,
config_hash: str = "",
) -> EventSourceResponse:
"""Invoke the runnable stream the output."""
"""Invoke the runnable stream the output.
This endpoint allows to stream the output of the runnable.
The endpoint uses a server sent event stream to stream the output.
https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events
Important: Set the "text/event-stream" media type for request headers if
not using an existing SDK.
This endpoint uses two different types of events:
* data - for streaming the output of the runnable
{
"event": "data",
"data": {
...
}
}
* error - for signaling an error in the stream, also ends the stream.
{
"event": "error",
"data": {
"status_code": 500,
"message": "Internal Server Error"
}
}
* end - for signaling the end of the stream.
This helps the client to know when to stop listening for events and
know that the streaming has ended successfully.
{
"event": "end",
}
"""
# Request is first validated using InvokeRequest which takes into account
# config_keys as well as input_type.
# After validation, the input is loaded using LangChain's load function.
input_ = _unpack_input(request.input)
config = _project_dict(request.config, config_keys)
input_ = _unpack_input(stream_request.input)
config = _unpack_config(
config_hash, stream_request.config, keys=config_keys, model=ConfigPayload
)
_add_tracing_info_to_metadata(config, request)
async def _stream() -> AsyncIterator[dict]:
"""Stream the output of the runnable."""
async for chunk in runnable.astream(
input_,
config=config,
**request.kwargs,
):
yield {"data": simple_dumps(chunk), "event": "data"}
yield {"event": "end"}
try:
async for chunk in runnable.astream(
input_,
config=config,
):
yield {"data": simple_dumps(chunk), "event": "data"}
yield {"event": "end"}
except BaseException:
yield {
"event": "error",
# Do not expose the error message to the client since
# the message may contain sensitive information.
# We'll add client side errors for validation as well.
"data": json.dumps(
{"status_code": 500, "message": "Internal Server Error"}
),
}
raise
return EventSourceResponse(_stream())
@app.post(namespace + "/c/{config_hash}/stream_log", tags=["config"])
@app.post(f"{namespace}/stream_log")
async def stream_log(
request: Annotated[StreamLogRequest, StreamLogRequest],
stream_log_request: Annotated[StreamLogRequest, StreamLogRequest],
request: Request,
config_hash: str = "",
) -> EventSourceResponse:
"""Invoke the runnable stream the output."""
"""Invoke the runnable stream_log the output.
This endpoint allows to stream the output of the runnable, including
the output of all intermediate steps.
The endpoint uses a server sent event stream to stream the output.
https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events
Important: Set the "text/event-stream" media type for request headers if
not using an existing SDK.
This endpoint uses two different types of events:
* data - for streaming the output of the runnable
{
"event": "data",
"data": {
...
}
}
* error - for signaling an error in the stream, also ends the stream.
{
"event": "error",
"data": {
"status_code": 500,
"message": "Internal Server Error"
}
}
* end - for signaling the end of the stream.
This helps the client to know when to stop listening for events and
know that the streaming has ended successfully.
{
"event": "end",
}
"""
# Request is first validated using InvokeRequest which takes into account
# config_keys as well as input_type.
# After validation, the input is loaded using LangChain's load function.
input_ = _unpack_input(request.input)
config = _project_dict(request.config, config_keys)
input_ = _unpack_input(stream_log_request.input)
config = _unpack_config(
config_hash,
stream_log_request.config,
keys=config_keys,
model=ConfigPayload,
)
_add_tracing_info_to_metadata(config, request)
async def _stream_log() -> AsyncIterator[dict]:
"""Stream the output of the runnable."""
async for run_log_patch in runnable.astream_log(
input_,
config=config,
include_names=request.include_names,
include_types=request.include_types,
include_tags=request.include_tags,
exclude_names=request.exclude_names,
exclude_types=request.exclude_types,
exclude_tags=request.exclude_tags,
**request.kwargs,
):
# Temporary adapter
try:
async for chunk in runnable.astream_log(
input_,
config=config,
diff=True,
include_names=stream_log_request.include_names,
include_types=stream_log_request.include_types,
include_tags=stream_log_request.include_tags,
exclude_names=stream_log_request.exclude_names,
exclude_types=stream_log_request.exclude_types,
exclude_tags=stream_log_request.exclude_tags,
):
if not isinstance(chunk, RunLogPatch):
raise AssertionError(
f"Expected a RunLog instance got {type(chunk)}"
)
data = {
"ops": chunk.ops,
}
# Temporary adapter
yield {
"data": simple_dumps(data),
"event": "data",
}
yield {"event": "end"}
except BaseException:
yield {
"data": simple_dumps({"ops": run_log_patch.ops}),
"event": "data",
"event": "error",
# Do not expose the error message to the client since
# the message may contain sensitive information.
# We'll add client side errors for validation as well.
"data": json.dumps(
{"status_code": 500, "message": "Internal Server Error"}
),
}
yield {"event": "end"}
raise
return EventSourceResponse(_stream_log())
@app.get(namespace + "/c/{config_hash}/input_schema", tags=["config"])
@app.get(f"{namespace}/input_schema")
async def input_schema(config_hash: str = "") -> Any:
"""Return the input schema of the runnable."""
return (
runnable.with_config(
_unpack_config(config_hash, keys=config_keys, model=ConfigPayload)
).input_schema.schema()
if input_type == "auto"
else input_type_.schema()
)
@app.get(namespace + "/c/{config_hash}/output_schema", tags=["config"])
@app.get(f"{namespace}/output_schema")
async def output_schema(config_hash: str = "") -> Any:
"""Return the output schema of the runnable."""
return (
runnable.with_config(
_unpack_config(config_hash, keys=config_keys, model=ConfigPayload)
).output_schema.schema()
if output_type_ == "auto"
else output_type_.schema()
)
@app.get(namespace + "/c/{config_hash}/config_schema", tags=["config"])
@app.get(f"{namespace}/config_schema")
async def config_schema(config_hash: str = "") -> Any:
"""Return the config schema of the runnable."""
return (
runnable.with_config(
_unpack_config(config_hash, keys=config_keys, model=ConfigPayload)
)
.config_schema(include=config_keys)
.schema()
)
@app.get(
namespace + "/c/{config_hash}/playground/{file_path:path}",
tags=["config"],
include_in_schema=False,
)
@app.get(namespace + "/playground/{file_path:path}", include_in_schema=False)
async def playground(file_path: str, config_hash: str = "") -> Any:
"""Return the playground of the runnable."""
return await serve_playground(
runnable.with_config(
_unpack_config(config_hash, keys=config_keys, model=ConfigPayload)
),
runnable.with_config(
_unpack_config(config_hash, keys=config_keys, model=ConfigPayload)
).input_schema
if input_type == "auto"
else input_type_,
config_keys,
f"{namespace}/playground",
file_path,
)
+161 -43
View File
@@ -1,50 +1,63 @@
from typing import List, Optional, Sequence, Type, Union
"""Code to dynamically create pydantic models for validating requests and responses.
from langchain.schema.runnable import RunnableConfig
Requests share the same basic shape of input, config, and kwargs.
Invoke and Batch responses use an `output` key for the output, other keys may
be added to the response at a later date.
Responses for stream and stream_log are specified as those endpoints use
a streaming response.
Type information for input, config and output can be specified by the user
per runnable. This type information will be used for validation of the input and
output and will appear in the OpenAPI spec for the corresponding endpoint.
Models are created with a namespace to avoid name collisions when hosting
multiple runnables. When present the name collisions prevent fastapi from
generating OpenAPI specs.
"""
from typing import List, Optional, Sequence, Union
try:
from pydantic.v1 import BaseModel, Field, create_model
except ImportError:
from pydantic import BaseModel, Field, create_model
from typing_extensions import TypedDict
from typing_extensions import Type, TypedDict
InputValidator = Union[Type[BaseModel], type]
# The following langchain objects are considered to be safe to load.
# Type that is either a python annotation or a pydantic model that can be
# used to validate the input or output of a runnable.
Validator = Union[Type[BaseModel], type]
# PUBLIC API
def create_runnable_config_model(
ns: str, config_keys: Sequence[str]
) -> type(TypedDict):
"""Create a projection of the runnable config type.
Args:
ns: The namespace of the runnable config type.
config_keys: The keys to include in the projection.
"""
subset_dict = {}
for key in config_keys:
if key in RunnableConfig.__annotations__:
subset_dict[key] = RunnableConfig.__annotations__[key]
else:
raise AssertionError(f"Key {key} not in RunnableConfig.")
return TypedDict(f"{ns}RunnableConfig", subset_dict, total=False)
def create_invoke_request_model(
namespace: str,
input_type: InputValidator,
input_type: Validator,
config: TypedDict,
) -> Type[BaseModel]:
"""Create a pydantic model for the invoke request."""
invoke_request_type = create_model(
f"{namespace}InvokeRequest",
input=(input_type, ...),
config=(config, Field(default_factory=dict)),
kwargs=(dict, Field(default_factory=dict)),
input=(input_type, Field(..., description="The input to the runnable.")),
config=(
config,
Field(
default_factory=dict,
description=(
"Subset of RunnableConfig object in LangChain. "
"Useful for passing information like tags, metadata etc."
),
),
),
kwargs=(
dict,
Field(
default_factory=dict,
description="Keyword arguments to the runnable. Currently ignored.",
),
),
)
invoke_request_type.update_forward_refs()
return invoke_request_type
@@ -52,15 +65,30 @@ def create_invoke_request_model(
def create_stream_request_model(
namespace: str,
input_type: InputValidator,
input_type: Validator,
config: TypedDict,
) -> Type[BaseModel]:
"""Create a pydantic model for the invoke request."""
"""Create a pydantic model for the stream request."""
stream_request_model = create_model(
f"{namespace}StreamRequest",
input=(input_type, ...),
config=(config, Field(default_factory=dict)),
kwargs=(dict, Field(default_factory=dict)),
input=(input_type, Field(..., description="The input to the runnable.")),
config=(
config,
Field(
default_factory=dict,
description=(
"Subset of RunnableConfig object in LangChain. "
"Useful for passing information like tags, metadata etc."
),
),
),
kwargs=(
dict,
Field(
default_factory=dict,
description="Keyword arguments to the runnable. Currently ignored.",
),
),
)
stream_request_model.update_forward_refs()
return stream_request_model
@@ -68,15 +96,31 @@ def create_stream_request_model(
def create_batch_request_model(
namespace: str,
input_type: InputValidator,
input_type: Validator,
config: TypedDict,
) -> Type[BaseModel]:
"""Create a pydantic model for the batch request."""
batch_request_type = create_model(
f"{namespace}BatchRequest",
inputs=(List[input_type], ...),
config=(Union[config, List[config]], Field(default_factory=dict)),
kwargs=(dict, Field(default_factory=dict)),
config=(
Union[config, List[config]],
Field(
default_factory=dict,
description=(
"Subset of RunnableConfig object in LangChain. Either specify one "
"config for all inputs or a list of configs with one per input. "
"Useful for passing information like tags, metadata etc."
),
),
),
kwargs=(
dict,
Field(
default_factory=dict,
description="Keyword arguments to the runnable. Currently ignored.",
),
),
)
batch_request_type.update_forward_refs()
return batch_request_type
@@ -84,7 +128,7 @@ def create_batch_request_model(
def create_stream_log_request_model(
namespace: str,
input_type: InputValidator,
input_type: Validator,
config: TypedDict,
) -> Type[BaseModel]:
"""Create a pydantic model for the invoke request."""
@@ -92,13 +136,87 @@ def create_stream_log_request_model(
f"{namespace}StreamLogRequest",
input=(input_type, ...),
config=(config, Field(default_factory=dict)),
include_names=(Optional[Sequence[str]], None),
include_types=(Optional[Sequence[str]], None),
include_tags=(Optional[Sequence[str]], None),
exclude_names=(Optional[Sequence[str]], None),
exclude_types=(Optional[Sequence[str]], None),
exclude_tags=(Optional[Sequence[str]], None),
include_names=(
Optional[Sequence[str]],
Field(
None,
description="If specified, filter to runnables with matching names",
),
),
include_types=(
Optional[Sequence[str]],
Field(
None,
description="If specified, filter to runnables with matching types",
),
),
include_tags=(
Optional[Sequence[str]],
Field(
None,
description="If specified, filter to runnables with matching tags",
),
),
exclude_names=(
Optional[Sequence[str]],
Field(
None,
description="If specified, exclude runnables with matching names",
),
),
exclude_types=(
Optional[Sequence[str]],
Field(
None,
description="If specified, exclude runnables with matching types",
),
),
exclude_tags=(
Optional[Sequence[str]],
Field(
None,
description="If specified, exclude runnables with matching tags",
),
),
kwargs=(dict, Field(default_factory=dict)),
)
stream_log_request.update_forward_refs()
return stream_log_request
def create_invoke_response_model(
namespace: str,
output_type: Validator,
) -> Type[BaseModel]:
"""Create a pydantic model for the invoke response."""
# The invoke response uses a key called `output` for the output, so
# other information can be added to the response at a later date.
invoke_response_type = create_model(
f"{namespace}InvokeResponse",
output=(output_type, Field(..., description="The output of the invocation.")),
)
invoke_response_type.update_forward_refs()
return invoke_response_type
def create_batch_response_model(
namespace: str,
output_type: Validator,
) -> Type[BaseModel]:
"""Create a pydantic model for the batch response."""
# The response uses a key called `output` for the output, so
# other information can be added to the response at a later date.
batch_response_type = create_model(
f"{namespace}BatchResponse",
output=(
List[output_type],
Field(
...,
description=(
"The outputs corresponding to the inputs the batch request."
),
),
),
)
batch_response_type.update_forward_refs()
return batch_response_type
+9
View File
@@ -0,0 +1,9 @@
"""Main entrypoint into package."""
from importlib import metadata
try:
__version__ = metadata.version(__package__)
except metadata.PackageNotFoundError:
# Case where package metadata is not available.
__version__ = ""
del metadata # optional, avoids polluting the results of dir(__package__)
+24
View File
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+2
View File
@@ -0,0 +1,2 @@
# LangServe Playground 🦜️🔗
+30
View File
@@ -0,0 +1,30 @@
{
"name": "langserve-playground",
"version": "0.0.1",
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"license": "MIT",
"author": "LangChain",
"scripts": {
"build": "tsup src/index.tsx",
"dev": "tsup src/index.tsx --watch"
},
"files": [
"dist"
],
"keywords": [
"langserve",
"playground"
],
"devDependencies": {
"@types/react": "^18.2.29",
"prettier": "^3.0.3",
"tsup": "^7.2.0",
"typescript": "^5.2.2"
},
"peerDependencies": {
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
}
}
+109
View File
@@ -0,0 +1,109 @@
"use client";
import React, { useEffect } from "react";
import { useLayoutEffect, useRef, useState } from "react";
export function LangServePlayground(props: {
baseUrl: string;
value: Record<string, unknown>;
onChange: (value: Record<string, unknown>) => void;
}) {
const [open, setOpen] = useState(false);
const ref = useRef<HTMLIFrameElement>(null);
const onChangeRef = useRef(props.onChange);
onChangeRef.current = props.onChange;
const onUpdateRef = useRef<(() => void) | null>(null);
onUpdateRef.current = () =>
ref.current?.contentWindow?.postMessage(
{ type: "update", value: { config: props.value } },
"*"
);
useEffect(() => void onUpdateRef.current?.(), [props.value, open]);
useLayoutEffect(() => {
function listener(event: MessageEvent) {
// Check the event origin to ensure it comes from the expected iframe
if (event.source === ref.current?.contentWindow) {
const message = event.data;
if (typeof message === "object" && message != null) {
switch (message.type) {
case "init": {
onUpdateRef.current?.();
break;
}
case "close": {
setOpen(false);
break;
}
case "apply": {
const value: {
targetUrl: string;
config: Record<string, unknown>;
} = message.value;
onChangeRef.current?.(value.config);
setOpen(false);
break;
}
}
}
}
}
window.addEventListener("message", listener);
return () => window.removeEventListener("message", listener);
}, []);
let iframeSrc = props.baseUrl;
if (iframeSrc.endsWith("/")) iframeSrc = iframeSrc.slice(0, -1);
iframeSrc += "/playground";
return (
<div
style={{
display: "flex",
position: "fixed",
bottom: "1rem",
right: "1rem",
flexDirection: "column",
alignItems: "flex-end",
gap: "1rem",
}}
>
{open && (
<iframe
src={iframeSrc}
style={{
minWidth: 360,
minHeight: 600,
borderRadius: 16,
background: "#fff",
}}
ref={ref}
/>
)}
<button
style={{
border: "none",
background: "#fff",
width: 52,
height: 52,
fontSize: 24,
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "100%",
cursor: "pointer",
}}
onClick={() => {
setOpen((open) => !open);
}}
>
🦜
</button>
</div>
);
}
+8
View File
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"jsx": "react",
"moduleResolution": "node",
"esModuleInterop": true,
"lib": ["es2015", "dom"]
}
}
+16
View File
@@ -0,0 +1,16 @@
import { defineConfig } from "tsup";
export default defineConfig({
minify: true,
target: "es2018",
external: ["react"],
sourcemap: true,
dts: true,
format: ["esm", "cjs"],
injectStyle: true,
esbuildOptions(options) {
options.banner = {
js: '"use client"',
};
},
});
+784
View File
@@ -0,0 +1,784 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@esbuild/android-arm64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz#984b4f9c8d0377443cc2dfcef266d02244593622"
integrity sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==
"@esbuild/android-arm@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.18.20.tgz#fedb265bc3a589c84cc11f810804f234947c3682"
integrity sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==
"@esbuild/android-x64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.18.20.tgz#35cf419c4cfc8babe8893d296cd990e9e9f756f2"
integrity sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==
"@esbuild/darwin-arm64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz#08172cbeccf95fbc383399a7f39cfbddaeb0d7c1"
integrity sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==
"@esbuild/darwin-x64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz#d70d5790d8bf475556b67d0f8b7c5bdff053d85d"
integrity sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==
"@esbuild/freebsd-arm64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz#98755cd12707f93f210e2494d6a4b51b96977f54"
integrity sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==
"@esbuild/freebsd-x64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz#c1eb2bff03915f87c29cece4c1a7fa1f423b066e"
integrity sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==
"@esbuild/linux-arm64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz#bad4238bd8f4fc25b5a021280c770ab5fc3a02a0"
integrity sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==
"@esbuild/linux-arm@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz#3e617c61f33508a27150ee417543c8ab5acc73b0"
integrity sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==
"@esbuild/linux-ia32@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz#699391cccba9aee6019b7f9892eb99219f1570a7"
integrity sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==
"@esbuild/linux-loong64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz#e6fccb7aac178dd2ffb9860465ac89d7f23b977d"
integrity sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==
"@esbuild/linux-mips64el@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz#eeff3a937de9c2310de30622a957ad1bd9183231"
integrity sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==
"@esbuild/linux-ppc64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz#2f7156bde20b01527993e6881435ad79ba9599fb"
integrity sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==
"@esbuild/linux-riscv64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz#6628389f210123d8b4743045af8caa7d4ddfc7a6"
integrity sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==
"@esbuild/linux-s390x@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz#255e81fb289b101026131858ab99fba63dcf0071"
integrity sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==
"@esbuild/linux-x64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz#c7690b3417af318a9b6f96df3031a8865176d338"
integrity sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==
"@esbuild/netbsd-x64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz#30e8cd8a3dded63975e2df2438ca109601ebe0d1"
integrity sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==
"@esbuild/openbsd-x64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz#7812af31b205055874c8082ea9cf9ab0da6217ae"
integrity sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==
"@esbuild/sunos-x64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz#d5c275c3b4e73c9b0ecd38d1ca62c020f887ab9d"
integrity sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==
"@esbuild/win32-arm64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz#73bc7f5a9f8a77805f357fab97f290d0e4820ac9"
integrity sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==
"@esbuild/win32-ia32@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz#ec93cbf0ef1085cc12e71e0d661d20569ff42102"
integrity sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==
"@esbuild/win32-x64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz#786c5f41f043b07afb1af37683d7c33668858f6d"
integrity sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==
"@jridgewell/gen-mapping@^0.3.2":
version "0.3.3"
resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098"
integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==
dependencies:
"@jridgewell/set-array" "^1.0.1"
"@jridgewell/sourcemap-codec" "^1.4.10"
"@jridgewell/trace-mapping" "^0.3.9"
"@jridgewell/resolve-uri@^3.1.0":
version "3.1.1"
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721"
integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==
"@jridgewell/set-array@^1.0.1":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72"
integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==
"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14":
version "1.4.15"
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
"@jridgewell/trace-mapping@^0.3.9":
version "0.3.20"
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz#72e45707cf240fa6b081d0366f8265b0cd10197f"
integrity sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==
dependencies:
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==
dependencies:
"@nodelib/fs.stat" "2.0.5"
run-parallel "^1.1.9"
"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
version "2.0.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b"
integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
"@nodelib/fs.walk@^1.2.3":
version "1.2.8"
resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a"
integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
dependencies:
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
"@types/prop-types@*":
version "15.7.9"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.9.tgz#b6f785caa7ea1fe4414d9df42ee0ab67f23d8a6d"
integrity sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g==
"@types/react@^18.2.29":
version "18.2.29"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.29.tgz#88b48a287e00f6fdcd6f95662878fb701ae18b27"
integrity sha512-Z+ZrIRocWtdD70j45izShRwDuiB4JZqDegqMFW/I8aG5DxxLKOzVNoq62UIO82v9bdgi+DO1jvsb9sTEZUSm+Q==
dependencies:
"@types/prop-types" "*"
"@types/scheduler" "*"
csstype "^3.0.2"
"@types/scheduler@*":
version "0.16.4"
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.4.tgz#fedc3e5b15c26dc18faae96bf1317487cb3658cf"
integrity sha512-2L9ifAGl7wmXwP4v3pN4p2FLhD0O1qsJpvKmNin5VA8+UvNVb447UDaAEV6UdrkA+m/Xs58U1RFps44x6TFsVQ==
any-promise@^1.0.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f"
integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==
anymatch@~3.1.2:
version "3.1.3"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==
dependencies:
normalize-path "^3.0.0"
picomatch "^2.0.4"
array-union@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
balanced-match@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
binary-extensions@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
dependencies:
balanced-match "^1.0.0"
concat-map "0.0.1"
braces@^3.0.2, braces@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
dependencies:
fill-range "^7.0.1"
bundle-require@^4.0.0:
version "4.0.2"
resolved "https://registry.yarnpkg.com/bundle-require/-/bundle-require-4.0.2.tgz#65fc74ff14eabbba36d26c9a6161bd78fff6b29e"
integrity sha512-jwzPOChofl67PSTW2SGubV9HBQAhhR2i6nskiOThauo9dzwDUgOWQScFVaJkjEfYX+UXiD+LEx8EblQMc2wIag==
dependencies:
load-tsconfig "^0.2.3"
cac@^6.7.12:
version "6.7.14"
resolved "https://registry.yarnpkg.com/cac/-/cac-6.7.14.tgz#804e1e6f506ee363cb0e3ccbb09cad5dd9870959"
integrity sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==
chokidar@^3.5.1:
version "3.5.3"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
dependencies:
anymatch "~3.1.2"
braces "~3.0.2"
glob-parent "~5.1.2"
is-binary-path "~2.1.0"
is-glob "~4.0.1"
normalize-path "~3.0.0"
readdirp "~3.6.0"
optionalDependencies:
fsevents "~2.3.2"
commander@^4.0.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068"
integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
cross-spawn@^7.0.3:
version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
dependencies:
path-key "^3.1.0"
shebang-command "^2.0.0"
which "^2.0.1"
csstype@^3.0.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b"
integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==
debug@^4.3.1:
version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
dependencies:
ms "2.1.2"
dir-glob@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==
dependencies:
path-type "^4.0.0"
esbuild@^0.18.2:
version "0.18.20"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.18.20.tgz#4709f5a34801b43b799ab7d6d82f7284a9b7a7a6"
integrity sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==
optionalDependencies:
"@esbuild/android-arm" "0.18.20"
"@esbuild/android-arm64" "0.18.20"
"@esbuild/android-x64" "0.18.20"
"@esbuild/darwin-arm64" "0.18.20"
"@esbuild/darwin-x64" "0.18.20"
"@esbuild/freebsd-arm64" "0.18.20"
"@esbuild/freebsd-x64" "0.18.20"
"@esbuild/linux-arm" "0.18.20"
"@esbuild/linux-arm64" "0.18.20"
"@esbuild/linux-ia32" "0.18.20"
"@esbuild/linux-loong64" "0.18.20"
"@esbuild/linux-mips64el" "0.18.20"
"@esbuild/linux-ppc64" "0.18.20"
"@esbuild/linux-riscv64" "0.18.20"
"@esbuild/linux-s390x" "0.18.20"
"@esbuild/linux-x64" "0.18.20"
"@esbuild/netbsd-x64" "0.18.20"
"@esbuild/openbsd-x64" "0.18.20"
"@esbuild/sunos-x64" "0.18.20"
"@esbuild/win32-arm64" "0.18.20"
"@esbuild/win32-ia32" "0.18.20"
"@esbuild/win32-x64" "0.18.20"
execa@^5.0.0:
version "5.1.1"
resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd"
integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==
dependencies:
cross-spawn "^7.0.3"
get-stream "^6.0.0"
human-signals "^2.1.0"
is-stream "^2.0.0"
merge-stream "^2.0.0"
npm-run-path "^4.0.1"
onetime "^5.1.2"
signal-exit "^3.0.3"
strip-final-newline "^2.0.0"
fast-glob@^3.2.9:
version "3.3.1"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4"
integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==
dependencies:
"@nodelib/fs.stat" "^2.0.2"
"@nodelib/fs.walk" "^1.2.3"
glob-parent "^5.1.2"
merge2 "^1.3.0"
micromatch "^4.0.4"
fastq@^1.6.0:
version "1.15.0"
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a"
integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==
dependencies:
reusify "^1.0.4"
fill-range@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
dependencies:
to-regex-range "^5.0.1"
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
fsevents@~2.3.2:
version "2.3.3"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
get-stream@^6.0.0:
version "6.0.1"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7"
integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==
glob-parent@^5.1.2, glob-parent@~5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
dependencies:
is-glob "^4.0.1"
glob@7.1.6:
version "7.1.6"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^3.0.4"
once "^1.3.0"
path-is-absolute "^1.0.0"
globby@^11.0.3:
version "11.1.0"
resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b"
integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==
dependencies:
array-union "^2.1.0"
dir-glob "^3.0.1"
fast-glob "^3.2.9"
ignore "^5.2.0"
merge2 "^1.4.1"
slash "^3.0.0"
human-signals@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
ignore@^5.2.0:
version "5.2.4"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324"
integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==
inflight@^1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==
dependencies:
once "^1.3.0"
wrappy "1"
inherits@2:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
is-binary-path@~2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
dependencies:
binary-extensions "^2.0.0"
is-extglob@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
is-glob@^4.0.1, is-glob@~4.0.1:
version "4.0.3"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
dependencies:
is-extglob "^2.1.1"
is-number@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
is-stream@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077"
integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==
isexe@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
joycon@^3.0.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03"
integrity sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==
lilconfig@^2.0.5:
version "2.1.0"
resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52"
integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==
lines-and-columns@^1.1.6:
version "1.2.4"
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
load-tsconfig@^0.2.3:
version "0.2.5"
resolved "https://registry.yarnpkg.com/load-tsconfig/-/load-tsconfig-0.2.5.tgz#453b8cd8961bfb912dea77eb6c168fe8cca3d3a1"
integrity sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==
lodash.sortby@^4.7.0:
version "4.7.0"
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==
merge-stream@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
merge2@^1.3.0, merge2@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
micromatch@^4.0.4:
version "4.0.5"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
dependencies:
braces "^3.0.2"
picomatch "^2.3.1"
mimic-fn@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
minimatch@^3.0.4:
version "3.1.2"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
dependencies:
brace-expansion "^1.1.7"
ms@2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
mz@^2.7.0:
version "2.7.0"
resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32"
integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==
dependencies:
any-promise "^1.0.0"
object-assign "^4.0.1"
thenify-all "^1.0.0"
normalize-path@^3.0.0, normalize-path@~3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
npm-run-path@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==
dependencies:
path-key "^3.0.0"
object-assign@^4.0.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
once@^1.3.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
dependencies:
wrappy "1"
onetime@^5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e"
integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==
dependencies:
mimic-fn "^2.1.0"
path-is-absolute@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==
path-key@^3.0.0, path-key@^3.1.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
path-type@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
pirates@^4.0.1:
version "4.0.6"
resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9"
integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==
postcss-load-config@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-4.0.1.tgz#152383f481c2758274404e4962743191d73875bd"
integrity sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==
dependencies:
lilconfig "^2.0.5"
yaml "^2.1.1"
prettier@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.0.3.tgz#432a51f7ba422d1469096c0fdc28e235db8f9643"
integrity sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==
punycode@^2.1.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f"
integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==
queue-microtask@^1.2.2:
version "1.2.3"
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
readdirp@~3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
dependencies:
picomatch "^2.2.1"
resolve-from@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69"
integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==
reusify@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
rollup@^3.2.5:
version "3.29.4"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.29.4.tgz#4d70c0f9834146df8705bfb69a9a19c9e1109981"
integrity sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==
optionalDependencies:
fsevents "~2.3.2"
run-parallel@^1.1.9:
version "1.2.0"
resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==
dependencies:
queue-microtask "^1.2.2"
shebang-command@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
dependencies:
shebang-regex "^3.0.0"
shebang-regex@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
signal-exit@^3.0.3:
version "3.0.7"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
slash@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
source-map@0.8.0-beta.0:
version "0.8.0-beta.0"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.8.0-beta.0.tgz#d4c1bb42c3f7ee925f005927ba10709e0d1d1f11"
integrity sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==
dependencies:
whatwg-url "^7.0.0"
strip-final-newline@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad"
integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==
sucrase@^3.20.3:
version "3.34.0"
resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.34.0.tgz#1e0e2d8fcf07f8b9c3569067d92fbd8690fb576f"
integrity sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==
dependencies:
"@jridgewell/gen-mapping" "^0.3.2"
commander "^4.0.0"
glob "7.1.6"
lines-and-columns "^1.1.6"
mz "^2.7.0"
pirates "^4.0.1"
ts-interface-checker "^0.1.9"
thenify-all@^1.0.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726"
integrity sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==
dependencies:
thenify ">= 3.1.0 < 4"
"thenify@>= 3.1.0 < 4":
version "3.3.1"
resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.1.tgz#8932e686a4066038a016dd9e2ca46add9838a95f"
integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==
dependencies:
any-promise "^1.0.0"
to-regex-range@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
dependencies:
is-number "^7.0.0"
tr46@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09"
integrity sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==
dependencies:
punycode "^2.1.0"
tree-kill@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc"
integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==
ts-interface-checker@^0.1.9:
version "0.1.13"
resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699"
integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==
tsup@^7.2.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/tsup/-/tsup-7.2.0.tgz#bb24c0d5e436477900c712e42adc67200607303c"
integrity sha512-vDHlczXbgUvY3rWvqFEbSqmC1L7woozbzngMqTtL2PGBODTtWlRwGDDawhvWzr5c1QjKe4OAKqJGfE1xeXUvtQ==
dependencies:
bundle-require "^4.0.0"
cac "^6.7.12"
chokidar "^3.5.1"
debug "^4.3.1"
esbuild "^0.18.2"
execa "^5.0.0"
globby "^11.0.3"
joycon "^3.0.1"
postcss-load-config "^4.0.1"
resolve-from "^5.0.0"
rollup "^3.2.5"
source-map "0.8.0-beta.0"
sucrase "^3.20.3"
tree-kill "^1.2.2"
typescript@^5.2.2:
version "5.2.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78"
integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==
webidl-conversions@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==
whatwg-url@^7.0.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06"
integrity sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==
dependencies:
lodash.sortby "^4.7.0"
tr46 "^1.0.1"
webidl-conversions "^4.0.2"
which@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
dependencies:
isexe "^2.0.0"
wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
yaml@^2.1.1:
version "2.3.3"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.3.tgz#01f6d18ef036446340007db8e016810e5d64aad9"
integrity sha512-zw0VAJxgeZ6+++/su5AFoqBbZbrEakwu+X0M5HmcwUiBL7AzcuPKjj5we4xfQLp78LkEMpD0cOnUhmgOVy3KdQ==
Generated
+906 -812
View File
File diff suppressed because it is too large Load Diff
+15 -6
View File
@@ -1,23 +1,28 @@
[tool.poetry]
name = "langserve"
version = "0.0.2"
version = "0.0.15"
description = ""
readme = "README.md"
authors = ["LangChain"]
license = "LangServe"
repository = "https://github.com/langchain-ai/langserve"
exclude = ["langserve/playground"]
include = ["langserve/playground/dist/**/*"]
[tool.poetry.dependencies]
python = ">3.8.1,<4"
httpx = "^0.23.0" # May be able to decrease this version
fastapi = {version = ">0.90.1", optional = true}
python = "^3.8.1"
httpx = ">=0.23.0" # May be able to decrease this version
fastapi = {version = ">=0.90.1", optional = true}
sse-starlette = {version = "^1.3.0", optional = true}
httpx-sse = {version = "^0.3.1", optional = true}
httpx-sse = {version = ">=0.3.1", optional = true}
pydantic = "^1"
langchain = ">=0.0.305"
langchain = ">=0.0.316"
[tool.poetry.group.dev.dependencies]
jupyterlab = "^3.6.1"
fastapi = ">=0.90.1"
sse-starlette = "^1.3.0"
httpx-sse = ">=0.3.1"
[tool.poetry.group.typing.dependencies]
@@ -32,10 +37,14 @@ pytest-cov = "^4.0.0"
pytest-asyncio = "^0.21.1"
pytest-mock = "^3.11.1"
pytest-socket = "^0.6.0"
pytest-watch = "^4.2.0"
[tool.poetry.group.examples.dependencies]
openai = "^0.28.0"
uvicorn = {extras = ["standard"], version = "^0.23.2"}
fastapi = ">=0.90.1"
sse-starlette = "^1.3.0"
httpx-sse = ">=0.3.1"
[tool.poetry.extras]
# Extras that are used for client
View File
-160
View File
@@ -1,160 +0,0 @@
import json
from typing import Any
import pytest
from langchain.schema.messages import (
HumanMessage,
SystemMessage,
)
try:
from pydantic.v1 import BaseModel
except ImportError:
from pydantic import BaseModel
from langserve.serialization import simple_dumps, simple_loads
@pytest.mark.parametrize(
"data, expected_json",
[
# Test with python primitives
(1, 1),
([], []),
({}, {}),
({"a": 1}, {"a": 1}),
(
{"output": [HumanMessage(content="hello")]},
{
"output": [
{
"content": "hello",
"additional_kwargs": {},
"type": "human",
"example": False,
}
]
},
),
# Test with a single message (HumanMessage)
(
HumanMessage(content="Hello"),
{
"additional_kwargs": {},
"content": "Hello",
"example": False,
"type": "human",
},
),
# Test with a list containing mixed elements
(
[HumanMessage(content="Hello"), SystemMessage(content="Hi"), 42, "world"],
[
{
"additional_kwargs": {},
"content": "Hello",
"example": False,
"type": "human",
},
{
"additional_kwargs": {},
"content": "Hi",
"type": "system",
},
42,
"world",
],
),
# Uncomment when langchain 0.0.306 is released
# # Attention: This test is not correct right now
# # Test with full and chunk messages
# (
# [HumanMessage(content="Hello"), HumanMessageChunk(content="Hi")],
# [
# {
# "additional_kwargs": {},
# "content": "Hello",
# "example": False,
# "type": "human",
# },
# {
# "additional_kwargs": {},
# "content": "Hi",
# "example": False,
# "type": "human",
# },
# ],
# ),
# # Attention: This test is not correct right now
# # Test with full and chunk messages
# (
# [HumanMessageChunk(content="Hello"), HumanMessage(content="Hi")],
# [
# {
# "additional_kwargs": {},
# "content": "Hello",
# "example": False,
# "type": "human",
# },
# {
# "additional_kwargs": {},
# "content": "Hi",
# "example": False,
# "type": "human",
# },
# ],
# ),
# Test with a dictionary containing mixed elements
(
{
"message": HumanMessage(content="Greetings"),
"numbers": [1, 2, 3],
"boom": "Hello, world!",
},
{
"message": {
"additional_kwargs": {},
"content": "Greetings",
"example": False,
"type": "human",
},
"numbers": [1, 2, 3],
"boom": "Hello, world!",
},
),
],
)
def test_serialization(data: Any, expected_json: Any) -> None:
"""Test that the LangChainEncoder encodes the data as expected."""
# Test encoding
assert json.loads(simple_dumps(data)) == expected_json
# Test decoding
assert simple_loads(json.dumps(expected_json)) == data
# Test full representation are equivalent including the pydantic model classes
assert _get_full_representation(data) == _get_full_representation(
simple_loads(json.dumps(expected_json))
)
def _get_full_representation(data: Any) -> Any:
"""Get the full representation of the data, replacing pydantic models with schema.
Pydantic tests two different models for equality based on equality
of their schema; instead we will rely on the equality of their full
schema representation. This will make sure that both models have the
same name (e.g., HumanMessage vs. HumanMessageChunk).
Args:
data: python primitives + pydantic models
Returns:
data represented entirely with python primitives
"""
if isinstance(data, dict):
return {key: _get_full_representation(value) for key, value in data.items()}
elif isinstance(data, list):
return [_get_full_representation(value) for value in data]
elif isinstance(data, BaseModel):
return data.schema()
else:
return data
+24
View File
@@ -0,0 +1,24 @@
from langserve.lzstring import LZString
def test_lzstring() -> None:
s = "Žluťoučký kůň úpěl ďábelské ódy!"
# generated with original js lib
jsLzStringBase64 = (
"r6ABsK6KaAD2aLCADWBfgBPQ9oCAlAZAvgDobEARlB4QAEOAjAUxAGd4BL5AZ4BMBPAQiA=="
)
jsLzStringBase64Json = "N4Ig5gNg9gzjCGAnAniAXKALgS0xApuiPgB7wC2ADgQASSwIogA0IA4tHACLYBu6WXASIBlFu04wAMthiYBEhgFEAdpiYYQASS6i2AWSniRURJgCCMPYfEcGAFXyJyozPBUATJB5pt8Kp3gIbAAvfB99JABrAFdKGil3MBj4MEJWcwBjRCgVZBc0EBEDIwyAIzLEfH5CrREAeRoADiaAdgBONABGdqaANltJLnwAMwVKJHgicxpyfDcAWnJouJoIJJS05hoYmHCaTCgabPx4THxZlfj1lWTU/BgaGBjMgAsaeEeuKEyAISgoFEAHSDBgifD4cwQGBQdAAbXYNlYAA0bABdAC+rDscHBhEKy0QsUoIAxZLJQA" # noqa: E501
compressed = LZString.compressToBase64(s)
assert LZString.compressToBase64(s) == jsLzStringBase64
assert LZString.decompressFromBase64(compressed) == LZString.decompressFromBase64(
jsLzStringBase64
)
assert s == LZString.decompressFromBase64(compressed)
jsonString = '{"glossary":{"title":"example glossary","GlossDiv":{"title":"S","GlossList":{"GlossEntry":{"ID":"SGML","SortAs":"SGML","GlossTerm":"Standard Generalized Markup Language","Acronym":"SGML","Abbrev":"ISO 8879:1986","GlossDef":{"para":"A meta-markup language, used to create markup languages such as DocBook.","GlossSeeAlso":["GML","XML"]},"GlossSee":"markup"}}}}}' # noqa: E501
compressed = LZString.compressToBase64(jsonString)
assert compressed == jsLzStringBase64Json
assert LZString.decompressFromBase64(compressed) == jsonString
+77
View File
@@ -0,0 +1,77 @@
from typing import Any
import pytest
from langchain.schema.messages import (
HumanMessage,
HumanMessageChunk,
SystemMessage,
)
try:
from pydantic.v1 import BaseModel
except ImportError:
from pydantic import BaseModel
from langserve.serialization import simple_dumps, simple_loads
@pytest.mark.parametrize(
"data",
[
# Test with python primitives
1,
[],
{},
{"a": 1},
{"output": [HumanMessage(content="hello")]},
# Test with a single message (HumanMessage)
HumanMessage(content="Hello"),
# Test with a list containing mixed elements
[HumanMessage(content="Hello"), SystemMessage(content="Hi"), 42, "world"],
# Uncomment when langchain 0.0.306 is released
[HumanMessage(content="Hello"), HumanMessageChunk(content="Hi")],
# Attention: This test is not correct right now
# Test with full and chunk messages
[HumanMessageChunk(content="Hello"), HumanMessage(content="Hi")],
# Test with a dictionary containing mixed elements
{
"message": HumanMessage(content="Greetings"),
"numbers": [1, 2, 3],
"boom": "Hello, world!",
},
],
)
def test_serialization(data: Any) -> None:
"""There and back again! :)"""
# Test encoding
assert isinstance(simple_dumps(data), str)
# Test simple equality (does not include pydantic class names)
assert simple_loads(simple_dumps(data)) == data
# Test full representation equality (includes pydantic class names)
assert _get_full_representation(
simple_loads(simple_dumps(data))
) == _get_full_representation(data)
def _get_full_representation(data: Any) -> Any:
"""Get the full representation of the data, replacing pydantic models with schema.
Pydantic tests two different models for equality based on equality
of their schema; instead we will rely on the equality of their full
schema representation. This will make sure that both models have the
same name (e.g., HumanMessage vs. HumanMessageChunk).
Args:
data: python primitives + pydantic models
Returns:
data represented entirely with python primitives
"""
if isinstance(data, dict):
return {key: _get_full_representation(value) for key, value in data.items()}
elif isinstance(data, list):
return [_get_full_representation(value) for value in data]
elif isinstance(data, BaseModel):
return data.schema()
else:
return data
+566 -33
View File
@@ -1,8 +1,9 @@
"""Test the server and client together."""
import asyncio
import json
from asyncio import AbstractEventLoop
from contextlib import asynccontextmanager
from typing import List, Optional, Union
from contextlib import asynccontextmanager, contextmanager
from typing import Any, Dict, Iterator, List, Optional, Sequence, Union
import httpx
import pytest
@@ -11,13 +12,29 @@ from fastapi import FastAPI
from fastapi.testclient import TestClient
from httpx import AsyncClient
from langchain.callbacks.tracers.log_stream import RunLogPatch
from langchain.prompts import PromptTemplate
from langchain.prompts.base import StringPromptValue
from langchain.schema.messages import HumanMessage, SystemMessage
from langchain.schema.runnable import RunnablePassthrough
from langchain.schema.runnable import Runnable, RunnableConfig, RunnablePassthrough
from langchain.schema.runnable.base import RunnableLambda
from langchain.schema.runnable.utils import ConfigurableField, Input, Output
from pytest_mock import MockerFixture
from typing_extensions import TypedDict
from langserve import server
from langserve.client import RemoteRunnable
from langserve.server import add_routes
from langserve.lzstring import LZString
from langserve.server import (
_rename_pydantic_model,
_replace_non_alphanumeric_with_underscores,
add_routes,
)
from tests.unit_tests.utils import FakeListLLM
try:
from pydantic.v1 import BaseModel, Field
except ImportError:
from pydantic import BaseModel, Field
@pytest.fixture(scope="session")
@@ -46,7 +63,30 @@ def app(event_loop: AbstractEventLoop) -> FastAPI:
runnable_lambda = RunnableLambda(func=add_one_or_passthrough)
app = FastAPI()
try:
add_routes(app, runnable_lambda)
add_routes(app, runnable_lambda, config_keys=["tags"])
yield app
finally:
del app
@pytest.fixture()
def app_for_config(event_loop: AbstractEventLoop) -> FastAPI:
"""A simple server that wraps a Runnable and exposes it as an API."""
async def return_config(
_: int,
config: RunnableConfig,
) -> Dict[str, Any]:
"""Add one to int or passthrough."""
return {
"tags": sorted(config["tags"]),
"configurable": config.get("configurable"),
}
runnable_lambda = RunnableLambda(func=return_config)
app = FastAPI()
try:
add_routes(app, runnable_lambda, config_keys=["tags", "metadata"])
yield app
finally:
del app
@@ -64,14 +104,19 @@ def client(app: FastAPI) -> RemoteRunnable:
@asynccontextmanager
async def get_async_client(
server: FastAPI, path: Optional[str] = None
server: FastAPI, *, path: Optional[str] = None, raise_app_exceptions: bool = True
) -> RemoteRunnable:
"""Get an async client."""
url = "http://localhost:9999"
if path:
url += path
remote_runnable_client = RemoteRunnable(url=url)
async_client = AsyncClient(app=server, base_url=url)
transport = httpx.ASGITransport(
app=server,
raise_app_exceptions=raise_app_exceptions,
)
async_client = AsyncClient(app=server, base_url=url, transport=transport)
remote_runnable_client.async_client = async_client
try:
yield remote_runnable_client
@@ -79,6 +124,25 @@ async def get_async_client(
await async_client.aclose()
@contextmanager
def get_sync_client(
server: FastAPI, *, path: Optional[str] = None, raise_server_exceptions: bool = True
) -> RemoteRunnable:
"""Get an async client."""
url = "http://localhost:9999"
if path:
url += path
remote_runnable_client = RemoteRunnable(url=url)
sync_client = TestClient(
app=server, base_url=url, raise_server_exceptions=raise_server_exceptions
)
remote_runnable_client.sync_client = sync_client
try:
yield remote_runnable_client
finally:
sync_client.close()
@pytest_asyncio.fixture()
async def async_client(app: FastAPI) -> RemoteRunnable:
"""Create a FastAPI app that exposes the Runnable as an API."""
@@ -100,6 +164,19 @@ def test_server(app: FastAPI) -> None:
"output": [2],
}
# Test schema
input_schema = sync_client.get("/input_schema").json()
assert isinstance(input_schema, dict)
assert input_schema["title"] == "RunnableLambdaInput"
output_schema = sync_client.get("/output_schema").json()
assert isinstance(output_schema, dict)
assert output_schema["title"] == "RunnableLambdaOutput"
output_schema = sync_client.get("/config_schema").json()
assert isinstance(output_schema, dict)
assert output_schema["title"] == "RunnableLambdaConfig"
# TODO(Team): Fix test. Issue with eventloops right now when using sync client
## Test stream
# response = sync_client.post("/stream", json={"input": 1})
@@ -126,6 +203,44 @@ async def test_server_async(app: FastAPI) -> None:
assert response.text == "event: data\r\ndata: 2\r\n\r\nevent: end\r\n\r\n"
@pytest.mark.asyncio
async def test_server_bound_async(app_for_config: FastAPI) -> None:
"""Test the server directly via HTTP requests."""
async_client = AsyncClient(app=app_for_config, base_url="http://localhost:9999")
config_hash = LZString.compressToEncodedURIComponent(json.dumps({"tags": ["test"]}))
# Test invoke
response = await async_client.post(
f"/c/{config_hash}/invoke",
json={"input": 1, "config": {"tags": ["another-one"]}},
)
assert response.status_code == 200
assert response.json() == {
"output": {"tags": ["another-one", "test"], "configurable": None}
}
# Test batch
response = await async_client.post(
f"/c/{config_hash}/batch",
json={"inputs": [1], "config": {"tags": ["another-one"]}},
)
assert response.status_code == 200
assert response.json() == {
"output": [{"tags": ["another-one", "test"], "configurable": None}]
}
# Test stream
response = await async_client.post(
f"/c/{config_hash}/stream",
json={"input": 1, "config": {"tags": ["another-one"]}},
)
assert response.status_code == 200
assert (
response.text
== """event: data\r\ndata: {"tags": ["another-one", "test"], "configurable": null}\r\n\r\nevent: end\r\n\r\n""" # noqa: E501
)
def test_invoke(client: RemoteRunnable) -> None:
"""Test sync invoke."""
assert client.invoke(1) == 2
@@ -188,28 +303,62 @@ async def test_astream(async_client: RemoteRunnable) -> None:
assert outputs == [data]
@pytest.mark.asyncio
async def test_astream_log_diff_no_effect(async_client: RemoteRunnable) -> None:
"""Test async stream."""
run_logs = []
async for chunk in async_client.astream_log(1, diff=False):
run_logs.append(chunk)
op = run_logs[0].ops[0]
uuid = op["value"]["id"]
assert [run_log_patch.ops for run_log_patch in run_logs] == [
[
{
"op": "replace",
"path": "",
"value": {
"final_output": {"output": 2},
"id": uuid,
"logs": {},
"streamed_output": [],
},
}
],
[{"op": "replace", "path": "/final_output", "value": {"output": 2}}],
[{"op": "add", "path": "/streamed_output/-", "value": 2}],
]
@pytest.mark.asyncio
async def test_astream_log(async_client: RemoteRunnable) -> None:
"""Test async stream."""
outputs = []
run_log_patches = []
async for chunk in async_client.astream_log(1):
outputs.append(chunk)
async for chunk in async_client.astream_log(1, diff=True):
run_log_patches.append(chunk)
assert len(outputs) == 3
op = outputs[0].ops[0]
op = run_log_patches[0].ops[0]
uuid = op["value"]["id"]
assert op == {
"op": "replace",
"path": "",
"value": {
"final_output": {"output": 2},
"id": uuid,
"logs": [],
"streamed_output": [],
},
}
assert [run_log_patch.ops for run_log_patch in run_log_patches] == [
[
{
"op": "replace",
"path": "",
"value": {
"final_output": {"output": 2},
"id": uuid,
"logs": {},
"streamed_output": [],
},
}
],
[{"op": "replace", "path": "/final_output", "value": {"output": 2}}],
[{"op": "add", "path": "/streamed_output/-", "value": 2}],
]
def test_invoke_as_part_of_sequence(client: RemoteRunnable) -> None:
@@ -301,7 +450,7 @@ async def test_invoke_as_part_of_sequence_async(async_client: RemoteRunnable) ->
"value": {
"final_output": None,
"id": first_op["value"]["id"],
"logs": [],
"logs": {},
"streamed_output": [],
},
}
@@ -328,6 +477,12 @@ async def test_multiple_runnables(event_loop: AbstractEventLoop) -> None:
path="/mul_2",
)
add_routes(app, PromptTemplate.from_template("{question}"), path="/prompt_1")
add_routes(
app, PromptTemplate.from_template("{question} {answer}"), path="/prompt_2"
)
async with get_async_client(app, path="/add_one") as runnable:
async with get_async_client(app, path="/mul_2") as runnable2:
assert await runnable.ainvoke(1) == 2
@@ -340,6 +495,16 @@ async def test_multiple_runnables(event_loop: AbstractEventLoop) -> None:
composite_runnable_2 = runnable | add_one | runnable2
assert await composite_runnable_2.ainvoke(3) == 10
async with get_async_client(app, path="/prompt_1") as runnable:
assert await runnable.ainvoke(
{"question": "What is your name?"}
) == StringPromptValue(text="What is your name?")
async with get_async_client(app, path="/prompt_2") as runnable:
assert await runnable.ainvoke(
{"question": "What is your name?", "answer": "Bob"}
) == StringPromptValue(text="What is your name? Bob")
@pytest.mark.asyncio
async def test_input_validation(
@@ -351,8 +516,8 @@ async def test_input_validation(
"""Add one to simulate a valid function"""
return x + 1
server_runnable = RunnableLambda(func=add_one, afunc=add_one)
server_runnable2 = RunnableLambda(func=add_one, afunc=add_one)
server_runnable = RunnableLambda(func=add_one)
server_runnable2 = RunnableLambda(func=add_one)
app = FastAPI()
add_routes(
@@ -367,7 +532,7 @@ async def test_input_validation(
server_runnable2,
input_type=int,
path="/add_one_config",
config_keys=["tags", "run_name"],
config_keys=["tags", "run_name", "metadata"],
)
async with get_async_client(app, path="/add_one") as runnable:
@@ -380,7 +545,7 @@ async def test_input_validation(
with pytest.raises(httpx.HTTPError):
await runnable.abatch(["hello"])
config = {"tags": ["test"]}
config = {"tags": ["test"], "metadata": {"a": 5}}
invoke_spy_1 = mocker.spy(server_runnable, "ainvoke")
# Verify config is handled correctly
@@ -388,14 +553,24 @@ async def test_input_validation(
# Verify that can be invoked with valid input
# Config ignored for runnable1
assert await runnable1.ainvoke(1, config=config) == 2
assert invoke_spy_1.call_args[1]["config"] == {}
# Config should be ignored but default debug information
# will still be added
config_seen = invoke_spy_1.call_args[1]["config"]
assert "metadata" in config_seen
assert "__useragent" in config_seen["metadata"]
assert "__langserve_version" in config_seen["metadata"]
invoke_spy_2 = mocker.spy(server_runnable2, "ainvoke")
async with get_async_client(app, path="/add_one_config") as runnable2:
# Config accepted for runnable2
assert await runnable2.ainvoke(1, config=config) == 2
# Config ignored
assert invoke_spy_2.call_args[1]["config"] == config
config_seen = invoke_spy_2.call_args[1]["config"]
assert config_seen["tags"] == ["test"]
assert config_seen["metadata"]["a"] == 5
assert "__useragent" in config_seen["metadata"]
assert "__langserve_version" in config_seen["metadata"]
@pytest.mark.asyncio
@@ -420,6 +595,12 @@ async def test_input_validation_with_lc_types(event_loop: AbstractEventLoop) ->
# Valid
result = await passthrough_runnable.ainvoke([HumanMessage(content="hello")])
# Valid
result = await passthrough_runnable.ainvoke(
[HumanMessage(content="hello")], config={"tags": ["test"]}
)
assert isinstance(result, list)
assert isinstance(result[0], HumanMessage)
@@ -477,21 +658,373 @@ async def test_openapi_docs_with_identical_runnables(
server_runnable = RunnableLambda(func=add_one)
server_runnable2 = RunnableLambda(func=add_one)
server_runnable3 = PromptTemplate.from_template("say {name}")
server_runnable4 = PromptTemplate.from_template("say {name} {hello}")
app = FastAPI()
add_routes(
app,
server_runnable,
path="/1",
path="/a",
)
# Add another route that uses the same schema (inferred from runnable input schema)
add_routes(
app,
server_runnable2,
path="/2",
config_keys=["tags", "run_name"],
path="/b",
config_keys=["tags"],
)
add_routes(
app,
server_runnable3,
path="/c",
config_keys=["tags"],
)
add_routes(
app,
server_runnable4,
path="/d",
config_keys=["tags"],
)
async with AsyncClient(app=app, base_url="http://localhost:9999") as async_client:
response = await async_client.get("/openapi.json")
assert response.status_code == 200
@pytest.mark.asyncio
async def test_configurable_runnables(event_loop: AbstractEventLoop) -> None:
"""Add tests for using langchain's configurable runnables"""
template = PromptTemplate.from_template("say {name}").configurable_fields(
template=ConfigurableField(
id="template",
name="Template",
description="The template to use for the prompt",
)
)
llm = (
RunnablePassthrough() | RunnableLambda(lambda prompt: prompt.text)
).configurable_alternatives(
ConfigurableField(
id="llm",
name="LLM",
),
hardcoded_llm=FakeListLLM(responses=["hello Mr. Kitten!"]),
)
chain = template | llm
# Check server side
assert chain.invoke({"name": "cat"}) == "say cat"
app = FastAPI()
add_routes(app, chain, config_keys=["tags", "configurable"])
async with get_async_client(app) as remote_runnable:
# Test with hard-coded LLM
assert chain.invoke({"name": "cat"}) == "say cat"
# Test with different prompt
assert (
await remote_runnable.ainvoke(
{"name": "foo"},
{"configurable": {"template": "hear {name}"}, "tags": ["h"]},
)
== "hear foo"
)
# Test with alternative passthrough LLM
assert (
await remote_runnable.ainvoke(
{"name": "foo"},
{"configurable": {"llm": "hardcoded_llm"}, "tags": ["h"]},
)
== "hello Mr. Kitten!"
)
# Test for utilities
@pytest.mark.parametrize(
"s,expected",
[
("hello", "hello"),
("hello world", "hello_world"),
("hello-world", "hello_world"),
("hello_world", "hello_world"),
("hello.world", "hello_world"),
],
)
def test_replace_non_alphanumeric(s: str, expected: str) -> None:
"""Test replace non alphanumeric."""
assert _replace_non_alphanumeric_with_underscores(s) == expected
def test_rename_pydantic_model() -> None:
"""Test rename pydantic model."""
class Foo(BaseModel):
bar: str = Field(..., description="A bar")
baz: str = Field(..., description="A baz")
Model = _rename_pydantic_model(Foo, "Bar")
assert isinstance(Model, type)
assert Model.__name__ == "Bar"
@pytest.mark.asyncio
async def test_input_config_output_schemas(event_loop: AbstractEventLoop) -> None:
"""Test schemas returned for different configurations."""
# TODO(Fix me): need to fix handling of global state -- we get problems
# gives inconsistent results when running multiple tests / results
# depending on ordering
server._SEEN_NAMES = set()
server._MODEL_REGISTRY = {}
async def add_one(x: int) -> int:
"""Add one to simulate a valid function"""
return x + 1
async def add_two(y: int) -> int:
"""Add one to simulate a valid function"""
return y + 2
app = FastAPI()
add_routes(app, RunnableLambda(add_one), path="/add_one")
# Custom input type
add_routes(
app,
RunnableLambda(add_two),
path="/add_two_custom",
input_type=float,
output_type=Sequence[float],
config_keys=["tags", "configurable"],
)
add_routes(app, PromptTemplate.from_template("{question}"), path="/prompt_1")
template = PromptTemplate.from_template("say {name}").configurable_fields(
template=ConfigurableField(
id="template",
name="Template",
description="The template to use for the prompt",
)
)
add_routes(app, template, path="/prompt_2", config_keys=["tags", "configurable"])
async with AsyncClient(app=app, base_url="http://localhost:9999") as async_client:
# input schema
response = await async_client.get("/add_one/input_schema")
assert response.json() == {"title": "RunnableLambdaInput", "type": "integer"}
response = await async_client.get("/add_two_custom/input_schema")
assert response.json() == {"title": "Input", "type": "number"}
response = await async_client.get("/prompt_1/input_schema")
assert response.json() == {
"properties": {"question": {"title": "Question", "type": "string"}},
"title": "PromptInput",
"type": "object",
}
response = await async_client.get("/prompt_2/input_schema")
assert response.json() == {
"properties": {"name": {"title": "Name", "type": "string"}},
"title": "PromptInput",
"type": "object",
}
# output schema
response = await async_client.get("/add_one/output_schema")
assert response.json() == {
"title": "RunnableLambdaOutput",
"type": "integer",
}
response = await async_client.get("/add_two_custom/output_schema")
assert response.json() == {
"items": {"type": "number"},
"title": "Output",
"type": "array",
}
# Just verify that the schema is not empty (it's pretty long)
# and the actual value should be tested in LangChain
response = await async_client.get("/prompt_1/output_schema")
assert response.json() != {} # Long string
response = await async_client.get("/prompt_2/output_schema")
assert response.json() != {} # Long string
## Config schema
response = await async_client.get("/add_one/config_schema")
assert response.json() == {
"properties": {},
"title": "RunnableLambdaConfig",
"type": "object",
}
response = await async_client.get("/add_two_custom/config_schema")
assert response.json() == {
"properties": {
"tags": {"items": {"type": "string"}, "title": "Tags", "type": "array"}
},
"title": "RunnableLambdaConfig",
"type": "object",
}
response = await async_client.get("/prompt_2/config_schema")
assert response.json() == {
"definitions": {
"Configurable": {
"properties": {
"template": {
"default": "say {name}",
"description": "The template to use for the prompt",
"title": "Template",
"type": "string",
}
},
"title": "Configurable",
"type": "object",
}
},
"properties": {
"configurable": {"$ref": "#/definitions/Configurable"},
"tags": {"items": {"type": "string"}, "title": "Tags", "type": "array"},
},
"title": "RunnableConfigurableFieldsConfig",
"type": "object",
}
@pytest.mark.asyncio
async def test_input_schema_typed_dict() -> None:
class InputType(TypedDict):
foo: str
bar: List[int]
async def passthrough_dict(d: Any) -> Any:
return d
runnable_lambda = RunnableLambda(func=passthrough_dict)
app = FastAPI()
add_routes(app, runnable_lambda, input_type=InputType, config_keys=["tags"])
async with AsyncClient(app=app, base_url="http://localhost:9999") as client:
res = await client.get("/input_schema")
assert res.json() == {
"title": "Input",
"allOf": [{"$ref": "#/definitions/InputType"}],
"definitions": {
"InputType": {
"properties": {
"bar": {
"items": {"type": "integer"},
"title": "Bar",
"type": "array",
},
"foo": {"title": "Foo", "type": "string"},
},
"required": ["foo", "bar"],
"title": "InputType",
"type": "object",
}
},
}
class ErroringRunnable(Runnable):
"""A custom runnable for testing errors are raised server side."""
def invoke(self, input: Input, config: Optional[RunnableConfig] = None) -> Output:
"""Invoke the runnable."""
raise ValueError("Server side error")
def stream(
self,
input: Input,
config: Optional[RunnableConfig] = None,
**kwargs: Optional[Any],
) -> Iterator[Output]:
yield 1
yield 2
raise ValueError("An exception occurred")
async def astream(
self,
input: Iterator[Input],
config: Optional[RunnableConfig] = None,
**kwargs: Optional[Any],
) -> Iterator[Output]:
yield 1
yield 2
raise ValueError("An exception occurred")
@pytest.mark.asyncio
async def test_server_side_error() -> None:
"""Test server side error handling."""
app = FastAPI()
add_routes(app, ErroringRunnable())
# Invoke request
async with get_async_client(app, raise_app_exceptions=False) as runnable:
with pytest.raises(httpx.HTTPStatusError) as cm:
assert await runnable.ainvoke(1)
assert isinstance(cm.value, httpx.HTTPStatusError)
with pytest.raises(httpx.HTTPStatusError) as cm:
assert await runnable.abatch([1, 2])
assert isinstance(cm.value, httpx.HTTPStatusError)
# Test astream
chunks = []
try:
async for chunk in runnable.astream({"a": 1}):
chunks.append(chunk)
except httpx.HTTPStatusError as e:
assert chunks == [1, 2]
assert e.response.status_code == 500
assert e.response.text == "Internal Server Error"
# # Failing right now, can uncomment or add callbacks
# # Test astream_log
# chunks = []
# try:
# async for chunk in runnable.astream_log({"a": 1}):
# chunks.append(chunk)
# except httpx.HTTPStatusError as e:
# assert chunks == []
# assert e.response.status_code == 500
# assert e.response.text == "Internal Server Error"
def test_server_side_error_sync() -> None:
"""Test server side error handling."""
app = FastAPI()
add_routes(app, ErroringRunnable())
# Invoke request
with get_sync_client(app, raise_server_exceptions=False) as runnable:
with pytest.raises(httpx.HTTPStatusError) as cm:
assert runnable.invoke(1)
assert isinstance(cm.value, httpx.HTTPStatusError)
with pytest.raises(httpx.HTTPStatusError) as cm:
assert runnable.batch([1, 2])
assert isinstance(cm.value, httpx.HTTPStatusError)
# Test astream
chunks = []
try:
for chunk in runnable.stream({"a": 1}):
chunks.append(chunk)
except httpx.HTTPStatusError as e:
assert chunks == [1, 2]
assert e.response.status_code == 500
assert e.response.text == "Internal Server Error"
+60 -7
View File
@@ -1,6 +1,10 @@
from typing import Optional
from typing import List, Optional
import pytest
from langchain.prompts import PromptTemplate
from langchain.schema.runnable.utils import ConfigurableField
from langserve.server import _unpack_config
try:
from pydantic.v1 import BaseModel, ValidationError
@@ -10,7 +14,6 @@ except ImportError:
from langserve.validation import (
create_batch_request_model,
create_invoke_request_model,
create_runnable_config_model,
)
@@ -65,9 +68,11 @@ def test_create_invoke_and_batch_models(test_case: dict) -> None:
b: Optional[str] = None
valid = test_case.pop("valid")
config = create_runnable_config_model("test", ["tags"])
model = create_invoke_request_model("namespace", Input, config)
class Config(BaseModel):
tags: Optional[List[str]] = None
model = create_invoke_request_model("namespace", Input, Config)
if valid:
model(**test_case)
@@ -78,7 +83,7 @@ def test_create_invoke_and_batch_models(test_case: dict) -> None:
# Validate batch request
# same structure as input request, but
# 'input' is a list of inputs and is called 'inputs'
batch_model = create_batch_request_model("namespace", Input, config)
batch_model = create_batch_request_model("namespace", Input, Config)
test_case["inputs"] = [test_case.pop("input")]
if valid:
@@ -120,11 +125,59 @@ def test_create_invoke_and_batch_models(test_case: dict) -> None:
)
def test_validation(test_case) -> None:
"""Test that the invoke request model is created correctly."""
config = create_runnable_config_model("test", [])
model = create_invoke_request_model("namespace", test_case.pop("type"), config)
class Config(BaseModel):
tags: Optional[List[str]] = None
model = create_invoke_request_model("namespace", test_case.pop("type"), Config)
if test_case["valid"]:
model(**test_case)
else:
with pytest.raises(ValidationError):
model(**test_case)
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(
id="template",
name="Template",
description="The template to use for the prompt",
)
)
config = runnable.config_schema(include=["tags", "run_name", "configurable"])
Model = create_invoke_request_model("", runnable.input_schema, config)
assert (
_unpack_config(
Model(
input={"name": "bob"},
).config,
keys=[],
model=config,
)
== {}
)
# Test that the config is unpacked correctly
request = Model(
input={"name": "bob"},
config={
"tags": ["hello"],
"run_name": "run",
"configurable": {"template": "goodbye {name}"},
},
)
assert request.input == {"name": "bob"}
assert request.config.tags == ["hello"]
assert request.config.run_name == "run"
assert isinstance(request.config.configurable, BaseModel)
assert request.config.configurable.dict() == {
"template": "goodbye {name}",
}
assert _unpack_config(request.config, keys=["configurable"], model=config) == {
"configurable": {"template": "goodbye {name}"},
}
+54
View File
@@ -0,0 +1,54 @@
from typing import Any, List, Mapping, Optional
from langchain.callbacks.manager import (
AsyncCallbackManagerForLLMRun,
CallbackManagerForLLMRun,
)
from langchain.llms.base import LLM
class FakeListLLM(LLM):
"""Fake LLM for testing purposes."""
responses: List[str]
sleep: Optional[float] = None
i: int = 0
@property
def _llm_type(self) -> str:
"""Return type of llm."""
return "fake-list"
def _call(
self,
prompt: str,
stop: Optional[List[str]] = None,
run_manager: Optional[CallbackManagerForLLMRun] = None,
**kwargs: Any,
) -> str:
"""Return next response"""
response = self.responses[self.i]
if self.i < len(self.responses) - 1:
self.i += 1
else:
self.i = 0
return response
async def _acall(
self,
prompt: str,
stop: Optional[List[str]] = None,
run_manager: Optional[AsyncCallbackManagerForLLMRun] = None,
**kwargs: Any,
) -> str:
"""Return next response"""
response = self.responses[self.i]
if self.i < len(self.responses) - 1:
self.i += 1
else:
self.i = 0
return response
@property
def _identifying_params(self) -> Mapping[str, Any]:
return {"responses": self.responses}