Compare commits
119 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3cb32e4618 | |||
| 9f7d672d20 | |||
| 4ba226e40a | |||
| c3f553bc3b | |||
| 4cbc20f6c4 | |||
| 5e00bfee38 | |||
| c4565e85b0 | |||
| 65709a7966 | |||
| 05afa7fb07 | |||
| 23827b3f5c | |||
| c50ec01632 | |||
| 174f47f4f7 | |||
| 6ef8a25f71 | |||
| db1016da62 | |||
| b0935f0266 | |||
| 20f04581cc | |||
| 5ee72a5f24 | |||
| 14b9c5f745 | |||
| 020f4e7ae7 | |||
| 33cf1f322f | |||
| d800c100bf | |||
| 063e10f755 | |||
| 001c924e4e | |||
| 37f4f3c599 | |||
| a54d7df974 | |||
| 4878d832cf | |||
| 24d96b7cac | |||
| 05f11af6e9 | |||
| 144eb0717e | |||
| bd5d8cda86 | |||
| ca06ab2e32 | |||
| 3df9fe8f28 | |||
| d37fce3f57 | |||
| 3534bfccbd | |||
| 0ec68c61f3 | |||
| f0a1831956 | |||
| 62106399dc | |||
| cc8d69eae1 | |||
| 20f2f0135a | |||
| 7225efe27b | |||
| a211178fa6 | |||
| c4e1a3154b | |||
| ee451b143c | |||
| dc23a3c27c | |||
| acef4d78e7 | |||
| 4208e5de26 | |||
| 9d4d56adab | |||
| 73ff692be7 | |||
| fd01b78c01 | |||
| cc6484de5c | |||
| 1bf88e8904 | |||
| 0a8538c743 | |||
| 43a0de08fe | |||
| bd884736ba | |||
| 6eeb274875 | |||
| 779b8e1cc9 | |||
| ada5d103b4 | |||
| 9b5fe1471c | |||
| c0ade4707e | |||
| fb0f9a0dc3 | |||
| e47494cf5b | |||
| dfab212e5b | |||
| 5b2ee137a2 | |||
| 786b7e3a93 | |||
| 972b99ddbf | |||
| eb0e97f8a1 | |||
| 4b2c89386f | |||
| 85320dc2cb | |||
| ce9161d02c | |||
| 2d9e756fa6 | |||
| d2b9570bd3 | |||
| fd8cfc327b | |||
| 5f1b9c800e | |||
| 8c9142948e | |||
| 44e8d7bc4d | |||
| 30a0f70bdf | |||
| ae51653a9c | |||
| e3d2c5f698 | |||
| a150be31c2 | |||
| 7d4bcdc18a | |||
| 8c3aba7475 | |||
| b315ed781b | |||
| e5dd75508c | |||
| 32d4ad89e3 | |||
| 1902209c35 | |||
| e819e9c905 | |||
| 3819dfba72 | |||
| 334f087709 | |||
| 4cbc39765e | |||
| a2966fa661 | |||
| 5216b34b82 | |||
| 034b9d1a29 | |||
| b3e791838a | |||
| 887565db26 | |||
| bb8ac0f7b8 | |||
| c44e3bf200 | |||
| 06fec4868b | |||
| 906e62e5a5 | |||
| 00fdca8d11 | |||
| 0993834cda | |||
| 3849342876 | |||
| d2dc4a9292 | |||
| 5506675152 | |||
| 5f3042861a | |||
| 6ec7ceadc2 | |||
| 90ec2e1032 | |||
| 54c89a2ff7 | |||
| 59390016d8 | |||
| 08c3dd9231 | |||
| d3dd8e0567 | |||
| 9462a8ac32 | |||
| a4ac5b6f40 | |||
| f973c6af3c | |||
| 48baf90ca1 | |||
| e1da4040bd | |||
| 298ae9605f | |||
| 001e165b8f | |||
| 72607daec3 | |||
| 9b87d317f7 |
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
######################
|
||||
|
||||
@@ -2,22 +2,57 @@
|
||||
|
||||
## 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
|
||||
- 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
|
||||
|
||||
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,18 @@ 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
|
||||
```
|
||||
|
||||
### Client
|
||||
|
||||
Python SDK
|
||||
|
||||
```python
|
||||
|
||||
from langchain.schema import SystemMessage, HumanMessage
|
||||
@@ -103,22 +137,94 @@ 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
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
@@ -316,7 +316,7 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.4"
|
||||
"version": "3.10.1"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
|
||||
@@ -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__":
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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__"]
|
||||
|
||||
@@ -3,10 +3,20 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import weakref
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from typing import Any, AsyncIterator, Iterator, List, Optional, Sequence, Union
|
||||
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
|
||||
@@ -102,16 +112,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)
|
||||
@@ -368,6 +410,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 +435,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,6 +458,7 @@ class RemoteRunnable(Runnable[Input, Output]):
|
||||
if sse.event == "data":
|
||||
data = simple_loads(sse.data)
|
||||
chunk = RunLogPatch(*data["ops"])
|
||||
|
||||
yield chunk
|
||||
|
||||
if final_output:
|
||||
|
||||
@@ -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]),
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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 },
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
After Width: | Height: | Size: 40 KiB |
@@ -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-6c8f83bb.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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"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",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
After Width: | Height: | Size: 40 KiB |
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
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">
|
||||
>({ data: {}, errors: [] });
|
||||
|
||||
const [inputData, setInputData] = useState<
|
||||
Pick<JsonFormsCore, "data" | "errors">
|
||||
>({ data: null, errors: [] });
|
||||
// fetch input and config schemas from the server
|
||||
const schemas = useSchemas();
|
||||
// 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: [],
|
||||
});
|
||||
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: [] });
|
||||
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 }) : 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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { resolveApiUrl } from "./utils/url";
|
||||
import { simplifySchema } from "./utils/simplifySchema";
|
||||
|
||||
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() {
|
||||
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();
|
||||
}, []);
|
||||
|
||||
return schemas;
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
export const JsonRefs: {
|
||||
resolveRefs(schema: any): Promise<{ resolved: any }>;
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
/// <reference types="vite/client" />
|
||||
/// <reference types="vite-plugin-svgr/client" />
|
||||
@@ -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: [],
|
||||
};
|
||||
@@ -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" }]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -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()],
|
||||
});
|
||||
@@ -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,
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -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,299 @@ 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": {
|
||||
...
|
||||
}
|
||||
}
|
||||
|
||||
* 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"}
|
||||
|
||||
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": {
|
||||
...
|
||||
}
|
||||
}
|
||||
|
||||
* 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(
|
||||
async for chunk 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,
|
||||
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({"ops": run_log_patch.ops}),
|
||||
"data": simple_dumps(data),
|
||||
"event": "data",
|
||||
}
|
||||
yield {"event": "end"}
|
||||
|
||||
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()
|
||||
|
||||
@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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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__)
|
||||
@@ -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?
|
||||
@@ -0,0 +1,2 @@
|
||||
# LangServe Playground 🦜️🔗
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "react",
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"lib": ["es2015", "dom"]
|
||||
}
|
||||
}
|
||||
@@ -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"',
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -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==
|
||||
@@ -1,23 +1,28 @@
|
||||
[tool.poetry]
|
||||
name = "langserve"
|
||||
version = "0.0.2"
|
||||
version = "0.0.13"
|
||||
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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 typing import Any, Dict, List, Optional, Union
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
@@ -11,13 +12,28 @@ 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.base import RunnableLambda
|
||||
from langchain.schema.runnable import RunnableConfig, RunnablePassthrough
|
||||
from langchain.schema.runnable.base import RunnableLambda, Runnable
|
||||
from langchain.schema.runnable.utils import ConfigurableField
|
||||
from pytest_mock import MockerFixture
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
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 +62,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
|
||||
@@ -100,6 +139,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 +178,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 +278,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 +425,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 +452,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 +470,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 +491,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 +507,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 +520,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 +528,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 +570,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 +633,180 @@ 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_runnable_assign(event_loop: AbstractEventLoop) -> None:
|
||||
"""Test serving multiple runnables."""
|
||||
|
||||
app = FastAPI()
|
||||
chain = RunnablePassthrough().assign(a=RunnableLambda(lambda _: "a"))
|
||||
# should only need "b" as input now
|
||||
add_routes(app, chain, path="/assigned")
|
||||
|
||||
async with get_async_client(app, path="/assigned") as runnable:
|
||||
assert await runnable.ainvoke({"b": "b"}) == {"a": "a", "b": "b"}
|
||||
assert runnable.invoke({"b": "b"}) == {"a": "a", "b": "b"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runnable_assign_nopath(event_loop: AbstractEventLoop) -> None:
|
||||
"""Test serving multiple runnables."""
|
||||
|
||||
app = FastAPI()
|
||||
chain = RunnablePassthrough().assign(a=RunnableLambda(lambda _: "a"))
|
||||
# should only need "b" as input now
|
||||
add_routes(app, chain)
|
||||
|
||||
async with get_async_client(app, path="/") as runnable:
|
||||
assert await runnable.ainvoke({"b": "b"}) == {"a": "a", "b": "b"}
|
||||
assert runnable.invoke({"b": "b"}) == {"a": "a", "b": "b"}
|
||||
|
||||
|
||||
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",
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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}"},
|
||||
}
|
||||
|
||||
@@ -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}
|
||||