Compare commits

...

51 Commits

Author SHA1 Message Date
Eugene Yurtsev a8e954ec9f 0.0.22 (#159)
See release notes for details
2023-11-02 21:41:58 -04:00
Eugene Yurtsev ce17cc4241 Fix bug in streaming associated with additive chunks (#158)
- Fix issue in stream/astream endpoint associated with addable types

- Have not been able to figure how to run sync unit tests yet with
pytest:
- fastapi app is async so there's an event loop that's created somewhere
  - within fastapi sse_starlette also deals with an event loop for the
    streaming endpoint
- This results in a failure when running all unit tests together
(https://github.com/sysid/sse-starlette/issues/68)
2023-11-02 21:39:53 -04:00
Eugene Yurtsev 876201e7f3 Remove names/tags from undocumented versions of endpoints (#155)
Forgot to remove these -- these cause a user warning when viewing docs
2023-11-02 14:46:54 -04:00
Nuno Campos 15816981fd Revert "Remove original playground code" (#152)
Reverts langchain-ai/langserve#151
2023-11-01 18:46:24 +00:00
Nuno Campos dc7da56416 Remove original playground code (#151)
This was moved to other folder weeks ago
2023-11-01 14:27:25 -04:00
Erick Friis 8d8bb49a9d Readme update for new cli (#150)
Fixes #149
2023-11-01 12:40:52 -04:00
jakerachleff cc2f9b886f Return run IDs from invoke/batch endpoints (#148)
Clients of LangServe may be interested in the run_id of each parent run
of LangChain. This can be used, for example, for external tracing
applications like LangSmith. In this PR, we introduce a new dictionary
in the response for the invoke/batch endpoints called "metadata" where
we put the run_id (or run_ids for batch).

### Notes
- Stream is not currently supported since we do not have callbacks set
up around streaming, we can add that later if needed

---------

Co-authored-by: Eugene Yurtsev <eyurtsev@gmail.com>
2023-11-01 09:36:32 -07:00
Eugene Yurtsev 4a7e8d5317 Update examples (#146)
Update examples
2023-10-31 22:14:44 -04:00
Eugene Yurtsev b7639f72c4 Add link to langserve hub templates (#147)
Add link to langserve hub templates
2023-10-31 22:14:19 -04:00
Eugene Yurtsev fdcb53e35b Add image of playground (#145) 2023-10-31 16:06:00 -04:00
Eugene Yurtsev 16da208c7e Bump lock file (#142)
Bump lock file
2023-10-31 14:05:50 -04:00
Eugene Yurtsev 4f6c855f42 Version 0.0.21 (#140)
Release 0.0.21
2023-10-31 13:18:38 -04:00
Eugene Yurtsev 9d69e37c18 Build new version of playground (#139)
Build playground assets
2023-10-31 13:15:41 -04:00
David Duong 6e4967ac31 Fix autotype history fix (#138) 2023-10-31 12:39:47 -04:00
David Duong 9b8e9a46e0 Add number of intermediate steps recorded, disable button (#136)
Closes #120
2023-10-31 12:34:48 -04:00
Brace Sproul 0ddf4b1a02 Fix disabled button state (#124)
Co-authored-by: Eugene Yurtsev <eyurtsev@gmail.com>
2023-10-31 12:33:59 -04:00
Eugene Yurtsev 43df917456 Dynamic validation for streaming endpoints (#133)
- [x] Add test to verify that client side errors are now raised for
streaming endpoints
- [x] Add more tests to verify that config and inputs are being
validated properly -- or introduce more pydantic models
2023-10-31 11:46:01 -04:00
Nuno Campos 4e8a72374d Group routes in API docs by path (#135)
<img width="1264" alt="Screenshot 2023-10-31 at 11 43 31"
src="https://github.com/langchain-ai/langserve/assets/56902/15348bde-d1f0-4b64-b869-9d342584bb77">
2023-10-31 15:32:08 +00:00
Nuno Campos 865140abac Fix crash when mounting same chain twice in same app (#134)
The crash was due to Config.configurable pydantic base model not being
renamed, fixed by recursively renaming field annotations which are
themselves base models
2023-10-31 09:51:46 -04:00
Nuno Campos 4a4ccb7235 Parse input dynamically to ensure new keys arent filtered out by pydantic (#79)
* Adds dynamic input parsing to invoke and batch endpoints
* Separate documentation from validation -- create dummy endpoints for
documentation using pydantic, and handle validation using our own logic
(our validation is more involved)

---------

Co-authored-by: Eugene Yurtsev <eyurtsev@gmail.com>
2023-10-30 22:15:07 -04:00
Eugene Yurtsev 5f2bad663e Version 0.0.20 (#132)
Version 0.0.20
2023-10-30 22:12:33 -04:00
Harrison Chase dcee6ec12e add hosted signup form (#131)
Co-authored-by: Eugene Yurtsev <eyurtsev@gmail.com>
2023-10-30 14:00:42 -07:00
Eugene Yurtsev cb3de13fea Fix bug for adding routes to APIRouter (#130)
-- Disables the startup logs and the check for re-used paths for API
routers.
-- API Router is unhashable, so we'll disable some non-essential
features for API routers
2023-10-30 14:01:24 -04:00
William FH a94b66696f Enforce trailing slash in the client (#126)
Would resolve the error in #125

---------

Co-authored-by: Eugene Yurtsev <eyurtsev@gmail.com>
2023-10-29 22:25:48 -04:00
Jacob Lee 7be2927518 Add logs on startup, global handler method (#123)
We can also add more global one-off endpoints (like a meta/directory UI)
in the handler in the future.

<img width="714" alt="Screenshot 2023-10-27 at 3 27 04 PM"
src="https://github.com/langchain-ai/langserve/assets/6952323/a7f43632-0e24-4ad8-8d52-447b702f929e">
2023-10-28 07:11:34 -07:00
Eugene Yurtsev 1ddf554f9d 0.0.19 (#121)
This release includes some playground fixes
2023-10-27 14:37:08 -04:00
David Duong f10509bb5a Playground UX fixes (#119)
- [x] Reset button
 - [x] Remove default single message
 - [x] Remove default `anyOf` handling
 - [x] `additional_kwargs` are not shown in the output
 - [x] Cmd+Enter on submit
 - [x] Loading spinner

Closes #105, #110
2023-10-27 14:27:49 -04:00
Eugene Yurtsev 14eb1a6ea4 Update README (#117)
Add inforamtion about widgets, input and output types
2023-10-26 22:09:16 -04:00
Eugene Yurtsev d6f4a58d99 Merge pull request #116
* bump version
2023-10-26 21:27:28 -04:00
Eugene Yurtsev 17eda1e85d Widgets (#91)
Add examples for widgets for playground

---------

Co-authored-by: Bagatur <22008038+baskaryan@users.noreply.github.com>
Co-authored-by: Nuno Campos <nuno@boringbits.io>
Co-authored-by: Tat Dat Duong <david@duong.cz>
2023-10-27 00:43:12 +02:00
Eugene Yurtsev 44dc693217 Add a custom user type (#115)
Add a custom user type
2023-10-26 15:31:13 -04:00
Eugene Yurtsev 3a3a1deed6 Add working with files to README.md (#114)
Add working with files
2023-10-26 14:52:51 -04:00
Eugene Yurtsev ef0018823b Add file processing example (#113)
Add file processing example
2023-10-26 14:21:47 -04:00
Eugene Yurtsev 77276eac89 version 0.0.17 (#111)
Release 0.0.17
2023-10-25 21:23:22 -04:00
Eugene Yurtsev 93ab04854d Error on path collisions (#109)
Error on path collisions.
2023-10-25 17:24:53 -04:00
Eugene Yurtsev 20e5f46697 Update input and output type logic if specified via add_routes (#107)
Update logic if specified via add routes
2023-10-25 17:10:42 -04:00
Eugene Yurtsev a0ffcf068a Error on missing / (#108)
Error on missing `/`
2023-10-25 17:10:26 -04:00
Eugene Yurtsev b91d84ac01 Use get config list for batch endpoint (#103)
Use get config list
2023-10-25 15:24:47 -04:00
Eugene Yurtsev b828fb7bae Test playground endpoint (#102)
Test playground file serve
2023-10-25 14:40:27 -04:00
Eugene Yurtsev 96239f87bf Security notice to README (#100)
Add security notice to README
2023-10-25 14:13:13 -04:00
Eugene Yurtsev 03c1fd981f Bump pytest (#101)
Bump pytest
2023-10-25 13:10:55 -04:00
Eugene Yurtsev 21238be7a0 Bump version (#99)
Bump version
2023-10-25 10:59:25 -04:00
David Duong b629689f3b Fix parent root escape (#98) 2023-10-25 10:55:41 -04:00
Nuno Campos 135c0c3ad6 Slash (#97) 2023-10-25 15:19:03 +01:00
Eugene Yurtsev b3166958c8 Serialization fix for callbacks (#95)
- Fix serialization of callbacks to add an LLMResults with concrete generation types
2023-10-25 05:52:04 -04:00
Eugene Yurtsev 94e08e7972 Add server side callback event handling to client (#92)
This PR adds the ability to handle server side events by the remote
runnables.

Prior to landing:

- Blocked on 2 changes in langchain to be released
- Should add ChatGeneration, Generation... to list of well known lc
types (otherwise traces with llms won't work correctly)
2023-10-24 21:21:12 -04:00
Bagatur 20875e0362 very important (#93)
Co-authored-by: Eugene Yurtsev <eyurtsev@gmail.com>
2023-10-24 16:09:31 -04:00
David Duong 73737949f9 New JS build (#90) 2023-10-24 15:56:53 +02:00
David Duong 7f6a23e9c1 Handle streamed message chunks in playground (#76) 2023-10-24 15:52:21 +02:00
David Duong 85c5acf1f6 Chat Messages input in Playground (#80)
https://github.com/langchain-ai/langserve/assets/1443449/a6053321-b4ee-409e-996d-2ff91e88acbb
2023-10-24 15:41:09 +02:00
Eugene Yurtsev 366feed79e Add callback events from server (#88)
This PR allows the server to send callback events to the client.

Limited to the invoke and batch end points for the time being.

This PR does NOT include the client code to use the events (will be
included in a separate PR to keep things simple).

Also updating the serializer to use an interface -- placeholder for
future code to accommodate user adding their own custom pydantic objects.
2023-10-23 22:29:36 -04:00
51 changed files with 4632 additions and 1125 deletions
+1
View File
@@ -13,6 +13,7 @@ on:
- 'langserve/**'
- 'examples/**'
- 'pyproject.toml'
- 'poetry.lock'
- 'Makefile'
workflow_dispatch: # Allows to trigger the workflow manually in GitHub UI
+163 -7
View File
@@ -1,4 +1,6 @@
# LangServe 🦜️🔗
# LangServe 🦜️🏓
🚩 We will be releasing a hosted version of LangServe for one-click deployments of LangChain applications. [Sign up here](https://airtable.com/app0hN6sd93QcKubv/shrAjst60xXa6quV2) to get on the waitlist.
## Overview
@@ -19,28 +21,40 @@ A javascript client is available in [LangChainJS](https://js.langchain.com/docs/
- Built-in (optional) tracing to [LangSmith](https://www.langchain.com/langsmith), just add your API key (see [Instructions](https://docs.smith.langchain.com/)])
- All built with battle-tested open-source Python libraries like FastAPI, Pydantic, uvloop and asyncio.
- Use the client SDK to call a LangServe server as if it was a Runnable running locally (or call the HTTP API directly)
- [LangServe Hub](https://github.com/langchain-ai/langchain/blob/master/templates/README.md)
### Limitations
- 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)
## Hosted LangServe
We will be releasing a hosted version of LangServe for one-click deployments of LangChain applications. [Sign up here](https://airtable.com/app0hN6sd93QcKubv/shrAjst60xXa6quV2) to get on the waitlist.
## Security
* Vulnerability in Versions 0.0.13 - 0.0.15 -- playground endpoint allows accessing arbitrary files on server. [Resolved in 0.0.16](https://github.com/langchain-ai/langserve/pull/98).
## 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]"`)
To use the langchain CLI make sure that you have a recent version of `langchain-cli`
installed. You can install it with `pip install -U "langchain-cli[serve]"`.
```sh
langchain ../path/to/directory
langchain app new ../path/to/directory
```
And follow the instructions...
## Examples
For more examples, see the [examples](./examples) directory.
Get your LangServe instance started quickly with
[LangChain Templates](https://github.com/langchain-ai/langchain/blob/master/templates/README.md).
For more examples, see the templates
[index](https://github.com/langchain-ai/langchain/blob/master/templates/docs/INDEX.md)
or the [examples](./examples) directory.
### Server
@@ -208,6 +222,8 @@ adds of these endpoints to the server:
You can find a playground page for your runnable at `/my_runnable/playground`. This exposes a simple UI to [configure](https://python.langchain.com/docs/expression_language/how_to/configure) and invoke your runnable with streaming output and intermediate steps.
![image](https://github.com/langchain-ai/langserve/assets/3205522/5ca56e29-f1bb-40f4-84b5-15916384a276)
## Installation
For both client and server:
@@ -240,3 +256,143 @@ 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
```
## Advanced
### Files
LLM applications often deal with files. There are different architectures
that can be made to implement file processing; at a high level:
1. The file may be uploaded to the server via a dedicated endpoint and processed using a separate endpoint
2. The file may be uploaded by either value (bytes of file) or reference (e.g., s3 url to file content)
3. The processing endpoint may be blocking or non-blocking
4. If significant processing is required, the processing may be offloaded to a dedicated process pool
You should determine what is the appropriate architecture for your application.
Currently, to upload files by value to a runnable, use base64 encoding for the
file (`multipart/form-data` is not supported yet).
Here's an [example](https://github.com/langchain-ai/langserve/tree/main/examples/file_processing) that shows
how to use base64 encoding to send a file to a remote runnable.
Remember, you can always upload files by reference (e.g., s3 url) or upload them as
multipart/form-data to a dedicated endpoint.
### Custom Input and Output Types
Input and Output types are defined on all runnables.
You can access them via the `input_schema` and `output_schema` properties.
`LangServe` uses these types for validation and documentation.
If you want to override the default inferred types, you can use the `with_types` method.
Here's a toy example to illustrate the idea:
```python
from typing import Any
from fastapi import FastAPI
from langchain.schema.runnable import RunnableLambda
app = FastAPI()
def func(x: Any) -> int:
"""Mistyped function that should accept an int but accepts anything."""
return x + 1
runnable = RunnableLambda(func).with_types(
input_schema=int,
)
add_routes(app, runnable)
```
### Custom User Types
Inherit from `CustomUserType` if you want the data to de-serialize into a
pydantic model rather than the equivalent dict representation.
At the moment, this type only works *server* side and is used
to specify desired *decoding* behavior. If inheriting from this type
the server will keep the decoded type as a pydantic model instead
of converting it into a dict.
```python
from fastapi import FastAPI
from langchain.schema.runnable import RunnableLambda
from langserve import add_routes
from langserve.schema import CustomUserType
app = FastAPI()
class Foo(CustomUserType):
bar: int
def func(foo: Foo) -> int:
"""Sample function that expects a Foo type which is a pydantic model"""
assert isinstance(foo, Foo)
return foo.bar
# Note that the input and output type are automatically inferred!
# You do not need to specify them.
# runnable = RunnableLambda(func).with_types( # <-- Not needed in this case
# input_schema=Foo,
# output_schema=int,
#
add_routes(app, RunnableLambda(func), path="/foo")
```
### Playground Widgets
The playground allows you to define custom widgets for your runnable from the backend.
- A widget is specified at the field level and shipped as part of the JSON schema of the input type
- A widget must contain a key called `type` with the value being one of a well known list of widgets
- Other widget keys will be associated with values that describe paths in a JSON object
General schema:
```typescript
type JsonPath = number | string | (number | string)[];
type NameSpacedPath = { title: string; path: JsonPath }; // Using title to mimick json schema, but can use namespace
type OneOfPath = { oneOf: JsonPath[] };
type Widget = {
type: string // Some well known type (e.g., base64file, chat etc.)
[key: string]: JsonPath | NameSpacedPath | OneOfPath;
};
```
#### File Upload Widget
Allows creation of a file upload input in the UI playground for files
that are uploaded as base64 encoded strings. Here's the full [example](https://github.com/langchain-ai/langserve/tree/main/examples/file_processing).
Snippet:
```python
from pydantic import Field
from langserve import CustomUserType
# ATTENTION: Inherit from CustomUserType instead of BaseModel otherwise
# the server will decode it into a dict instead of a pydantic model.
class FileProcessingRequest(CustomUserType):
"""Request including a base64 encoded file."""
# The extra field is used to specify a widget for the playground UI.
file: str = Field(..., extra={"widget": {"type": "base64file"}})
num_chars: int = 100
```
+13 -17
View File
@@ -18,16 +18,19 @@
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"execution_count": 1,
"metadata": {
"tags": []
},
"outputs": [
{
"data": {
"text/plain": [
"{'output': {'output': 'Eugene thinks that cats like fish.'}}"
"{'output': {'output': 'Eugene thinks that cats like fish.'},\n",
" 'callback_events': []}"
]
},
"execution_count": 7,
"execution_count": 1,
"metadata": {},
"output_type": "execute_result"
}
@@ -50,7 +53,7 @@
},
{
"cell_type": "code",
"execution_count": 3,
"execution_count": 2,
"metadata": {
"tags": []
},
@@ -70,7 +73,7 @@
},
{
"cell_type": "code",
"execution_count": 4,
"execution_count": 3,
"metadata": {
"tags": []
},
@@ -81,7 +84,7 @@
"{'output': 'Hello! How can I assist you today?'}"
]
},
"execution_count": 4,
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
@@ -92,7 +95,7 @@
},
{
"cell_type": "code",
"execution_count": 5,
"execution_count": 6,
"metadata": {
"tags": []
},
@@ -103,7 +106,7 @@
"{'output': 'Eugene thinks that cats like fish.'}"
]
},
"execution_count": 5,
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
@@ -111,13 +114,6 @@
"source": [
"remote_runnable.invoke({\"input\": \"what does eugene think of cats?\"})"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
@@ -136,7 +132,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.1"
"version": "3.9.6"
}
},
"nbformat": 4,
+1 -1
View File
@@ -74,7 +74,7 @@ class Output(BaseModel):
# /invoke
# /batch
# /stream
add_routes(app, agent_executor, input_type=Input, output_type=Output)
add_routes(app, agent_executor.with_types(input_type=Input, output_type=Output))
if __name__ == "__main__":
import uvicorn
-191
View File
@@ -1,191 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Client\n",
"\n",
"Demo of client interacting with the simple chain server, which deploys a chain that tells jokes about a particular topic."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"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)"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"from langserve import RemoteRunnable\n",
"\n",
"remote_runnable = RemoteRunnable(\"http://localhost:8000/\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Remote runnable has the same interface as local runnables"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"response = await remote_runnable.ainvoke({\"topic\": \"sports\"})"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The client can also execute langchain code synchronously, and pass in configs"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {
"tags": []
},
"outputs": [
{
"data": {
"text/plain": [
"[AIMessage(content='Why did the football coach go to the bank?\\n\\nBecause he wanted to get his quarterback!', additional_kwargs={}, example=False),\n",
" AIMessage(content='Why did the car bring a sweater to the race?\\n\\nBecause it wanted to have a \"car-digan\" finish!', additional_kwargs={}, example=False)]"
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from langchain.schema.runnable.config import RunnableConfig\n",
"\n",
"remote_runnable.batch([{\"topic\": \"sports\"}, {\"topic\": \"cars\"}])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The server supports streaming (using HTTP server-side events), which can help interact with long responses in real time"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {
"tags": []
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Ah, indulge me in this lighthearted endeavor, dear interlocutor! Allow me to regale you with a rather verbose jest concerning our hirsute friends of the wilderness, the bears!\n",
"\n",
"Once upon a time, in the vast expanse of a verdant forest, there existed a most erudite and sagacious bear, renowned for his prodigious intellect and unabated curiosity. This bear, with his inquisitive disposition, embarked on a quest to uncover the secrets of humor, for he believed that laughter possessed the power to unite and uplift the spirits of all creatures, great and small.\n",
"\n",
"Upon his journey, our erudite bear encountered a group of mischievous woodland creatures, who, captivated by his exalted intelligence, dared to challenge him to create a jest that would truly encompass the majestic essence of the bear. Our sagacious bear, never one to back down from a challenge, took a moment to ponder, his profound thoughts swirling amidst the verdant canopy above.\n",
"\n",
"After much contemplation, the bear delivered his jest, thusly: \"Pray, dear friends, envision a most estimable gathering of bears, replete with their formidable bulk and majestic presence. In this symposium of ursine brilliance, one bear, with a prodigious appetite, sauntered forth to procure his daily sustenance. Alas, upon reaching his intended destination, he encountered a dapper gentleman, clad in a most resplendent suit, hitherto unseen in the realm of the forest.\n",
"\n",
"The gentleman, possessing an air of sophistication, addressed the bear with an air of candor, remarking, 'Good sir, I must confess that your corporeal form inspires awe and admiration in equal measure. However, I beseech you, kindly abstain from consuming the berries that grow in this territory, for they possess a most deleterious effect upon the digestive systems of bears.'\n",
"\n",
"In response, the bear, known for his indomitable spirit, replied in a most eloquent manner, 'Dearest sir, I appreciate your concern and your eloquent admonition, yet I must humbly convey that the allure of these succulent berries is simply irresistible. The culinary satisfaction they bring far outweighs the potential discomfort they may inflict upon my digestive faculties. Therefore, I am compelled to disregard your sage counsel and indulge in their delectable essence.'\n",
"\n",
"And so, dear listener, the bear, driven by his insatiable hunger, proceeded to relish the berries with unmitigated gusto, heedless of the gentleman's cautions. After partaking in his feast, the bear, much to his chagrin, soon discovered the veracity of the gentleman's warning, as his digestive faculties embarked upon an unrestrained journey of turmoil and trepidation.\n",
"\n",
"In the aftermath of his ill-fated indulgence, the bear, with a countenance of utmost regret, turned to the gentleman and uttered, 'Verily, good sir, your counsel was indeed sagacious and prescient. I find myself ensnared in a maelstrom of gastrointestinal distress, beseeching the heavens for respite from this discomfort.'\n",
"\n",
"And thus, dear interlocutor, we find ourselves at the crux of this jest, whereupon the bear, in his most vulnerable state, beseeches the heavens for relief from his gastrointestinal plight. In this moment of levity, we are reminded that even the most erudite and sagacious among us can succumb to the allure of temptation, and the consequences that follow serve as a timeless lesson for all creatures within the realm of nature.\"\n",
"\n",
"Oh, the whimsy of the bear's gastronomic misadventure! May it serve as a reminder that, even amidst the grandeur of the natural world, we must exercise prudence and contemplate the ramifications of our actions."
]
}
],
"source": [
"async for chunk in remote_runnable.astream({\"topic\": \"bears, but super verbose\"}):\n",
" print(chunk.content, end=\"\", flush=True)"
]
},
{
"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
}
+203
View File
@@ -0,0 +1,203 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Client\n",
"\n",
"Demo of client interacting with the simple chain server, which deploys a chain that tells jokes about a particular topic."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"You can interact with this via API directly"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {
"tags": []
},
"outputs": [
{
"data": {
"text/plain": [
"{'output': \"Why don't scientists trust atoms who play sports?\\n\\nBecause they make up everything!\",\n",
" 'callback_events': []}"
]
},
"execution_count": 1,
"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)"
]
},
{
"cell_type": "code",
"execution_count": null,
"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": null,
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"response = await remote_runnable.ainvoke({\"topic\": \"sports\"})"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The client can also execute langchain code synchronously, and pass in configs"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"from langchain.schema.runnable.config import RunnableConfig\n",
"\n",
"remote_runnable.batch([{\"topic\": \"sports\"}, {\"topic\": \"cars\"}])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The server supports streaming (using HTTP server-side events), which can help interact with long responses in real time"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"async for chunk in remote_runnable.astream({\"topic\": \"bears, but a bit verbose\"}):\n",
" print(chunk, end=\"\", flush=True)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Configurability\n",
"\n",
"The server chains have been exposed as configurable chains!\n",
"\n",
"```python \n",
"\n",
"model = ChatOpenAI(temperature=0.5).configurable_alternatives(\n",
" ConfigurableField(\n",
" id=\"llm\",\n",
" name=\"LLM\",\n",
" description=(\n",
" \"Decide whether to use a high or a low temperature parameter for the LLM.\"\n",
" ),\n",
" ),\n",
" high_temp=ChatOpenAI(temperature=0.9),\n",
" low_temp=ChatOpenAI(temperature=0.1),\n",
" default_key=\"medium_temp\",\n",
")\n",
"prompt = PromptTemplate.from_template(\n",
" \"tell me a joke about {topic}.\"\n",
").configurable_fields( # Example of a configurable field\n",
" template=ConfigurableField(\n",
" id=\"prompt\",\n",
" name=\"Prompt\",\n",
" description=(\"The prompt to use. Must contain {topic}.\"),\n",
" )\n",
")\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We can now use the configurability of the runnable in the API!"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"await remote_runnable.ainvoke(\n",
" {\"topic\": \"sports\"},\n",
" config={\n",
" \"configurable\": {\"prompt\": \"how to say {topic} in french\", \"llm\": \"low_temp\"}\n",
" },\n",
")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.9.6"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
@@ -1,30 +1,39 @@
#!/usr/bin/env python
"""Example LangChain server exposes a chain composed of a prompt and an LLM."""
"""Example of configurable runnables.
This example shows how to use two options for configuration of runnables:
1) Configurable Fields: Use this to specify values for a given initialization parameter
2) Configurable Alternatives: Use this to specify complete alternative runnables
"""
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from langchain.chat_models import ChatOpenAI
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(temperature=0.5).configurable_alternatives(
ConfigurableField(id="llm", name="LLM"),
ConfigurableField(
id="llm",
name="LLM",
description=(
"Decide whether to use a high or a low temperature parameter for the LLM."
),
),
high_temp=ChatOpenAI(temperature=0.9),
low_temp=ChatOpenAI(temperature=0.1, max_tokens=1),
low_temp=ChatOpenAI(temperature=0.1),
default_key="medium_temp",
)
prompt = PromptTemplate.from_template(
"tell me a joke about {topic}."
).configurable_fields(
).configurable_fields( # Example of a configurable field
template=ConfigurableField(
id="prompt",
name="Prompt",
description="The prompt to use. Must contain {topic}",
description="The prompt to use. Must contain {topic}.",
)
)
chain = prompt | model | StrOutputParser()
@@ -46,20 +55,9 @@ app.add_middleware(
)
# 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
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)
# Add routes requires you to specify which config keys are accepted
# specifically, you must accept `configurable` as a config key.
add_routes(app, chain, config_keys=["configurable"])
if __name__ == "__main__":
import uvicorn
@@ -18,16 +18,18 @@
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"execution_count": 1,
"metadata": {
"tags": []
},
"outputs": [
{
"data": {
"text/plain": [
"{'output': {'answer': 'Cats like fish.'}}"
"{'output': {'answer': 'Cats like fish.'}, 'callback_events': []}"
]
},
"execution_count": 2,
"execution_count": 1,
"metadata": {},
"output_type": "execute_result"
}
@@ -35,7 +37,7 @@
"source": [
"import requests\n",
"\n",
"inputs = {\"input\": {\"question\": \"what do cats like?\", \"chat_history\": \"\"}}\n",
"inputs = {\"input\": {\"question\": \"what do cats like?\", \"chat_history\": []}}\n",
"response = requests.post(\"http://localhost:8000/invoke\", json=inputs)\n",
"\n",
"response.json()"
@@ -50,7 +52,7 @@
},
{
"cell_type": "code",
"execution_count": 6,
"execution_count": 2,
"metadata": {
"tags": []
},
@@ -70,7 +72,7 @@
},
{
"cell_type": "code",
"execution_count": 7,
"execution_count": 3,
"metadata": {
"tags": []
},
@@ -81,18 +83,18 @@
"{'answer': 'Cats like fish.'}"
]
},
"execution_count": 7,
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"await remote_runnable.ainvoke({\"question\": \"what do cats like?\", \"chat_history\": \"\"})"
"await remote_runnable.ainvoke({\"question\": \"what do cats like?\", \"chat_history\": []})"
]
},
{
"cell_type": "code",
"execution_count": 10,
"execution_count": 6,
"metadata": {
"tags": []
},
@@ -103,7 +105,7 @@
"{'answer': 'Cats like fish.'}"
]
},
"execution_count": 10,
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
@@ -138,7 +140,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.1"
"version": "3.9.6"
}
},
"nbformat": 4,
@@ -1,10 +1,13 @@
#!/usr/bin/env python
"""Example LangChain server exposes a conversational retrieval chain."""
from typing import List, Tuple
from fastapi import FastAPI
from langchain.chains import ConversationalRetrievalChain
from langchain.chat_models import ChatOpenAI
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import FAISS
from pydantic import BaseModel, Field
from langserve import add_routes
@@ -17,6 +20,22 @@ model = ChatOpenAI()
chain = ConversationalRetrievalChain.from_llm(model, retriever)
# User input
class ChatHistory(BaseModel):
"""Chat history with the bot."""
chat_history: List[Tuple[str, str]] = Field(
...,
extra={"widget": {"type": "chat", "input": "question"}},
)
question: str
chain = ConversationalRetrievalChain.from_llm(model, retriever).with_types(
input_type=ChatHistory
)
app = FastAPI(
title="LangChain Server",
version="1.0",
+156
View File
@@ -0,0 +1,156 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# File processing\n",
"\n",
"This client will be uploading a PDF file to the langserve server which will read the PDF and extract content from the first page."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's load the file in base64 encoding:"
]
},
{
"cell_type": "code",
"execution_count": 18,
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"import base64\n",
"\n",
"with open(\"sample.pdf\", \"rb\") as f:\n",
" data = f.read()\n",
"\n",
"encoded_data = base64.b64encode(data).decode(\"utf-8\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Using raw requests"
]
},
{
"cell_type": "code",
"execution_count": 19,
"metadata": {
"tags": []
},
"outputs": [
{
"data": {
"text/plain": [
"{'output': 'If youre reading this you might be using LangServe 🦜🏓!\\n\\nThis is a sample PDF!\\n\\n\\x0c',\n",
" 'callback_events': []}"
]
},
"execution_count": 19,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"import requests\n",
"\n",
"requests.post(\n",
" \"http://localhost:8000/pdf/invoke/\", json={\"input\": {\"file\": encoded_data}}\n",
").json()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Using the SDK"
]
},
{
"cell_type": "code",
"execution_count": 20,
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"from langserve import RemoteRunnable\n",
"\n",
"runnable = RemoteRunnable(\"http://localhost:8000/pdf/\")"
]
},
{
"cell_type": "code",
"execution_count": 21,
"metadata": {
"tags": []
},
"outputs": [
{
"data": {
"text/plain": [
"'If youre reading this you might be using LangServe 🦜🏓!\\n\\nThis is a sample PDF!\\n\\n\\x0c'"
]
},
"execution_count": 21,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"runnable.invoke({\"file\": encoded_data})"
]
},
{
"cell_type": "code",
"execution_count": 22,
"metadata": {
"tags": []
},
"outputs": [
{
"data": {
"text/plain": [
"['If youre reading this you might be using LangServe 🦜🏓!\\n\\nThis is a sample PDF!\\n\\n\\x0c',\n",
" 'If youre ']"
]
},
"execution_count": 22,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"runnable.batch([{\"file\": encoded_data}, {\"file\": encoded_data, \"num_chars\": 10}])"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.9.6"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
Binary file not shown.
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env python
"""Example that shows how to upload files and process files in the server.
This example uses a very simple architecture for dealing with file uploads
and processing.
The main issue with this approach is that processing is done in
the same process rather than offloaded to a process pool. A smaller
issue is that the base64 encoding incurs an additional encoding/decoding
overhead.
This example also specifies a "base64file" widget, which will create a widget
allowing one to upload a binary file using the langserve playground UI.
"""
import base64
from fastapi import FastAPI
from langchain.document_loaders.blob_loaders import Blob
from langchain.document_loaders.parsers.pdf import PDFMinerParser
from langchain.schema.runnable import RunnableLambda
from pydantic import Field
from langserve import CustomUserType, add_routes
app = FastAPI(
title="LangChain Server",
version="1.0",
description="Spin up a simple api server using Langchain's Runnable interfaces",
)
# ATTENTION: Inherit from CustomUserType instead of BaseModel otherwise
# the server will decode it into a dict instead of a pydantic model.
class FileProcessingRequest(CustomUserType):
"""Request including a base64 encoded file."""
# The extra field is used to specify a widget for the playground UI.
file: str = Field(..., extra={"widget": {"type": "base64file"}})
num_chars: int = 100
def _process_file(request: FileProcessingRequest) -> str:
"""Extract the text from the first page of the PDF."""
content = base64.b64decode(request.file.encode("utf-8"))
blob = Blob(data=content)
documents = list(PDFMinerParser().lazy_parse(blob))
content = documents[0].page_content
return content[: request.num_chars]
add_routes(
app,
RunnableLambda(_process_file).with_types(input_type=FileProcessingRequest),
config_keys=["configurable"],
path="/pdf",
)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="localhost", port=8000)
+82
View File
@@ -0,0 +1,82 @@
#!/usr/bin/env python
"""Endpoint shows off available playground widgets."""
import base64
from typing import Any, Dict, List, Tuple
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from langchain.document_loaders.blob_loaders import Blob
from langchain.document_loaders.parsers.pdf import PDFMinerParser
from langchain.schema.runnable import RunnableLambda
from pydantic import BaseModel, Field
from langserve.server import add_routes
app = FastAPI(
title="LangChain Server",
version="1.0",
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 ChatHistory(BaseModel):
chat_history: List[Tuple[str, str]] = Field(
...,
examples=[[("a", "aa")]],
extra={"widget": {"type": "chat", "input": "question", "output": "answer"}},
)
question: str
class FileProcessingRequest(BaseModel):
file: bytes = Field(..., extra={"widget": {"type": "base64file"}})
num_chars: int = 100
def chat_with_bot(input: Dict[str, Any]) -> Dict[str, Any]:
"""Bot that repeats the question twice."""
return {
"answer": input["question"] * 2,
"woof": "its so bad to woof, meow is better",
}
def process_file(input: Dict[str, Any]) -> str:
"""Extract the text from the first page of the PDF."""
content = base64.decodebytes(input["file"])
blob = Blob(data=content)
documents = list(PDFMinerParser().lazy_parse(blob))
content = documents[0].page_content
return content[: input["num_chars"]]
add_routes(
app,
RunnableLambda(chat_with_bot).with_types(input_type=ChatHistory),
config_keys=["configurable"],
path="/chat",
)
add_routes(
app,
RunnableLambda(process_file).with_types(input_type=FileProcessingRequest),
config_keys=["configurable"],
path="/pdf",
)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="localhost", port=8000)
+7 -2
View File
@@ -1,7 +1,12 @@
"""Main entrypoint into package."""
"""Main entrypoint into package.
This is the ONLY public interface into the package. All other modules are
to be considered private and subject to change without notice.
"""
from langserve.client import RemoteRunnable
from langserve.schema import CustomUserType
from langserve.server import add_routes
from langserve.version import __version__
__all__ = ["RemoteRunnable", "add_routes", "__version__"]
__all__ = ["RemoteRunnable", "add_routes", "__version__", "CustomUserType"]
+475
View File
@@ -0,0 +1,475 @@
from __future__ import annotations
import uuid
from typing import Any, Dict, List, Optional, Sequence
from uuid import UUID
from langchain.callbacks.base import AsyncCallbackHandler
from langchain.callbacks.manager import (
BaseRunManager,
ahandle_event,
handle_event,
)
from langchain.schema import AgentAction, AgentFinish, BaseMessage, Document, LLMResult
from typing_extensions import TypedDict
class CallbackEventDict(TypedDict, total=False):
"""A dictionary representation of a callback event."""
type: str
parent_run_id: Optional[UUID]
run_id: UUID
class AsyncEventAggregatorCallback(AsyncCallbackHandler):
"""A callback handler that aggregates all the events that have been called.
This callback handler aggregates all the events that have been called placing
them in a single mutable list.
This callback handler is not threading safe, and is meant to be used in an async
context only.
"""
def __init__(self) -> None:
"""Get a list of all the callback events that have been called."""
super().__init__()
# Callback events is a mutable state that is used only in an async context,
# so it should be safe to mutate without the usage of a lock.
self.callback_events: List[CallbackEventDict] = []
def log_callback(self, event: CallbackEventDict) -> None:
"""Log the callback event."""
self.callback_events.append(event)
async def on_chat_model_start(
self,
serialized: Dict[str, Any],
messages: List[List[BaseMessage]],
*,
run_id: UUID,
parent_run_id: Optional[UUID] = None,
tags: Optional[List[str]] = None,
metadata: Optional[Dict[str, Any]] = None,
**kwargs: Any,
) -> Any:
"""Attempt to serialize the callback event."""
self.log_callback(
{
"type": "on_chat_model_start",
"serialized": serialized,
"messages": messages,
"run_id": run_id,
"parent_run_id": parent_run_id,
"tags": tags,
"metadata": metadata,
"kwargs": kwargs,
}
)
async def on_chain_start(
self,
serialized: Dict[str, Any],
inputs: Dict[str, Any],
*,
run_id: UUID,
parent_run_id: Optional[UUID] = None,
tags: Optional[List[str]] = None,
metadata: Optional[Dict[str, Any]] = None,
**kwargs: Any,
) -> None:
"""Attempt to serialize the callback event."""
self.log_callback(
{
"type": "on_chain_start",
"serialized": serialized,
"inputs": inputs,
"run_id": run_id,
"parent_run_id": parent_run_id,
"tags": tags,
"metadata": metadata,
"kwargs": kwargs,
}
)
async def on_chain_end(
self,
outputs: Any,
*,
run_id: UUID,
parent_run_id: Optional[UUID] = None,
tags: Optional[List[str]] = None,
**kwargs: Any,
) -> None:
self.log_callback(
{
"type": "on_chain_end",
"outputs": outputs,
"run_id": run_id,
"parent_run_id": parent_run_id,
"tags": tags,
"kwargs": kwargs,
}
)
async def on_chain_error(
self,
error: BaseException,
*,
run_id: UUID,
parent_run_id: Optional[UUID] = None,
tags: Optional[List[str]] = None,
**kwargs: Any,
) -> None:
self.log_callback(
{
"type": "on_chain_error",
"error": error,
"run_id": run_id,
"parent_run_id": parent_run_id,
"tags": tags,
"kwargs": kwargs,
}
)
async def on_retriever_start(
self,
serialized: Dict[str, Any],
query: str,
*,
run_id: UUID,
parent_run_id: Optional[UUID] = None,
tags: Optional[List[str]] = None,
metadata: Optional[Dict[str, Any]] = None,
**kwargs: Any,
) -> None:
self.log_callback(
{
"type": "on_retriever_start",
"serialized": serialized,
"query": query,
"run_id": run_id,
"parent_run_id": parent_run_id,
"tags": tags,
"metadata": metadata,
"kwargs": kwargs,
}
)
async def on_retriever_end(
self,
documents: Sequence[Document],
*,
run_id: UUID,
parent_run_id: Optional[UUID] = None,
tags: Optional[List[str]] = None,
**kwargs: Any,
) -> None:
self.log_callback(
{
"type": "on_retriever_end",
"documents": documents,
"run_id": run_id,
"parent_run_id": parent_run_id,
"tags": tags,
"kwargs": kwargs,
}
)
async def on_retriever_error(
self,
error: BaseException,
*,
run_id: UUID,
parent_run_id: Optional[UUID] = None,
tags: Optional[List[str]] = None,
**kwargs: Any,
) -> None:
self.log_callback(
{
"type": "on_retriever_error",
"error": error,
"run_id": run_id,
"parent_run_id": parent_run_id,
"tags": tags,
"kwargs": kwargs,
}
)
async def on_tool_start(
self,
serialized: Dict[str, Any],
input_str: str,
*,
run_id: UUID,
parent_run_id: Optional[UUID] = None,
tags: Optional[List[str]] = None,
metadata: Optional[Dict[str, Any]] = None,
**kwargs: Any,
) -> None:
self.log_callback(
{
"type": "on_tool_start",
"serialized": serialized,
"input_str": input_str,
"run_id": run_id,
"parent_run_id": parent_run_id,
"tags": tags,
"metadata": metadata,
"kwargs": kwargs,
}
)
async def on_tool_end(
self,
output: str,
*,
run_id: UUID,
parent_run_id: Optional[UUID] = None,
tags: Optional[List[str]] = None,
**kwargs: Any,
) -> None:
self.log_callback(
{
"type": "on_tool_end",
"output": output,
"run_id": run_id,
"parent_run_id": parent_run_id,
"tags": tags,
"kwargs": kwargs,
}
)
async def on_tool_error(
self,
error: BaseException,
*,
run_id: UUID,
parent_run_id: Optional[UUID] = None,
tags: Optional[List[str]] = None,
**kwargs: Any,
) -> None:
self.log_callback(
{
"type": "on_tool_error",
"error": error,
"run_id": run_id,
"parent_run_id": parent_run_id,
"tags": tags,
"kwargs": kwargs,
}
)
async def on_agent_action(
self,
action: AgentAction,
*,
run_id: UUID,
parent_run_id: Optional[UUID] = None,
tags: Optional[List[str]] = None,
**kwargs: Any,
) -> None:
self.log_callback(
{
"type": "on_agent_action",
"action": action,
"run_id": run_id,
"parent_run_id": parent_run_id,
"tags": tags,
"kwargs": kwargs,
}
)
async def on_agent_finish(
self,
finish: AgentFinish,
*,
run_id: UUID,
parent_run_id: Optional[UUID] = None,
tags: Optional[List[str]] = None,
**kwargs: Any,
) -> None:
self.log_callback(
{
"type": "on_agent_finish",
"finish": finish,
"run_id": run_id,
"parent_run_id": parent_run_id,
"tags": tags,
"kwargs": kwargs,
}
)
async def on_llm_start(
self,
serialized: Dict[str, Any],
prompts: List[str],
*,
run_id: UUID,
parent_run_id: Optional[UUID] = None,
tags: Optional[List[str]] = None,
metadata: Optional[Dict[str, Any]] = None,
**kwargs: Any,
) -> None:
self.log_callback(
{
"type": "on_llm_start",
"serialized": serialized,
"prompts": prompts,
"run_id": run_id,
"parent_run_id": parent_run_id,
"tags": tags,
"metadata": metadata,
"kwargs": kwargs,
}
)
async def on_llm_end(
self,
response: LLMResult,
*,
run_id: UUID,
parent_run_id: Optional[UUID] = None,
tags: Optional[List[str]] = None,
**kwargs: Any,
) -> None:
self.log_callback(
{
"type": "on_llm_end",
"response": response,
"run_id": run_id,
"parent_run_id": parent_run_id,
"tags": tags,
"kwargs": kwargs,
}
)
async def on_llm_error(
self,
error: BaseException,
*,
run_id: UUID,
parent_run_id: Optional[UUID] = None,
tags: Optional[List[str]] = None,
**kwargs: Any,
) -> None:
self.log_callback(
{
"type": "on_llm_error",
"error": error,
"run_id": run_id,
"parent_run_id": parent_run_id,
"tags": tags,
"kwargs": kwargs,
}
)
def replace_uuids(
callback_events: Sequence[CallbackEventDict],
) -> List[CallbackEventDict]:
"""Replace uids in the event callbacks with new uids.
This function mutates the event callback events in place.
Args:
callback_events: A list of event callbacks.
"""
# Create a dictionary to store mappings from old UID to new UID
uid_mapping: dict = {}
updated_events = []
# Iterate through the list of event callbacks
for event in callback_events:
updated_event = event.copy()
# Replace UIDs in the 'run_id' field
if "run_id" in updated_event and updated_event["run_id"] is not None:
if updated_event["run_id"] not in uid_mapping:
# Generate a new UUID
new_uid = uuid.uuid4()
uid_mapping[updated_event["run_id"]] = new_uid
# Replace the old UID with the new one
updated_event["run_id"] = uid_mapping[updated_event["run_id"]]
# Replace UIDs in the 'parent_run_id' field if it's not None
if (
"parent_run_id" in updated_event
and updated_event["parent_run_id"] is not None
):
if updated_event["parent_run_id"] not in uid_mapping:
# Generate a new UUID
new_uid = uuid.uuid4()
uid_mapping[updated_event["parent_run_id"]] = new_uid
# Replace the old UID with the new one
updated_event["parent_run_id"] = uid_mapping[updated_event["parent_run_id"]]
updated_events.append(updated_event)
return updated_events
# Mapping from event name to ignore condition name
NAME_TO_IGNORE_CONDITION = {
"on_retry": "ignore_retry",
"on_text": None,
"on_agent_action": "ignore_agent",
"on_agent_finish": "ignore_agent",
"on_llm_start": "ignore_llm",
"on_llm_end": "ignore_llm",
"on_llm_error": "ignore_llm",
"on_chain_start": "ignore_chain",
"on_chain_end": "ignore_chain",
"on_chain_error": "ignore_chain",
"on_chat_model_start": "ignore_chat_model",
"on_tool_start": "ignore_agent",
"on_tool_end": "ignore_agent",
"on_tool_error": "ignore_agent",
"on_retriever_start": "ignore_retriever",
"on_retriever_end": "ignore_retriever",
"on_retriever_error": "ignore_retriever",
}
async def ahandle_callbacks(
callback_manager: BaseRunManager,
callback_events: Sequence[CallbackEventDict],
) -> None:
"""Invoke all the callback handlers with the given callback events."""
callback_events = replace_uuids(callback_events)
# 1. Do I need inheritable handlers
for event in callback_events:
if event["parent_run_id"] is None: # How do we make sure it's None!?
event["parent_run_id"] = callback_manager.run_id
event_data = {key: value for key, value in event.items() if key != "type"}
await ahandle_event(
# Unpacking like this may not work
callback_manager.handlers,
event["type"],
ignore_condition_name=NAME_TO_IGNORE_CONDITION.get(event["type"], None),
**event_data,
)
def handle_callbacks(
callback_manager: BaseRunManager,
callback_events: Sequence[CallbackEventDict],
) -> None:
"""Invoke all the callback handlers with the given callback events."""
callback_events = replace_uuids(callback_events)
for event in callback_events:
if event["parent_run_id"] is None: # How do we make sure it's None!?
event["parent_run_id"] = callback_manager.run_id
event_data = {key: value for key, value in event.items() if key != "type"}
handle_event(
# Unpacking like this may not work
callback_manager.handlers,
event["type"],
ignore_condition_name=NAME_TO_IGNORE_CONDITION.get(event["type"], None),
**event_data,
)
+204 -42
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
import asyncio
import copy
import json
import logging
import weakref
@@ -14,12 +15,17 @@ from typing import (
List,
Optional,
Sequence,
Tuple,
Union,
)
from urllib.parse import urljoin
import httpx
from httpx._types import AuthTypes, CertTypes, CookieTypes, HeaderTypes, VerifyTypes
from langchain.callbacks.manager import (
AsyncCallbackManagerForChainRun,
CallbackManagerForChainRun,
)
from langchain.callbacks.tracers.log_stream import RunLogPatch
from langchain.load.dump import dumpd
from langchain.schema.runnable import Runnable
@@ -29,9 +35,14 @@ from langchain.schema.runnable.config import (
get_async_callback_manager_for_config,
get_callback_manager_for_config,
)
from langchain.schema.runnable.utils import Input, Output
from langchain.schema.runnable.utils import AddableDict, Input, Output
from langserve.serialization import simple_dumpd, simple_loads
from langserve.callbacks import CallbackEventDict, ahandle_callbacks, handle_callbacks
from langserve.serialization import (
Serializer,
WellKnownLCSerializer,
load_events,
)
logger = logging.getLogger(__name__)
@@ -42,12 +53,35 @@ def _without_callbacks(config: Optional[RunnableConfig]) -> RunnableConfig:
return {k: v for k, v in _config.items() if k != "callbacks"}
@lru_cache(maxsize=1_000) # Will accommodate up to 100 different error messages
@lru_cache(maxsize=1_000) # Will accommodate up to 1_000 different error messages
def _log_error_message_once(error_message: str) -> None:
"""Log an error message once."""
logger.error(error_message)
def _sanitize_request(request: httpx.Request) -> httpx.Request:
"""Remove sensitive headers from the request."""
accept_headers = {
"accept",
"content-type",
"user-agent",
"connection",
"content-length",
"accept-encoding",
"host",
}
new_headers = request.headers.copy()
for key, value in new_headers.items():
if key.lower() not in accept_headers:
new_headers[key] = "<redacted>"
else:
new_headers[key] = value
new_request = copy.copy(request)
new_request.headers = new_headers
return new_request
def _raise_for_status(response: httpx.Response) -> None:
"""Re-raise with a more informative message.
@@ -69,7 +103,7 @@ def _raise_for_status(response: httpx.Response) -> None:
raise httpx.HTTPStatusError(
message=message,
request=e.request,
request=_sanitize_request(e.request),
response=e.response,
)
@@ -112,12 +146,12 @@ def _raise_exception_from_data(data: str, request: httpx.Request) -> None:
except json.JSONDecodeError:
raise httpx.HTTPStatusError(
message="invalid json in error event sent from server",
request=request,
request=_sanitize_request(request),
response=httpx.Response(status_code=500, text=data),
)
raise httpx.HTTPStatusError(
message=decoded_data["message"],
request=request,
request=_sanitize_request(request),
response=httpx.Response(
status_code=decoded_data["status_code"],
text=decoded_data["message"],
@@ -125,6 +159,42 @@ def _raise_exception_from_data(data: str, request: httpx.Request) -> None:
)
def _decode_response(
serializer: Serializer,
response: httpx.Response,
*,
is_batch: bool = False,
) -> Tuple[Any, Union[List[CallbackEventDict], List[List[CallbackEventDict]]]]:
"""Decode the response."""
_raise_for_status(response)
obj = response.json()
if not isinstance(obj, dict):
raise ValueError(f"Expected a dictionary, got {obj}")
if "output" not in obj:
raise ValueError("Key `output` not found in")
output = serializer.loadd(obj["output"])
if "callback_events" in obj:
if is_batch:
if not isinstance(obj["callback_events"], list):
raise ValueError(
f"Expected a list of callback events, got {obj['callback_events']}"
)
else:
callback_events = [
load_events(callback_events)
for callback_events in obj["callback_events"]
]
else:
callback_events = load_events(obj["callback_events"])
else:
callback_events = []
return output, callback_events
class RemoteRunnable(Runnable[Input, Output]):
"""A RemoteRunnable is a runnable that is executed on a remote server.
@@ -134,8 +204,6 @@ class RemoteRunnable(Runnable[Input, Output]):
- `batch` with `return_exceptions=True` since we do not support exception
translation from the server.
- Callbacks via the `config` argument as serialization of callbacks is not
supported.
"""
def __init__(
@@ -149,6 +217,7 @@ class RemoteRunnable(Runnable[Input, Output]):
verify: VerifyTypes = True,
cert: Optional[CertTypes] = None,
client_kwargs: Optional[Dict[str, Any]] = None,
use_server_callback_events: bool = True,
) -> None:
"""Initialize the client.
@@ -161,7 +230,9 @@ class RemoteRunnable(Runnable[Input, Output]):
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
and async httpx clients
use_server_callback_events: Whether to invoke callbacks on any
callback events returned by the server.
"""
_client_kwargs = client_kwargs or {}
self.url = url
@@ -188,21 +259,32 @@ class RemoteRunnable(Runnable[Input, Output]):
# Register cleanup handler once RemoteRunnable is garbage collected
weakref.finalize(self, _close_clients, self.sync_client, self.async_client)
self._lc_serializer = WellKnownLCSerializer()
self._use_server_callback_events = use_server_callback_events
def _invoke(
self, input: Input, config: Optional[RunnableConfig] = None, **kwargs: Any
self,
input: Input,
run_manager: CallbackManagerForChainRun,
config: Optional[RunnableConfig] = None,
**kwargs: Any,
) -> Output:
"""Invoke the runnable with the given input and config."""
response = self.sync_client.post(
"/invoke",
json={
"input": simple_dumpd(input),
"input": self._lc_serializer.dumpd(input),
"config": _without_callbacks(config),
"kwargs": kwargs,
},
)
_raise_for_status(response)
return simple_loads(response.text)["output"]
output, callback_events = _decode_response(
self._lc_serializer, response, is_batch=False
)
if self._use_server_callback_events and callback_events:
handle_callbacks(run_manager, callback_events)
return output
def invoke(
self, input: Input, config: Optional[RunnableConfig] = None, **kwargs: Any
@@ -212,18 +294,27 @@ class RemoteRunnable(Runnable[Input, Output]):
return self._call_with_config(self._invoke, input, config=config)
async def _ainvoke(
self, input: Input, config: Optional[RunnableConfig] = None, **kwargs: Any
self,
input: Input,
run_manager: AsyncCallbackManagerForChainRun,
config: Optional[RunnableConfig] = None,
**kwargs: Any,
) -> Output:
"""Invoke the runnable with the given input and config."""
response = await self.async_client.post(
"/invoke",
json={
"input": simple_dumpd(input),
"input": self._lc_serializer.dumpd(input),
"config": _without_callbacks(config),
"kwargs": kwargs,
},
)
_raise_for_status(response)
return simple_loads(response.text)["output"]
output, callback_events = _decode_response(
self._lc_serializer, response, is_batch=False
)
if self._use_server_callback_events and callback_events:
handle_callbacks(run_manager, callback_events)
return output
async def ainvoke(
self, input: Input, config: Optional[RunnableConfig] = None, **kwargs: Any
@@ -235,6 +326,7 @@ class RemoteRunnable(Runnable[Input, Output]):
def _batch(
self,
inputs: List[Input],
run_manager: List[CallbackManagerForChainRun],
config: Optional[Union[RunnableConfig, List[RunnableConfig]]] = None,
*,
return_exceptions: bool = False,
@@ -255,13 +347,28 @@ class RemoteRunnable(Runnable[Input, Output]):
response = self.sync_client.post(
"/batch",
json={
"inputs": simple_dumpd(inputs),
"inputs": self._lc_serializer.dumpd(inputs),
"config": _config,
"kwargs": kwargs,
},
)
_raise_for_status(response)
return simple_loads(response.text)["output"]
outputs, corresponding_callback_events = _decode_response(
self._lc_serializer, response, is_batch=True
)
# Now handle callbacks if any were returned
if self._use_server_callback_events and corresponding_callback_events:
for run_manager_, callback_events in zip(
run_manager, corresponding_callback_events
):
handle_callbacks(run_manager_, callback_events)
return outputs
def _enforce_trailing_slash(self, url: str) -> str:
if url.endswith("/"):
return url
return url + "/"
def batch(
self,
@@ -276,6 +383,7 @@ class RemoteRunnable(Runnable[Input, Output]):
async def _abatch(
self,
inputs: List[Input],
run_manager: List[AsyncCallbackManagerForChainRun],
config: Optional[Union[RunnableConfig, List[RunnableConfig]]] = None,
*,
return_exceptions: bool = False,
@@ -297,13 +405,27 @@ class RemoteRunnable(Runnable[Input, Output]):
response = await self.async_client.post(
"/batch",
json={
"inputs": simple_dumpd(inputs),
"inputs": self._lc_serializer.dumpd(inputs),
"config": _config,
"kwargs": kwargs,
},
)
_raise_for_status(response)
return simple_loads(response.text)["output"]
outputs, corresponding_callback_events = _decode_response(
self._lc_serializer, response, is_batch=True
)
# Now handle callbacks
if self._use_server_callback_events and corresponding_callback_events:
tasks = []
for run_manager_, callback_events in zip(
run_manager, corresponding_callback_events
):
tasks.append(ahandle_callbacks(run_manager_, callback_events))
# Execute coroutines concurrently
await asyncio.gather(*tasks)
return outputs
async def abatch(
self,
@@ -331,18 +453,19 @@ class RemoteRunnable(Runnable[Input, Output]):
callback_manager = get_callback_manager_for_config(config)
final_output: Optional[Output] = None
final_output_supported = True
run_manager = callback_manager.on_chain_start(
dumpd(self),
simple_dumpd(input),
self._lc_serializer.dumpd(input),
name=config.get("run_name"),
)
data = {
"input": simple_dumpd(input),
"input": self._lc_serializer.dumpd(input),
"config": _without_callbacks(config),
"kwargs": kwargs,
}
endpoint = urljoin(self.url, "stream")
endpoint = urljoin(self._enforce_trailing_slash(self.url), "stream")
try:
from httpx_sse import connect_sse
@@ -358,13 +481,32 @@ class RemoteRunnable(Runnable[Input, Output]):
) as event_source:
for sse in event_source.iter_sse():
if sse.event == "data":
chunk = simple_loads(sse.data)
chunk = self._lc_serializer.loads(sse.data)
if isinstance(chunk, dict):
# Any dict returned from streaming end point
# is assumed to follow additive semantics
# and will be converted to an AddableDict
# automatically
chunk = AddableDict(chunk)
yield chunk
if final_output:
final_output += chunk
else:
final_output = chunk
if final_output_supported:
# here we attempt to aggregate the final output
# from the stream.
# the final output is used for the final callback
# event (`on_chain_end`)
# Aggregating the final output is only supported
# if the output is additive (e.g., string or
# AddableDict, etc.)
# We attempt to aggregate it on best effort basis.
if final_output is None:
final_output = chunk
else:
try:
final_output = final_output + chunk
except TypeError:
final_output = None
final_output_supported = False
elif sse.event == "error":
# This can only be a server side error
_raise_exception_from_data(
@@ -394,18 +536,19 @@ class RemoteRunnable(Runnable[Input, Output]):
callback_manager = get_async_callback_manager_for_config(config)
final_output: Optional[Output] = None
final_output_supported = True
run_manager = await callback_manager.on_chain_start(
dumpd(self),
simple_dumpd(input),
self._lc_serializer.dumpd(input),
name=config.get("run_name"),
)
data = {
"input": simple_dumpd(input),
"input": self._lc_serializer.dumpd(input),
"config": _without_callbacks(config),
"kwargs": kwargs,
}
endpoint = urljoin(self.url, "stream")
endpoint = urljoin(self._enforce_trailing_slash(self.url), "stream")
try:
from httpx_sse import aconnect_sse
@@ -418,13 +561,32 @@ class RemoteRunnable(Runnable[Input, Output]):
) as event_source:
async for sse in event_source.aiter_sse():
if sse.event == "data":
chunk = simple_loads(sse.data)
chunk = self._lc_serializer.loads(sse.data)
if isinstance(chunk, dict):
# Any dict returned from streaming end point
# is assumed to follow additive semantics
# and will be converted to an AddableDict
# automatically
chunk = AddableDict(chunk)
yield chunk
if final_output:
final_output += chunk
else:
final_output = chunk
if final_output_supported:
# here we attempt to aggregate the final output
# from the stream.
# the final output is used for the final callback
# event (`on_chain_end`)
# Aggregating the final output is only supported
# if the output is additive (e.g., string or
# AddableDict, etc.)
# We attempt to aggregate it on best effort basis.
if final_output is None:
final_output = chunk
else:
try:
final_output = final_output + chunk
except TypeError:
final_output = None
final_output_supported = False
elif sse.event == "error":
# This can only be a server side error
@@ -477,11 +639,11 @@ class RemoteRunnable(Runnable[Input, Output]):
run_manager = await callback_manager.on_chain_start(
dumpd(self),
simple_dumpd(input),
self._lc_serializer.dumpd(input),
name=config.get("run_name"),
)
data = {
"input": simple_dumpd(input),
"input": self._lc_serializer.dumpd(input),
"config": _without_callbacks(config),
"kwargs": kwargs,
"diff": True,
@@ -505,7 +667,7 @@ class RemoteRunnable(Runnable[Input, Output]):
) as event_source:
async for sse in event_source.aiter_sse():
if sse.event == "data":
data = simple_loads(sse.data)
data = self._lc_serializer.loads(sse.data)
chunk = RunLogPatch(*data["ops"])
yield chunk
+35 -21
View File
@@ -2,7 +2,7 @@ import json
import mimetypes
import os
from string import Template
from typing import List, Type
from typing import Sequence, Type
from fastapi.responses import Response
from langchain.schema.runnable import Runnable
@@ -20,28 +20,42 @@ class PlaygroundTemplate(Template):
async def serve_playground(
runnable: Runnable,
input_schema: Type[BaseModel],
config_keys: List[str],
config_keys: Sequence[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",
"""Serve the playground."""
local_file_path = os.path.abspath(
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)
base_dir = os.path.abspath(
os.path.join(os.path.dirname(__file__), "./playground/dist")
)
if base_dir != os.path.commonpath((base_dir, local_file_path)):
return Response("Not Found", status_code=404)
try:
with open(local_file_path) as f:
mime_type = mimetypes.guess_type(local_file_path)[0]
if mime_type in ("text/html", "text/css", "application/javascript"):
response = 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:
response = f.buffer.read()
except FileNotFoundError:
return Response("Not Found", status_code=404)
return Response(response, media_type=mime_type)
+3 -1
View File
@@ -23,4 +23,6 @@ dist-ssr
*.sln
*.sw?
.yarn
.yarn
!dist
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -5,8 +5,8 @@
<link rel="icon" href="/____LANGSERVE_BASE_URL/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Playground</title>
<script type="module" crossorigin src="/____LANGSERVE_BASE_URL/assets/index-9fe7f71c.js"></script>
<link rel="stylesheet" href="/____LANGSERVE_BASE_URL/assets/index-5008c8a8.css">
<script type="module" crossorigin src="/____LANGSERVE_BASE_URL/assets/index-ea49ff70.js"></script>
<link rel="stylesheet" href="/____LANGSERVE_BASE_URL/assets/index-244b2b9b.css">
</head>
<body>
<div id="root"></div>
-1
View File
@@ -23,7 +23,6 @@
"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",
+151 -93
View File
@@ -1,13 +1,11 @@
import "./App.css";
import React, { useEffect, useRef, useState } from "react";
import defaults from "json-schema-defaults";
import { useEffect, useMemo, useRef, useState } from "react";
import defaults from "./utils/defaults";
import { JsonForms } from "@jsonforms/react";
import {
materialAllOfControlTester,
MaterialAllOfRenderer,
materialAnyOfControlTester,
MaterialAnyOfRenderer,
MaterialObjectRenderer,
materialOneOfControlTester,
MaterialOneOfRenderer,
@@ -17,7 +15,6 @@ 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 {
@@ -42,9 +39,8 @@ import {
vanillaRenderers,
InputControl,
} from "@jsonforms/vanilla-renderers";
import { useSchemas } from "./useSchemas";
import { RunState, useStreamLog } from "./useStreamLog";
import { useStreamLog } from "./useStreamLog";
import {
JsonFormsCore,
RankedTester,
@@ -59,18 +55,30 @@ import CustomArrayControlRenderer, {
} from "./components/CustomArrayControlRenderer";
import CustomTextAreaCell from "./components/CustomTextAreaCell";
import JsonTextAreaCell from "./components/JsonTextAreaCell";
import { cn } from "./utils/cn";
import { getStateFromUrl, ShareDialog } from "./components/ShareDialog";
import {
chatMessagesTester,
ChatMessagesControlRenderer,
} from "./components/ChatMessagesControlRenderer";
import {
ChatMessageTuplesControlRenderer,
chatMessagesTupleTester,
} from "./components/ChatMessageTuplesControlRenderer";
import {
fileBase64Tester,
FileBase64ControlRenderer,
} from "./components/FileBase64Tester";
import { IntermediateSteps } from "./components/IntermediateSteps";
import { StreamOutput } from "./components/StreamOutput";
import {
customAnyOfTester,
CustomAnyOfRenderer,
} from "./components/CustomAnyOfRenderer";
import { cn } from "./utils/cn";
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(
@@ -85,26 +93,33 @@ const isObjectWithPropertiesControl = rankWith(
const isObject = rankWith(1, and(uiTypeIs("Control"), schemaTypeIs("object")));
const isElse = rankWith(1, and(uiTypeIs("Control")));
const renderers = [
export const renderers = [
...vanillaRenderers,
// use material renderers to handle objects and json schema references
// they should yield the rendering to simpler cells
{ tester: isObjectWithPropertiesControl, renderer: MaterialObjectRenderer },
{ tester: materialAllOfControlTester, renderer: MaterialAllOfRenderer },
{ tester: materialAnyOfControlTester, renderer: MaterialAnyOfRenderer },
{ tester: materialOneOfControlTester, renderer: MaterialOneOfRenderer },
{ tester: customAnyOfTester, renderer: CustomAnyOfRenderer },
// custom renderers
{ tester: materialArrayControlTester, renderer: CustomArrayControlRenderer },
{ tester: isObject, renderer: InputControl },
{ tester: chatMessagesTester, renderer: ChatMessagesControlRenderer },
{
tester: chatMessagesTupleTester,
renderer: ChatMessageTuplesControlRenderer,
},
{ tester: fileBase64Tester, renderer: FileBase64ControlRenderer },
];
const nestedArrayControlTester: RankedTester = rankWith(1, (_, jsonSchema) => {
return jsonSchema.type === "array";
});
const cells = [
export const cells = [
{ tester: booleanCellTester, cell: BooleanCell },
{ tester: dateCellTester, cell: DateCell },
{ tester: dateTimeCellTester, cell: DateTimeCell },
@@ -119,41 +134,6 @@ const cells = [
{ 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);
@@ -184,7 +164,8 @@ function App() {
errors: [],
defaults: true,
});
setInputData({ data: null, errors: [] });
setInputData({ data: defaults(schemas.input), errors: [] });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [schemas.config]);
@@ -222,46 +203,109 @@ function App() {
return () => window.removeEventListener("message", listener);
}, []);
return schemas.config && schemas.input ? (
const isInputResetable = useMemo(() => {
if (!schemas.input) return false;
return (
JSON.stringify(defaults(schemas.input)) !== JSON.stringify(inputData.data)
);
}, [schemas.input, inputData.data]);
function onSubmit() {
if (
!stopStream &&
(!!inputData.errors?.length || !!configData.errors?.length)
) {
return;
}
if (stopStream) {
stopStream();
} else {
startStream(inputData.data, configData.data);
}
}
const submitRef = useRef<(() => void) | null>(null);
submitRef.current = onSubmit;
useEffect(() => {
window.addEventListener("keydown", (e) => {
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
submitRef.current?.();
}
});
}, []);
const isSendDisabled =
!stopStream &&
(!!inputData.errors?.length || !!configData.errors?.length)
if (!schemas.config || !schemas.input) {
return <></>;
}
return (
<div className="flex items-center flex-col text-ls-black bg-gradient-to-b from-[#F9FAFB] to-[#EFF8FF] min-h-[100dvh] dark:from-[#0C111C] dark:to-[#0C111C]">
<div className="flex flex-col flex-grow gap-4 px-4 pt-6 max-w-[800px] w-full">
<h1 className="text-2xl text-left">
<strong>🦜 LangServe</strong> Playground
</h1>
<div className="flex flex-col gap-3">
{!isIframe && <h2 className="text-xl font-semibold">Configure</h2>}
{Object.keys(schemas.config).length > 0 && (
<div className="flex flex-col gap-3 [&:has(.content>.vertical-layout:first-child:last-child:empty)]:hidden">
{!isIframe && <h2 className="text-xl font-semibold">Configure</h2>}
<JsonForms
schema={schemas.config}
data={configData.data}
renderers={renderers}
cells={cells}
onChange={({ data, errors }) =>
data
? setConfigData({ data, errors, defaults: false })
: undefined
}
/>
{!!configData.errors?.length && configData.data && (
<div className="bg-background rounded-xl">
<div className="bg-red-500/10 text-red-700 dark:text-red-300 rounded-xl p-3">
<strong className="font-bold">Validation Errors</strong>
<ul className="list-disc pl-5">
{configData.errors?.map((e, i) => (
<li key={i}>{e.message}</li>
))}
</ul>
</div>
<div className="content flex flex-col gap-3">
<JsonForms
schema={schemas.config}
data={configData.data}
renderers={renderers}
cells={cells}
onChange={({ data, errors }) =>
data
? setConfigData({ data, errors, defaults: false })
: undefined
}
/>
{!!configData.errors?.length && configData.data && (
<div className="bg-background rounded-xl">
<div className="bg-red-500/10 text-red-700 dark:text-red-300 rounded-xl p-3">
<strong className="font-bold">Validation Errors</strong>
<ul className="list-disc pl-5">
{configData.errors?.map((e, i) => (
<li key={i}>{e.message}</li>
))}
</ul>
</div>
</div>
)}
</div>
)}
</div>
</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>
<div className="flex items-center justify-between">
<h3 className="font-medium">Inputs</h3>
{isInputResetable && (
<button
type="button"
className="text-sm px-1 -mr-1 py-0.5 rounded-md hover:bg-divider-500/50 active:bg-divider-500 text-ls-gray-100"
onClick={() =>
setInputData({
data: defaults(schemas.input),
errors: [],
})
}
>
Reset
</button>
)}
</div>
<JsonForms
schema={schemas.input}
@@ -286,7 +330,7 @@ function App() {
<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("") || "..."}
<StreamOutput streamed={latest.streamed_output} />
</div>
<IntermediateSteps latest={latest} />
</div>
@@ -344,19 +388,33 @@ function App() {
</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)
}
className={cn("px-4 py-3 gap-3 font-medium border border-transparent rounded-full flex items-center justify-center bg-blue-500 disabled:opacity-50 transition-colors", !isSendDisabled ? "hover:bg-blue-600 active:bg-blue-700" : "")}
onClick={onSubmit}
disabled={isSendDisabled}
>
{stopStream ? (
<span className="text-white">Stop</span>
<>
<div role="status">
<svg
aria-hidden="true"
className="w-5 h-5 animate-spin text-white fill-ls-blue"
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill"
/>
</svg>
<span className="sr-only">Loading...</span>
</div>
<span className="text-white">Stop</span>
</>
) : (
<>
<SendIcon className="flex-shrink-0" />
@@ -369,7 +427,7 @@ function App() {
</div>
</div>
</div>
) : null;
);
}
export default App;
@@ -0,0 +1,115 @@
import { withJsonFormsControlProps } from "@jsonforms/react";
import PlusIcon from "../assets/PlusIcon.svg?react";
import TrashIcon from "../assets/TrashIcon.svg?react";
import {
rankWith,
and,
schemaMatches,
Paths,
isControl,
} from "@jsonforms/core";
import { AutosizeTextarea } from "./AutosizeTextarea";
import { isJsonSchemaExtra } from "../utils/schema";
type MessageTuple = [string, string];
export const chatMessagesTupleTester = rankWith(
12,
and(
isControl,
schemaMatches((schema) => {
if (schema.type !== "array") return false;
if (typeof schema.items !== "object" || schema.items == null)
return false;
if (!isJsonSchemaExtra(schema) || schema.extra.widget.type !== "chat") {
return false;
}
if ("type" in schema.items) {
return (
schema.items.type === "array" &&
schema.items.minItems === 2 &&
schema.items.maxItems === 2 &&
Array.isArray(schema.items.items) &&
schema.items.items.length === 2 &&
schema.items.items.every((schema) => schema.type === "string")
);
}
return false;
})
)
);
export const ChatMessageTuplesControlRenderer = withJsonFormsControlProps(
(props) => {
const data: Array<MessageTuple> = props.data ?? [];
return (
<div className="control">
<div className="flex items-center justify-between">
<label className="text-xs uppercase font-semibold text-ls-gray-100">
{props.label || "Messages"}
</label>
<button
className="p-1 rounded-full"
onClick={() => {
const lastRole = data.length ? data[data.length - 1][0] : "ai";
props.handleChange(props.path, [
...data,
[lastRole === "ai" ? "human" : "ai", ""],
]);
}}
>
<PlusIcon className="w-5 h-5" />
</button>
</div>
<div className="flex flex-col gap-3 mt-1 empty:hidden">
{data.map(([type, content], index) => {
const msgPath = Paths.compose(props.path, `${index}`);
return (
<div className="control group" key={index}>
<div className="flex items-start justify-between gap-2">
<select
className="-ml-1 min-w-[100px]"
value={type}
onChange={(e) => {
props.handleChange(
Paths.compose(msgPath, "0"),
e.target.value
);
}}
>
<option value="human">Human</option>
<option value="ai">AI</option>
<option value="system">System</option>
</select>
<button
className="p-1 border rounded opacity-0 transition-opacity border-divider-700 group-focus-within:opacity-100 group-hover:opacity-100"
onClick={() => {
props.handleChange(
props.path,
data.filter((_, i) => i !== index)
);
}}
>
<TrashIcon className="w-4 h-4" />
</button>
</div>
<AutosizeTextarea
value={content}
onChange={(content) => {
props.handleChange(Paths.compose(msgPath, "1"), content);
}}
/>
</div>
);
})}
</div>
</div>
);
}
);
@@ -0,0 +1,172 @@
import { withJsonFormsControlProps } from "@jsonforms/react";
import PlusIcon from "../assets/PlusIcon.svg?react";
import TrashIcon from "../assets/TrashIcon.svg?react";
import {
rankWith,
and,
schemaMatches,
Paths,
isControl,
} from "@jsonforms/core";
import { AutosizeTextarea } from "./AutosizeTextarea";
export const chatMessagesTester = rankWith(
12,
and(
isControl,
schemaMatches((schema) => {
if (schema.type !== "array") return false;
if (typeof schema.items !== "object" || schema.items == null)
return false;
if (
"type" in schema.items &&
schema.items.type != null &&
schema.items.title != null
) {
return (
schema.items.type === "object" &&
(schema.items.title?.endsWith("Message") ||
schema.items.title?.endsWith("MessageChunk"))
);
}
if ("anyOf" in schema.items && schema.items.anyOf != null) {
return schema.items.anyOf.every((schema) => {
const isObjectMessage =
schema.type === "object" &&
(schema.title?.endsWith("Message") ||
schema.title?.endsWith("MessageChunk"));
const isTupleMessage =
schema.type === "array" &&
schema.minItems === 2 &&
schema.maxItems === 2 &&
Array.isArray(schema.items) &&
schema.items.length === 2 &&
schema.items.every((schema) => schema.type === "string");
return isObjectMessage || isTupleMessage;
});
}
return false;
})
)
);
interface MessageFields {
content: string;
additional_kwargs?: { [key: string]: unknown };
name?: string;
type?: string;
role?: string;
}
export const ChatMessagesControlRenderer = withJsonFormsControlProps(
(props) => {
const data: Array<MessageFields> = props.data ?? [];
return (
<div className="control">
<div className="flex items-center justify-between">
<label className="text-xs uppercase font-semibold text-ls-gray-100">
{props.label || "Messages"}
</label>
<button
className="p-1 rounded-full"
onClick={() => {
const lastRole = data.length ? data[data.length - 1].type : "ai";
props.handleChange(props.path, [
...data,
{ content: "", type: lastRole === "human" ? "ai" : "human" },
]);
}}
>
<PlusIcon className="w-5 h-5" />
</button>
</div>
<div className="flex flex-col gap-3 mt-1 empty:hidden">
{data.map((message, index) => {
const msgPath = Paths.compose(props.path, `${index}`);
const type = message.type ?? "chat";
return (
<div className="control group" key={index}>
<div className="flex items-start justify-between gap-2">
<select
className="-ml-1 min-w-[100px]"
value={type}
onChange={(e) => {
props.handleChange(
Paths.compose(msgPath, "type"),
e.target.value
);
}}
>
<option value="human">Human</option>
<option value="ai">AI</option>
<option value="system">System</option>
<option value="function">Function</option>
<option value="chat">Chat</option>
</select>
<button
className="p-1 border rounded opacity-0 transition-opacity border-divider-700 group-focus-within:opacity-100 group-hover:opacity-100"
onClick={() => {
props.handleChange(
props.path,
data.filter((_, i) => i !== index)
);
}}
>
<TrashIcon className="w-4 h-4" />
</button>
</div>
{type === "chat" && (
<input
className="mb-1"
placeholder="Role"
value={message.role ?? ""}
onChange={(e) => {
props.handleChange(
Paths.compose(msgPath, "role"),
e.target.value
);
}}
/>
)}
{type === "function" && (
<input
className="mb-1"
placeholder="Function Name"
value={message.name ?? ""}
onChange={(e) => {
props.handleChange(
Paths.compose(msgPath, "name"),
e.target.value
);
}}
/>
)}
<AutosizeTextarea
value={message.content}
onChange={(content) => {
props.handleChange(
Paths.compose(msgPath, "content"),
content
);
}}
/>
</div>
);
})}
</div>
</div>
);
}
);
@@ -0,0 +1,37 @@
import { JsonFormsDispatch, withJsonFormsAnyOfProps } from "@jsonforms/react";
import {
rankWith,
createCombinatorRenderInfos,
JsonSchema,
isAnyOfControl,
} from "@jsonforms/core";
import { renderers, cells } from "../App";
export const CustomAnyOfRenderer = withJsonFormsAnyOfProps((props) => {
const anyOfRenderInfos = createCombinatorRenderInfos(
(props.schema as JsonSchema).anyOf!,
props.rootSchema,
"anyOf",
props.uischema,
props.path,
props.uischemas
);
// just assume the last type is the selected one
// for `anyOf` caused by passing inputs from LLMs/Chat Models
// this will result in showing the Message renderer
const selectedIndex = anyOfRenderInfos.length - 1;
const selectedAnyOfRenderInfo = anyOfRenderInfos[selectedIndex];
return (
<JsonFormsDispatch
schema={selectedAnyOfRenderInfo.schema}
uischema={selectedAnyOfRenderInfo.uischema}
path={props.path}
renderers={renderers}
cells={cells}
/>
);
});
export const customAnyOfTester = rankWith(3, isAnyOfControl);
@@ -84,7 +84,7 @@ export const MaterialArrayControlRenderer = (props: ArrayLayoutProps) => {
// eslint-disable-next-line react-refresh/only-export-components
export const materialArrayControlTester: RankedTester = rankWith(
999,
11,
or(isObjectArrayControl, isPrimitiveArrayControl, isObjectArrayWithNesting)
);
@@ -0,0 +1,44 @@
import { ChangeEvent } from "react";
import { withJsonFormsControlProps } from "@jsonforms/react";
import { rankWith, and, schemaMatches, isControl } from "@jsonforms/core";
import { isJsonSchemaExtra } from "../utils/schema";
export const fileBase64Tester = rankWith(
12,
and(
isControl,
schemaMatches((schema) => {
if (!isJsonSchemaExtra(schema)) return false;
return schema.extra.widget.type === "base64file";
})
)
);
export const FileBase64ControlRenderer = withJsonFormsControlProps((props) => {
const handleFileUpload = (event: ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = () => {
const base64String = reader.result as string | null;
if (base64String != null) {
const prefix = base64String.indexOf("base64,") + "base64,".length;
props.handleChange(props.path, base64String.slice(prefix));
}
};
reader.readAsDataURL(file);
};
return (
<div className="control">
<label className="text-xs uppercase font-semibold text-ls-gray-100">
{props.label}
</label>
<input type="file" onChange={handleFileUpload} />
</div>
);
});
@@ -0,0 +1,53 @@
import { useState } from "react";
import dayjs from "dayjs";
import ChevronRight from "../assets/ChevronRight.svg?react";
import { RunState } from "../useStreamLog";
import { cn } from "../utils/cn";
import { str } from "../utils/str";
export function IntermediateSteps(props: { latest: RunState }) {
const [expanded, setExpanded] = useState(false);
const length = Object.values(props.latest.logs).length;
const disabled = length === 0;
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"
disabled={disabled}
onClick={() => setExpanded((open) => !open)}
>
<span>
Intermediate steps{" "}
<span className="bg-ls-gray-400 text-ls-gray-100 text-sm px-1 py-0.5 rounded-md ml-1">
{length}
</span>
</span>
<ChevronRight
className={cn(
"transition-all",
expanded && "rotate-90",
disabled && "opacity-20"
)}
/>
</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>
);
}
@@ -0,0 +1,108 @@
import { str } from "../utils/str";
// inlined from langchain/schema
interface BaseMessageFields {
content: string;
name?: string;
additional_kwargs?: {
[key: string]: unknown;
};
}
class AIMessageChunk {
/** The text of the message. */
content: string;
/** The name of the message sender in a multi-user chat. */
name?: string;
/** Additional keyword arguments */
additional_kwargs: NonNullable<BaseMessageFields["additional_kwargs"]>;
constructor(fields: BaseMessageFields) {
// Make sure the default value for additional_kwargs is passed into super() for serialization
if (!fields.additional_kwargs) {
// eslint-disable-next-line no-param-reassign
fields.additional_kwargs = {};
}
this.name = fields.name;
this.content = fields.content;
this.additional_kwargs = fields.additional_kwargs;
}
static _mergeAdditionalKwargs(
left: NonNullable<BaseMessageFields["additional_kwargs"]>,
right: NonNullable<BaseMessageFields["additional_kwargs"]>
): NonNullable<BaseMessageFields["additional_kwargs"]> {
const merged = { ...left };
for (const [key, value] of Object.entries(right)) {
if (merged[key] === undefined) {
merged[key] = value;
} else if (typeof merged[key] !== typeof value) {
throw new Error(
`additional_kwargs[${key}] already exists in the message chunk, but with a different type.`
);
} else if (typeof merged[key] === "string") {
merged[key] = (merged[key] as string) + value;
} else if (
!Array.isArray(merged[key]) &&
typeof merged[key] === "object"
) {
merged[key] = this._mergeAdditionalKwargs(
merged[key] as NonNullable<BaseMessageFields["additional_kwargs"]>,
value as NonNullable<BaseMessageFields["additional_kwargs"]>
);
} else {
throw new Error(
`additional_kwargs[${key}] already exists in this message chunk.`
);
}
}
return merged;
}
concat(chunk: AIMessageChunk) {
return new AIMessageChunk({
content: this.content + chunk.content,
additional_kwargs: AIMessageChunk._mergeAdditionalKwargs(
this.additional_kwargs,
chunk.additional_kwargs
),
});
}
}
function isAiMessageChunkFields(value: unknown): value is BaseMessageFields {
if (typeof value !== "object" || value == null) return false;
return "content" in value && typeof value["content"] === "string";
}
function isAiMessageChunkFieldsList(
value: unknown[]
): value is BaseMessageFields[] {
return value.length > 0 && value.every((x) => isAiMessageChunkFields(x));
}
export function StreamOutput(props: { streamed: unknown[] }) {
// check if we're streaming AIMessageChunk
if (isAiMessageChunkFieldsList(props.streamed)) {
const concat = props.streamed.reduce<AIMessageChunk | null>(
(memo, field) => {
const chunk = new AIMessageChunk(field);
if (memo == null) return chunk;
return memo.concat(chunk);
},
null
);
const functionCall = concat?.additional_kwargs?.function_call;
return (
concat?.content ||
(!!functionCall && JSON.stringify(functionCall, null, 2)) ||
"..."
);
}
return props.streamed.map(str).join("") || "...";
}
@@ -1,5 +0,0 @@
declare module "json-schema-defaults" {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function defaults(schema: any): any;
export = defaults;
}
+7 -2
View File
@@ -17,7 +17,12 @@ declare global {
export function useSchemas(
configData: Pick<JsonFormsCore, "data" | "errors"> & { defaults: boolean }
) {
const [schemas, setSchemas] = useState({
const [schemas, setSchemas] = useState<{
// eslint-disable-next-line @typescript-eslint/no-explicit-any
config: null | any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
input: null | any;
}>({
config: null,
input: null,
});
@@ -55,7 +60,7 @@ export function useSchemas(
if (!debouncedConfigData.defaults) {
fetch(
resolveApiUrl(
`c/${compressToEncodedURIComponent(
`/c/${compressToEncodedURIComponent(
JSON.stringify(debouncedConfigData.data)
)}/input_schema`
)
+220
View File
@@ -0,0 +1,220 @@
// (c) 2015 Chute Corporation. Released under the terms of the MIT License.
// Modified to use TypeScript and handle edge cases with tuples
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable no-prototype-builtins */
"use strict";
/**
* check whether item is plain object
* @param {*} item
* @return {Boolean}
*/
const isObject = (item: unknown): item is Record<string, unknown> => {
return (
typeof item === "object" &&
item !== null &&
item.toString() === {}.toString()
);
};
/**
* deep JSON object clone
*
* @param {Object} source
* @return {Object}
*/
const cloneJSON = (source: any): any => {
return JSON.parse(JSON.stringify(source));
};
/**
* returns a result of deep merge of two objects
*
* @param {Object} target
* @param {Object} source
* @return {Object}
*/
const merge = (
target: Record<string, unknown>,
source: Record<string, unknown>
) => {
target = cloneJSON(target);
for (const key in source) {
if (source.hasOwnProperty(key)) {
const sourceKeyValue = source[key];
const targetKeyValue = target[key];
if (isObject(sourceKeyValue) && isObject(targetKeyValue)) {
target[key] = merge(targetKeyValue, sourceKeyValue);
} else {
target[key] = sourceKeyValue;
}
}
}
return target;
};
/**
* get object by reference. works only with local references that points on
* definitions object
*
* @param {String} path
* @param {Object} definitions
* @return {Object}
*/
const getLocalRef = function (
inputPath: string,
definitions: Record<string, unknown>
) {
const path = inputPath.replace(/^#\/definitions\//, "").split("/");
const find = function (path: string[], root: any): any {
const key = path.shift();
if (!key) return {};
if (!root[key]) {
return {};
} else if (!path.length) {
return root[key];
} else {
return find(path, root[key]);
}
};
const result = find(path, definitions);
if (!isObject(result)) {
return result;
}
return cloneJSON(result);
};
/**
* merge list of objects from allOf properties
* if some of objects contains $ref field extracts this reference and merge it
*
* @param {Array} allOfList
* @param {Object} definitions
* @return {Object}
*/
const mergeAllOf = function (allOfList: any[], definitions: any) {
const length = allOfList.length;
let index = -1,
result = {};
while (++index < length) {
let item = allOfList[index];
item =
typeof item.$ref !== "undefined"
? getLocalRef(item.$ref, definitions)
: item;
result = merge(result, item);
}
return result;
};
/**
* returns a object that built with default values from json schema
*
* @param {Object} schema
* @param {Object} definitions
* @return {Object}
*/
const defaults = (schema: any, definitions: Record<string, any>): unknown => {
if (typeof schema["default"] !== "undefined") {
return schema["default"];
} else if (typeof schema.allOf !== "undefined") {
const mergedItem = mergeAllOf(schema.allOf, definitions);
return defaults(mergedItem, definitions);
} else if (typeof schema.$ref !== "undefined") {
const reference = getLocalRef(schema.$ref, definitions);
return defaults(reference, definitions);
} else if (schema.type === "object") {
if (!schema.properties) {
return {};
}
for (const key in schema.properties) {
if (schema.properties.hasOwnProperty(key)) {
schema.properties[key] = defaults(schema.properties[key], definitions);
if (typeof schema.properties[key] === "undefined") {
delete schema.properties[key];
}
}
}
return schema.properties;
} else if (schema.type === "array") {
if (!schema.items) {
return [];
}
// minimum item count
const ct = schema.minItems || 0;
// tuple-typed arrays
if (schema.items.constructor === Array) {
const values = schema.items.map((item: unknown) =>
defaults(item, definitions)
);
// remove undefined items at the end (unless required by minItems)
for (let i = values.length - 1; i >= 0; i--) {
if (typeof values[i] !== "undefined") {
break;
}
if (i + 1 > ct) {
values.pop();
}
}
// if all values are undefined -> return undefined even
// if minItems is set
if (values.every((item: unknown) => typeof item === "undefined")) {
return undefined;
}
return values;
}
// object-typed arrays
const value = defaults(schema.items, definitions);
if (typeof value === "undefined") {
return [];
} else {
const values = [];
for (let i = 0; i < Math.max(1, ct); i++) {
values.push(cloneJSON(value));
}
return values;
}
}
};
/**
* main function
*
* @param {Object} schema
* @param {Object|undefined} definitions
* @return {Object}
*/
export default function (
schema: any,
definitions?: Record<string, unknown> | undefined
) {
if (typeof definitions === "undefined") {
definitions = (schema.definitions as Record<string, unknown>) || {};
} else if (isObject(schema.definitions)) {
definitions = merge(definitions, schema.definitions);
}
return defaults(cloneJSON(schema), definitions);
}
+27
View File
@@ -0,0 +1,27 @@
import { JsonSchema } from "@jsonforms/core";
type JsonSchemaExtra = JsonSchema & {
extra: {
widget: {
type: string;
};
};
};
export function isJsonSchemaExtra(x: JsonSchema): x is JsonSchemaExtra {
if (!("extra" in x && typeof x.extra === "object" && x.extra != null)) {
return false;
}
if (
!(
"widget" in x.extra &&
typeof x.extra.widget === "object" &&
x.extra.widget != null
)
) {
return false;
}
return true;
}
+5
View File
@@ -0,0 +1,5 @@
export function str(o: unknown): React.ReactNode {
return typeof o === "object"
? JSON.stringify(o, null, 2)
: (o as React.ReactNode);
}
+2 -5
View File
@@ -1,8 +1,5 @@
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];
let prefix = window.location.pathname.split("/playground")[0];
if (prefix.endsWith("/")) prefix = prefix.slice(0, -1);
return new URL(prefix + path, window.location.origin);
}
+9
View File
@@ -6,4 +6,13 @@ import svgr from "vite-plugin-svgr";
export default defineConfig({
base: "/____LANGSERVE_BASE_URL/",
plugins: [svgr(), react()],
server: {
proxy: {
"^/____LANGSERVE_BASE_URL.*/(config_schema|input_schema|stream_log)": {
target: "http://127.0.0.1:8000",
changeOrigin: true,
rewrite: (path) => path.replace("/____LANGSERVE_BASE_URL", ""),
},
},
},
});
-19
View File
@@ -1224,13 +1224,6 @@ arg@^5.0.2:
resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c"
integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==
argparse@^1.0.9:
version "1.0.10"
resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==
dependencies:
sprintf-js "~1.0.2"
argparse@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
@@ -1985,13 +1978,6 @@ json-parse-even-better-errors@^2.3.0:
resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
json-schema-defaults@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/json-schema-defaults/-/json-schema-defaults-0.4.0.tgz#b63ee7e7aa83f29b54cb31d31ecddeb056c3306c"
integrity sha512-UsUrkDVNvHTneyeQOYHH9ZHb3+6OjwYfJ831SdO0yjtXtYZ7Jh8BKWsuJYUQW7qckP5JhHawsg4GI6A5fMaR/Q==
dependencies:
argparse "^1.0.9"
json-schema-traverse@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
@@ -2522,11 +2508,6 @@ source-map@^0.5.7:
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==
sprintf-js@~1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==
strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
+61
View File
@@ -0,0 +1,61 @@
from typing import List
try:
from pydantic.v1 import BaseModel
except ImportError:
from pydantic import BaseModel
class CustomUserType(BaseModel):
"""Inherit from this class to create a custom user type.
Use a custom user type if you want the data to de-serialize
into a pydantic model rather than the equivalent dict representation.
In general, make sure to add a `type` attribute to your class
to help pydantic to discriminate unions.
https://docs.pydantic.dev/1.10/usage/types/#discriminated-unions-aka-tagged-unions
Limitations:
At the moment, this type only works SERVER side and is used
to specify desired DECODING behavior. If inheriting from this type
the server will keep the decoded type as a pydantic model instead
of converting it into a dict.
"""
class SharedResponseMetadata(BaseModel):
"""
Any response metadata should inherit from this class. Response metadata
represents non-output data that may be useful to some clients, but
ignorable to most. For example, the run_ids associated with each run
kicked off by the associated request.
SharedResponseMetadata is an abstraction to represent any metadata
representing a LangServe response shared across all outputs in said
response.
"""
pass
class SingletonResponseMetadata(SharedResponseMetadata):
"""
Represents response metadata used for just single input/output LangServe
responses.
"""
# Represents the parent run id for a given request
run_id: str
class BatchResponseMetadata(SharedResponseMetadata):
"""
Represents response metadata used for batches of input/output LangServe
responses.
"""
# Represents each parent run id for a given request, in
# the same order in which they were received
run_ids: List[str]
+156 -25
View File
@@ -1,10 +1,20 @@
"""Serialization module for Well Known LangChain objects.
"""Serialization for well known objects and callback events.
Specialized JSON serialization for well known LangChain objects that
can be expected to be frequently transmitted between chains.
Callback events handle well known objects together with a few other
common types like UUIDs and Exceptions that might appear in the callback.
By default, exceptions are serialized as a generic exception without
any information about the exception. This is done to prevent leaking
sensitive information from the server to the client.
"""
import abc
import json
from typing import Any, Union
import logging
from functools import lru_cache
from typing import Any, Dict, List, Union
from langchain.prompts.base import StringPromptValue
from langchain.prompts.chat import ChatPromptValueConcrete
@@ -22,6 +32,14 @@ from langchain.schema.messages import (
SystemMessage,
SystemMessageChunk,
)
from langchain.schema.output import (
ChatGeneration,
ChatGenerationChunk,
Generation,
LLMResult,
)
from langserve.validation import CallbackEvent
try:
from pydantic.v1 import BaseModel, ValidationError
@@ -29,6 +47,15 @@ except ImportError:
from pydantic import BaseModel, ValidationError
logger = logging.getLogger(__name__)
@lru_cache(maxsize=1_000) # Will accommodate up to 1_000 different error messages
def _log_error_message_once(error_message: str) -> None:
"""Log an error message once."""
logger.error(error_message)
class WellKnownLCObject(BaseModel):
"""A well known LangChain object.
@@ -54,6 +81,10 @@ class WellKnownLCObject(BaseModel):
AgentAction,
AgentFinish,
AgentActionMessageLog,
LLMResult,
ChatGeneration,
Generation,
ChatGenerationChunk,
]
@@ -67,41 +98,141 @@ class _LangChainEncoder(json.JSONEncoder):
return super().default(obj)
# Custom JSON Decoder
class _LangChainDecoder(json.JSONDecoder):
"""Custom JSON Decoder that handles well known LangChain objects."""
def _decode_lc_objects(value: Any) -> Any:
"""Decode the value."""
if isinstance(value, dict):
v = {key: _decode_lc_objects(v) for key, v in value.items()}
def __init__(self, *args: Any, **kwargs: Any) -> None:
"""Initialize the LangChainDecoder."""
super().__init__(object_hook=self.decoder, *args, **kwargs)
try:
obj = WellKnownLCObject.parse_obj(v)
parsed = obj.__root__
if set(parsed.dict()) != set(value):
raise ValueError("Invalid object")
return parsed
except (ValidationError, ValueError):
return v
elif isinstance(value, list):
return [_decode_lc_objects(item) for item in value]
else:
return value
def decoder(self, value) -> Any:
"""Decode the value."""
if isinstance(value, dict):
class ServerSideException(Exception):
"""Exception raised when a server side exception occurs.
The goal of this exception is to provide a way to communicate
to the client that a server side exception occurred without
revealing too much information about the exception as it may contain
sensitive information.
"""
def _decode_event_data(value: Any) -> Any:
"""Decode the event data from a JSON object representation."""
if isinstance(value, dict):
try:
obj = CallbackEvent.parse_obj(value)
return obj.__root__
except ValidationError:
try:
obj = WellKnownLCObject.parse_obj(value)
return obj.__root__
except ValidationError:
return {key: self.decoder(v) for key, v in value.items()}
elif isinstance(value, list):
return [self.decoder(item) for item in value]
else:
return value
return {key: _decode_event_data(v) for key, v in value.items()}
elif isinstance(value, list):
return [_decode_event_data(item) for item in value]
else:
return value
# PUBLIC API
def simple_dumpd(obj: Any) -> Any:
"""Convert the given object to a JSON serializable object."""
return json.loads(json.dumps(obj, cls=_LangChainEncoder))
class Serializer(abc.ABC):
@abc.abstractmethod
def dumpd(self, obj: Any) -> Any:
"""Convert the given object to a JSON serializable object."""
@abc.abstractmethod
def dumps(self, obj: Any) -> str:
"""Dump the given object as a JSON string."""
@abc.abstractmethod
def loads(self, s: str) -> Any:
"""Load the given JSON string."""
@abc.abstractmethod
def loadd(self, obj: Any) -> Any:
"""Load the given object."""
def simple_dumps(obj: Any) -> str:
"""Dump the given object as a JSON string."""
return json.dumps(obj, cls=_LangChainEncoder)
class WellKnownLCSerializer(Serializer):
def dumpd(self, obj: Any) -> Any:
"""Convert the given object to a JSON serializable object."""
return json.loads(json.dumps(obj, cls=_LangChainEncoder)) # :*(
def dumps(self, obj: Any) -> str:
"""Dump the given object as a JSON string."""
return json.dumps(obj, cls=_LangChainEncoder)
def loadd(self, obj: Any) -> Any:
return _decode_lc_objects(obj)
def loads(self, s: str) -> Any:
"""Load the given JSON string."""
return self.loadd(json.loads(s))
def simple_loads(s: str) -> Any:
"""Load the given JSON string."""
return json.loads(s, cls=_LangChainDecoder)
def _project_top_level(model: BaseModel) -> Dict[str, Any]:
"""Project the top level of the model as dict."""
return {key: getattr(model, key) for key in model.__fields__}
def load_events(events: Any) -> List[Dict[str, Any]]:
"""Load and validate the event.
Args:
events: The events to load and validate.
Returns:
The loaded and validated events.
"""
if not isinstance(events, list):
_log_error_message_once(f"Expected a list got {type(events)}")
return []
decoded_events = []
for event in events:
if not isinstance(event, dict):
_log_error_message_once(f"Expected a dict got {type(event)}")
# Discard the event / potentially error
continue
# First load all inner objects
decoded_event_data = {
key: _decode_lc_objects(value) for key, value in event.items()
}
# Then validate the event
try:
full_event = CallbackEvent.parse_obj(decoded_event_data)
except ValidationError as e:
msg = f"Encountered an invalid event: {e}"
if "type" in decoded_event_data:
msg += f' of type {repr(decoded_event_data["type"])}'
_log_error_message_once(msg)
continue
decoded_event_data = _project_top_level(full_event.__root__)
if decoded_event_data["type"].endswith("_error"):
# Data is validated by this point, so we can assume that the shape
# of the data is correct
error = decoded_event_data["error"]
msg = f"{error['status_code']}: {error['message']}"
decoded_event_data["error"] = ServerSideException(msg)
decoded_events.append(decoded_event_data)
return decoded_events
+642 -214
View File
File diff suppressed because it is too large Load Diff
+267 -1
View File
@@ -16,7 +16,18 @@ 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
from typing import Any, Dict, List, Literal, Optional, Sequence, Union
from uuid import UUID
from langchain.schema import (
BaseMessage,
ChatGeneration,
Document,
Generation,
RunInfo,
)
from langserve.schema import BatchResponseMetadata, SingletonResponseMetadata
try:
from pydantic.v1 import BaseModel, Field, create_model
@@ -194,6 +205,20 @@ def create_invoke_response_model(
invoke_response_type = create_model(
f"{namespace}InvokeResponse",
output=(output_type, Field(..., description="The output of the invocation.")),
callback_events=(
List[CallbackEvent],
Field(..., description="Callback events generated by the server side."),
),
metadata=(
SingletonResponseMetadata,
Field(
...,
description=(
"Metadata about the response that may be useful to "
"specific clients"
),
),
),
)
invoke_response_type.update_forward_refs()
return invoke_response_type
@@ -217,6 +242,247 @@ def create_batch_response_model(
),
),
),
callback_events=(
List[List[CallbackEvent]],
Field(
...,
description=(
"Callback events generated by the server side."
"The outer list corresponds to the inputs and the inner "
"list corresponds to the callbacks generated for that input."
),
),
),
metadata=(
BatchResponseMetadata,
Field(
...,
description=(
"Metadata about the response that may be useful to specific clients"
),
),
),
)
batch_response_type.update_forward_refs()
return batch_response_type
class InvokeRequestShallowValidator(BaseModel):
"""Shallow validator for Invoke Request.
Validate basic shape of invoke request, downstream code
is expected to do further validation.
"""
input: Any = Field(..., description="The input to the runnable.")
config: Optional[Dict[str, Any]] = Field(default_factory=dict)
class BatchRequestShallowValidator(BaseModel):
"""Shallow validator for Batch Request."""
inputs: Any = Field(..., description="The inputs to the runnable.")
config: Optional[Union[Dict[str, Any], List[Dict[str, Any]]]] = Field(
default_factory=dict
)
class StreamLogParameters(BaseModel):
"""Shallow validator for Stream Log Request"""
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
# Pydantic validators for callback events
# These objects may have a slightly different shape than the callback events
# used internally in langchain because they represent a serialized version
# of the callback event.
# For example, exceptions are replaced by error objects consisting of a
# status code and a message.
class OnChainStart(BaseModel):
"""On Chain Start Callback Event."""
serialized: Dict[str, Any]
inputs: Any
run_id: UUID
parent_run_id: Optional[UUID] = None
tags: Optional[List[str]] = None
metadata: Optional[Dict[str, Any]] = None
kwargs: Any = None
type: Literal["on_chain_start"] = "on_chain_start"
class OnChainEnd(BaseModel):
"""On Chain End Callback Event."""
outputs: Any
run_id: UUID
parent_run_id: Optional[UUID] = None
tags: Optional[List[str]] = None
kwargs: Any = None
type: Literal["on_chain_end"] = "on_chain_end"
class Error(BaseModel):
"""Error object that is modeled after an HTTP error format."""
status_code: int
message: str
type: Literal["error"] = "error"
class OnChainError(BaseModel):
"""On Chain Error Callback Event."""
error: Error
run_id: UUID
parent_run_id: Optional[UUID] = None
tags: Optional[List[str]] = None
kwargs: Any = None
type: Literal["on_chain_error"] = "on_chain_error"
class OnToolStart(BaseModel):
"""On Tool Start Callback Event."""
serialized: Dict[str, Any]
input_str: str
run_id: UUID
parent_run_id: Optional[UUID] = None
tags: Optional[List[str]] = None
metadata: Optional[Dict[str, Any]] = None
kwargs: Any = None
type: Literal["on_tool_start"] = "on_tool_start"
class OnToolEnd(BaseModel):
"""On Tool End Callback Event."""
output: str
run_id: UUID
parent_run_id: Optional[UUID] = None
tags: Optional[List[str]] = None
kwargs: Any = None
type: Literal["on_tool_end"] = "on_tool_end"
class OnToolError(BaseModel):
"""On Tool Error Callback Event."""
error: Error
run_id: UUID
parent_run_id: Optional[UUID] = None
tags: Optional[List[str]] = None
kwargs: Any = None
type: Literal["on_tool_error"] = "on_tool_error"
class OnChatModelStart(BaseModel):
"""On Chat Model Start Callback Event."""
serialized: Dict[str, Any]
messages: List[List[BaseMessage]]
run_id: UUID
parent_run_id: Optional[UUID] = None
tags: Optional[List[str]] = None
metadata: Optional[Dict[str, Any]] = None
kwargs: Any = None
type: Literal["on_chat_model_start"] = "on_chat_model_start"
class OnLLMStart(BaseModel):
"""On LLM Start Callback Event."""
serialized: Dict[str, Any]
prompts: List[str]
run_id: UUID
parent_run_id: Optional[UUID] = None
tags: Optional[List[str]] = None
metadata: Optional[Dict[str, Any]] = None
kwargs: Any = None
type: Literal["on_llm_start"] = "on_llm_start"
class LLMResult(BaseModel):
"""Concrete instance of LLMResult for validation only.
Must be kept in sync with langchain.schema.llm.LLMResult.
"""
generations: List[List[Union[Generation, ChatGeneration]]]
"""List of generated outputs. This is a List[List[]] because
each input could have multiple candidate generations."""
llm_output: Optional[dict] = None
"""Arbitrary LLM provider-specific output."""
run: Optional[List[RunInfo]] = None
"""List of metadata info for model call for each input."""
class OnLLMEnd(BaseModel):
"""On LLM End Callback Event."""
response: LLMResult
run_id: UUID
parent_run_id: Optional[UUID] = None
tags: Optional[List[str]] = None
kwargs: Any = None
type: Literal["on_llm_end"] = "on_llm_end"
class OnRetrieverStart(BaseModel):
"""On Retriever Start Callback Event."""
serialized: Dict[str, Any]
query: str
run_id: UUID
parent_run_id: Optional[UUID] = None
tags: Optional[List[str]] = None
metadata: Optional[Dict[str, Any]] = None
kwargs: Any = None
type: Literal["on_retriever_start"] = "on_retriever_start"
class OnRetrieverError(BaseModel):
"""On Retriever Error Callback Event."""
error: Error
run_id: UUID
parent_run_id: Optional[UUID] = None
tags: Optional[List[str]] = None
kwargs: Any = None
type: Literal["on_retriever_error"] = "on_retriever_error"
class OnRetrieverEnd(BaseModel):
"""On Retriever End Callback Event."""
documents: Sequence[Document]
run_id: UUID
parent_run_id: Optional[UUID] = None
tags: Optional[List[str]] = None
kwargs: Any = None
type: Literal["on_retriever_end"] = "on_retriever_end"
class CallbackEvent(BaseModel):
__root__: Union[
OnChainStart,
OnChainEnd,
OnChainError,
OnChatModelStart,
OnLLMStart,
OnLLMEnd,
OnToolStart,
OnToolEnd,
OnToolError,
OnRetrieverStart,
OnRetrieverEnd,
OnRetrieverError,
]
Generated
+35 -14
View File
@@ -966,7 +966,7 @@ files = [
{file = "greenlet-3.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0b72b802496cccbd9b31acea72b6f87e7771ccfd7f7927437d592e5c92ed703c"},
{file = "greenlet-3.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:527cd90ba3d8d7ae7dceb06fda619895768a46a1b4e423bdb24c1969823b8362"},
{file = "greenlet-3.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:37f60b3a42d8b5499be910d1267b24355c495064f271cfe74bf28b17b099133c"},
{file = "greenlet-3.0.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1482fba7fbed96ea7842b5a7fc11d61727e8be75a077e603e8ab49d24e234383"},
{file = "greenlet-3.0.0-cp311-universal2-macosx_10_9_universal2.whl", hash = "sha256:c3692ecf3fe754c8c0f2c95ff19626584459eab110eaab66413b1e7425cd84e9"},
{file = "greenlet-3.0.0-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:be557119bf467d37a8099d91fbf11b2de5eb1fd5fc5b91598407574848dc910f"},
{file = "greenlet-3.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73b2f1922a39d5d59cc0e597987300df3396b148a9bd10b76a058a2f2772fc04"},
{file = "greenlet-3.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1e22c22f7826096ad503e9bb681b05b8c1f5a8138469b255eb91f26a76634f2"},
@@ -976,6 +976,7 @@ files = [
{file = "greenlet-3.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:952256c2bc5b4ee8df8dfc54fc4de330970bf5d79253c863fb5e6761f00dda35"},
{file = "greenlet-3.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:269d06fa0f9624455ce08ae0179430eea61085e3cf6457f05982b37fd2cefe17"},
{file = "greenlet-3.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:9adbd8ecf097e34ada8efde9b6fec4dd2a903b1e98037adf72d12993a1c80b51"},
{file = "greenlet-3.0.0-cp312-universal2-macosx_10_9_universal2.whl", hash = "sha256:553d6fb2324e7f4f0899e5ad2c427a4579ed4873f42124beba763f16032959af"},
{file = "greenlet-3.0.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6b5ce7f40f0e2f8b88c28e6691ca6806814157ff05e794cdd161be928550f4c"},
{file = "greenlet-3.0.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ecf94aa539e97a8411b5ea52fc6ccd8371be9550c4041011a091eb8b3ca1d810"},
{file = "greenlet-3.0.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80dcd3c938cbcac986c5c92779db8e8ce51a89a849c135172c88ecbdc8c056b7"},
@@ -1654,13 +1655,13 @@ test = ["hatch", "ipykernel", "openapi-core (>=0.18.0,<0.19.0)", "openapi-spec-v
[[package]]
name = "langchain"
version = "0.0.319"
version = "0.0.327"
description = "Building applications with LLMs through composability"
optional = false
python-versions = ">=3.8.1,<4.0"
files = [
{file = "langchain-0.0.319-py3-none-any.whl", hash = "sha256:a61448fd418ff9478f2be3477c9c92acbf6b6acd8163c58c994a6158d4d116f8"},
{file = "langchain-0.0.319.tar.gz", hash = "sha256:4fe5025e5fd48dcf8e02107fefe173ba997af3c8960871cc4a4467e24bb89375"},
{file = "langchain-0.0.327-py3-none-any.whl", hash = "sha256:21835600e1ab11e2a939d9e473c13ed51402a3b75418ca02689877a5764da398"},
{file = "langchain-0.0.327.tar.gz", hash = "sha256:2710fba0c0735d1a63327cad83387571adc457fe75075c70335e8ea628f0a8a2"},
]
[package.dependencies]
@@ -1669,7 +1670,7 @@ anyio = "<4.0"
async-timeout = {version = ">=4.0.0,<5.0.0", markers = "python_version < \"3.11\""}
dataclasses-json = ">=0.5.7,<0.7"
jsonpatch = ">=1.33,<2.0"
langsmith = ">=0.0.43,<0.1.0"
langsmith = ">=0.0.52,<0.1.0"
numpy = ">=1,<2"
pydantic = ">=1,<3"
PyYAML = ">=5.3"
@@ -1678,14 +1679,14 @@ SQLAlchemy = ">=1.4,<3"
tenacity = ">=8.1.0,<9.0.0"
[package.extras]
all = ["O365 (>=2.0.26,<3.0.0)", "aleph-alpha-client (>=2.15.0,<3.0.0)", "amadeus (>=8.1.0)", "arxiv (>=1.4,<2.0)", "atlassian-python-api (>=3.36.0,<4.0.0)", "awadb (>=0.3.9,<0.4.0)", "azure-ai-formrecognizer (>=3.2.1,<4.0.0)", "azure-ai-vision (>=0.11.1b1,<0.12.0)", "azure-cognitiveservices-speech (>=1.28.0,<2.0.0)", "azure-cosmos (>=4.4.0b1,<5.0.0)", "azure-identity (>=1.12.0,<2.0.0)", "beautifulsoup4 (>=4,<5)", "clarifai (>=9.1.0)", "clickhouse-connect (>=0.5.14,<0.6.0)", "cohere (>=4,<5)", "deeplake (>=3.6.8,<4.0.0)", "docarray[hnswlib] (>=0.32.0,<0.33.0)", "duckduckgo-search (>=3.8.3,<4.0.0)", "elasticsearch (>=8,<9)", "esprima (>=4.0.1,<5.0.0)", "faiss-cpu (>=1,<2)", "google-api-python-client (==2.70.0)", "google-auth (>=2.18.1,<3.0.0)", "google-search-results (>=2,<3)", "gptcache (>=0.1.7)", "html2text (>=2020.1.16,<2021.0.0)", "huggingface_hub (>=0,<1)", "jinja2 (>=3,<4)", "jq (>=1.4.1,<2.0.0)", "lancedb (>=0.1,<0.2)", "langkit (>=0.0.6,<0.1.0)", "lark (>=1.1.5,<2.0.0)", "libdeeplake (>=0.0.60,<0.0.61)", "librosa (>=0.10.0.post2,<0.11.0)", "lxml (>=4.9.2,<5.0.0)", "manifest-ml (>=0.0.1,<0.0.2)", "marqo (>=1.2.4,<2.0.0)", "momento (>=1.10.1,<2.0.0)", "nebula3-python (>=3.4.0,<4.0.0)", "neo4j (>=5.8.1,<6.0.0)", "networkx (>=2.6.3,<4)", "nlpcloud (>=1,<2)", "nltk (>=3,<4)", "nomic (>=1.0.43,<2.0.0)", "openai (>=0,<1)", "openlm (>=0.0.5,<0.0.6)", "opensearch-py (>=2.0.0,<3.0.0)", "pdfminer-six (>=20221105,<20221106)", "pexpect (>=4.8.0,<5.0.0)", "pgvector (>=0.1.6,<0.2.0)", "pinecone-client (>=2,<3)", "pinecone-text (>=0.4.2,<0.5.0)", "psycopg2-binary (>=2.9.5,<3.0.0)", "pymongo (>=4.3.3,<5.0.0)", "pyowm (>=3.3.0,<4.0.0)", "pypdf (>=3.4.0,<4.0.0)", "pytesseract (>=0.3.10,<0.4.0)", "python-arango (>=7.5.9,<8.0.0)", "pyvespa (>=0.33.0,<0.34.0)", "qdrant-client (>=1.3.1,<2.0.0)", "rdflib (>=6.3.2,<7.0.0)", "redis (>=4,<5)", "requests-toolbelt (>=1.0.0,<2.0.0)", "sentence-transformers (>=2,<3)", "singlestoredb (>=0.7.1,<0.8.0)", "tensorflow-text (>=2.11.0,<3.0.0)", "tigrisdb (>=1.0.0b6,<2.0.0)", "tiktoken (>=0.3.2,<0.6.0)", "torch (>=1,<3)", "transformers (>=4,<5)", "weaviate-client (>=3,<4)", "wikipedia (>=1,<2)", "wolframalpha (==5.0.0)"]
all = ["O365 (>=2.0.26,<3.0.0)", "aleph-alpha-client (>=2.15.0,<3.0.0)", "amadeus (>=8.1.0)", "arxiv (>=1.4,<2.0)", "atlassian-python-api (>=3.36.0,<4.0.0)", "awadb (>=0.3.9,<0.4.0)", "azure-ai-formrecognizer (>=3.2.1,<4.0.0)", "azure-ai-vision (>=0.11.1b1,<0.12.0)", "azure-cognitiveservices-speech (>=1.28.0,<2.0.0)", "azure-cosmos (>=4.4.0b1,<5.0.0)", "azure-identity (>=1.12.0,<2.0.0)", "beautifulsoup4 (>=4,<5)", "clarifai (>=9.1.0)", "clickhouse-connect (>=0.5.14,<0.6.0)", "cohere (>=4,<5)", "deeplake (>=3.8.3,<4.0.0)", "docarray[hnswlib] (>=0.32.0,<0.33.0)", "duckduckgo-search (>=3.8.3,<4.0.0)", "elasticsearch (>=8,<9)", "esprima (>=4.0.1,<5.0.0)", "faiss-cpu (>=1,<2)", "google-api-python-client (==2.70.0)", "google-auth (>=2.18.1,<3.0.0)", "google-search-results (>=2,<3)", "gptcache (>=0.1.7)", "html2text (>=2020.1.16,<2021.0.0)", "huggingface_hub (>=0,<1)", "jinja2 (>=3,<4)", "jq (>=1.4.1,<2.0.0)", "lancedb (>=0.1,<0.2)", "langkit (>=0.0.6,<0.1.0)", "lark (>=1.1.5,<2.0.0)", "librosa (>=0.10.0.post2,<0.11.0)", "lxml (>=4.9.2,<5.0.0)", "manifest-ml (>=0.0.1,<0.0.2)", "marqo (>=1.2.4,<2.0.0)", "momento (>=1.10.1,<2.0.0)", "nebula3-python (>=3.4.0,<4.0.0)", "neo4j (>=5.8.1,<6.0.0)", "networkx (>=2.6.3,<4)", "nlpcloud (>=1,<2)", "nltk (>=3,<4)", "nomic (>=1.0.43,<2.0.0)", "openai (>=0,<1)", "openlm (>=0.0.5,<0.0.6)", "opensearch-py (>=2.0.0,<3.0.0)", "pdfminer-six (>=20221105,<20221106)", "pexpect (>=4.8.0,<5.0.0)", "pgvector (>=0.1.6,<0.2.0)", "pinecone-client (>=2,<3)", "pinecone-text (>=0.4.2,<0.5.0)", "psycopg2-binary (>=2.9.5,<3.0.0)", "pymongo (>=4.3.3,<5.0.0)", "pyowm (>=3.3.0,<4.0.0)", "pypdf (>=3.4.0,<4.0.0)", "pytesseract (>=0.3.10,<0.4.0)", "python-arango (>=7.5.9,<8.0.0)", "pyvespa (>=0.33.0,<0.34.0)", "qdrant-client (>=1.3.1,<2.0.0)", "rdflib (>=6.3.2,<7.0.0)", "redis (>=4,<5)", "requests-toolbelt (>=1.0.0,<2.0.0)", "sentence-transformers (>=2,<3)", "singlestoredb (>=0.7.1,<0.8.0)", "tensorflow-text (>=2.11.0,<3.0.0)", "tigrisdb (>=1.0.0b6,<2.0.0)", "tiktoken (>=0.3.2,<0.6.0)", "torch (>=1,<3)", "transformers (>=4,<5)", "weaviate-client (>=3,<4)", "wikipedia (>=1,<2)", "wolframalpha (==5.0.0)"]
azure = ["azure-ai-formrecognizer (>=3.2.1,<4.0.0)", "azure-ai-vision (>=0.11.1b1,<0.12.0)", "azure-cognitiveservices-speech (>=1.28.0,<2.0.0)", "azure-core (>=1.26.4,<2.0.0)", "azure-cosmos (>=4.4.0b1,<5.0.0)", "azure-identity (>=1.12.0,<2.0.0)", "azure-search-documents (==11.4.0b8)", "openai (>=0,<1)"]
clarifai = ["clarifai (>=9.1.0)"]
cli = ["typer (>=0.9.0,<0.10.0)"]
cohere = ["cohere (>=4,<5)"]
docarray = ["docarray[hnswlib] (>=0.32.0,<0.33.0)"]
embeddings = ["sentence-transformers (>=2,<3)"]
extended-testing = ["aiosqlite (>=0.19.0,<0.20.0)", "amazon-textract-caller (<2)", "anthropic (>=0.3.11,<0.4.0)", "arxiv (>=1.4,<2.0)", "assemblyai (>=0.17.0,<0.18.0)", "atlassian-python-api (>=3.36.0,<4.0.0)", "beautifulsoup4 (>=4,<5)", "bibtexparser (>=1.4.0,<2.0.0)", "cassio (>=0.1.0,<0.2.0)", "chardet (>=5.1.0,<6.0.0)", "dashvector (>=1.0.1,<2.0.0)", "esprima (>=4.0.1,<5.0.0)", "faiss-cpu (>=1,<2)", "feedparser (>=6.0.10,<7.0.0)", "geopandas (>=0.13.1,<0.14.0)", "gitpython (>=3.1.32,<4.0.0)", "gql (>=3.4.1,<4.0.0)", "html2text (>=2020.1.16,<2021.0.0)", "jinja2 (>=3,<4)", "jq (>=1.4.1,<2.0.0)", "lxml (>=4.9.2,<5.0.0)", "markdownify (>=0.11.6,<0.12.0)", "motor (>=3.3.1,<4.0.0)", "mwparserfromhell (>=0.6.4,<0.7.0)", "mwxml (>=0.3.3,<0.4.0)", "newspaper3k (>=0.2.8,<0.3.0)", "numexpr (>=2.8.6,<3.0.0)", "openai (>=0,<1)", "openapi-pydantic (>=0.3.2,<0.4.0)", "pandas (>=2.0.1,<3.0.0)", "pdfminer-six (>=20221105,<20221106)", "pgvector (>=0.1.6,<0.2.0)", "psychicapi (>=0.8.0,<0.9.0)", "py-trello (>=0.19.0,<0.20.0)", "pymupdf (>=1.22.3,<2.0.0)", "pypdf (>=3.4.0,<4.0.0)", "pypdfium2 (>=4.10.0,<5.0.0)", "pyspark (>=3.4.0,<4.0.0)", "rank-bm25 (>=0.2.2,<0.3.0)", "rapidfuzz (>=3.1.1,<4.0.0)", "rapidocr-onnxruntime (>=1.3.2,<2.0.0)", "requests-toolbelt (>=1.0.0,<2.0.0)", "rspace_client (>=2.5.0,<3.0.0)", "scikit-learn (>=1.2.2,<2.0.0)", "sqlite-vss (>=0.1.2,<0.2.0)", "streamlit (>=1.18.0,<2.0.0)", "sympy (>=1.12,<2.0)", "telethon (>=1.28.5,<2.0.0)", "timescale-vector (>=0.0.1,<0.0.2)", "tqdm (>=4.48.0)", "upstash-redis (>=0.15.0,<0.16.0)", "xata (>=1.0.0a7,<2.0.0)", "xmltodict (>=0.13.0,<0.14.0)"]
extended-testing = ["aiosqlite (>=0.19.0,<0.20.0)", "aleph-alpha-client (>=2.15.0,<3.0.0)", "anthropic (>=0.3.11,<0.4.0)", "arxiv (>=1.4,<2.0)", "assemblyai (>=0.17.0,<0.18.0)", "atlassian-python-api (>=3.36.0,<4.0.0)", "beautifulsoup4 (>=4,<5)", "bibtexparser (>=1.4.0,<2.0.0)", "cassio (>=0.1.0,<0.2.0)", "chardet (>=5.1.0,<6.0.0)", "dashvector (>=1.0.1,<2.0.0)", "esprima (>=4.0.1,<5.0.0)", "faiss-cpu (>=1,<2)", "feedparser (>=6.0.10,<7.0.0)", "fireworks-ai (>=0.6.0,<0.7.0)", "geopandas (>=0.13.1,<0.14.0)", "gitpython (>=3.1.32,<4.0.0)", "google-cloud-documentai (>=2.20.1,<3.0.0)", "gql (>=3.4.1,<4.0.0)", "html2text (>=2020.1.16,<2021.0.0)", "jinja2 (>=3,<4)", "jq (>=1.4.1,<2.0.0)", "jsonschema (>1)", "lxml (>=4.9.2,<5.0.0)", "markdownify (>=0.11.6,<0.12.0)", "motor (>=3.3.1,<4.0.0)", "mwparserfromhell (>=0.6.4,<0.7.0)", "mwxml (>=0.3.3,<0.4.0)", "newspaper3k (>=0.2.8,<0.3.0)", "numexpr (>=2.8.6,<3.0.0)", "openai (>=0,<1)", "openapi-pydantic (>=0.3.2,<0.4.0)", "pandas (>=2.0.1,<3.0.0)", "pdfminer-six (>=20221105,<20221106)", "pgvector (>=0.1.6,<0.2.0)", "psychicapi (>=0.8.0,<0.9.0)", "py-trello (>=0.19.0,<0.20.0)", "pymupdf (>=1.22.3,<2.0.0)", "pypdf (>=3.4.0,<4.0.0)", "pypdfium2 (>=4.10.0,<5.0.0)", "pyspark (>=3.4.0,<4.0.0)", "rank-bm25 (>=0.2.2,<0.3.0)", "rapidfuzz (>=3.1.1,<4.0.0)", "rapidocr-onnxruntime (>=1.3.2,<2.0.0)", "requests-toolbelt (>=1.0.0,<2.0.0)", "rspace_client (>=2.5.0,<3.0.0)", "scikit-learn (>=1.2.2,<2.0.0)", "sqlite-vss (>=0.1.2,<0.2.0)", "streamlit (>=1.18.0,<2.0.0)", "sympy (>=1.12,<2.0)", "telethon (>=1.28.5,<2.0.0)", "timescale-vector (>=0.0.1,<0.0.2)", "tqdm (>=4.48.0)", "upstash-redis (>=0.15.0,<0.16.0)", "xata (>=1.0.0a7,<2.0.0)", "xmltodict (>=0.13.0,<0.14.0)"]
javascript = ["esprima (>=4.0.1,<5.0.0)"]
llms = ["clarifai (>=9.1.0)", "cohere (>=4,<5)", "huggingface_hub (>=0,<1)", "manifest-ml (>=0.0.1,<0.0.2)", "nlpcloud (>=1,<2)", "openai (>=0,<1)", "openlm (>=0.0.5,<0.0.6)", "torch (>=1,<3)", "transformers (>=4,<5)"]
openai = ["openai (>=0,<1)", "tiktoken (>=0.3.2,<0.6.0)"]
@@ -1694,13 +1695,13 @@ text-helpers = ["chardet (>=5.1.0,<6.0.0)"]
[[package]]
name = "langsmith"
version = "0.0.47"
version = "0.0.54"
description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform."
optional = false
python-versions = ">=3.8.1,<4.0"
files = [
{file = "langsmith-0.0.47-py3-none-any.whl", hash = "sha256:90279d4888e4513f83703becf38a6d92b7c7d696b120872fa1fb28c5c67bfd91"},
{file = "langsmith-0.0.47.tar.gz", hash = "sha256:04b8373cbfcf7b9c28ba53174206095c50dc09ae1131a99b501f92776c8ad0c8"},
{file = "langsmith-0.0.54-py3-none-any.whl", hash = "sha256:55eca5967cadb661a49ad32aecda48a824fadef202ca384575209a9d6f823b74"},
{file = "langsmith-0.0.54.tar.gz", hash = "sha256:76c8e34b4d10ad93541107138089635829f9d60601a7f6bddf5ba582d178e521"},
]
[package.dependencies]
@@ -1734,6 +1735,16 @@ files = [
{file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"},
{file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"},
{file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"},
{file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"},
{file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"},
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"},
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"},
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"},
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"},
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"},
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"},
{file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"},
{file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"},
@@ -2438,13 +2449,13 @@ plugins = ["importlib-metadata"]
[[package]]
name = "pytest"
version = "7.4.2"
version = "7.4.3"
description = "pytest: simple powerful testing with Python"
optional = false
python-versions = ">=3.7"
files = [
{file = "pytest-7.4.2-py3-none-any.whl", hash = "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002"},
{file = "pytest-7.4.2.tar.gz", hash = "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069"},
{file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"},
{file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"},
]
[package.dependencies]
@@ -2641,6 +2652,7 @@ files = [
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
{file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
{file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
{file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
{file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
@@ -2648,8 +2660,15 @@ files = [
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
{file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
{file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
{file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
{file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
@@ -2666,6 +2685,7 @@ files = [
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
{file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"},
{file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
{file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
{file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
@@ -2673,6 +2693,7 @@ files = [
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
{file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"},
{file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
{file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
{file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
@@ -3888,4 +3909,4 @@ server = ["fastapi", "sse-starlette"]
[metadata]
lock-version = "2.0"
python-versions = "^3.8.1"
content-hash = "a02e9f4afeff9a7fd8cf972fd48e80fb576941aa35af43c653c7b0de1a88c691"
content-hash = "0cca716274936ceacbe60c6d9697ddcc6714a57245c8ae18518046a1329c05cf"
+2 -2
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langserve"
version = "0.0.15"
version = "0.0.22"
description = ""
readme = "README.md"
authors = ["LangChain"]
@@ -16,7 +16,7 @@ fastapi = {version = ">=0.90.1", optional = true}
sse-starlette = {version = "^1.3.0", optional = true}
httpx-sse = {version = ">=0.3.1", optional = true}
pydantic = "^1"
langchain = ">=0.0.316"
langchain = ">=0.0.322"
[tool.poetry.group.dev.dependencies]
jupyterlab = "^3.6.1"
+62
View File
@@ -0,0 +1,62 @@
import uuid
import pytest
from langserve.callbacks import AsyncEventAggregatorCallback, replace_uuids
@pytest.mark.asyncio
async def test_event_aggregator() -> None:
"""Test that the event aggregator is aggregating events."""
from langchain.llms import FakeListLLM
from langchain.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_template("{question}")
llm = FakeListLLM(responses=["hello", "world"])
chain = prompt | llm
callback = AsyncEventAggregatorCallback()
assert callback.callback_events == []
assert chain.invoke({"question": "hello"}, {"callbacks": [callback]}) == "hello"
callback_events = callback.callback_events
assert isinstance(callback_events, list)
assert len(callback_events) == 6
assert [event["type"] for event in callback_events] == [
"on_chain_start",
"on_chain_start",
"on_chain_end",
"on_llm_start",
"on_llm_end",
"on_chain_end",
]
def test_replace_uuids() -> None:
"""Test replace uuids in place."""
uuid1 = uuid.UUID(int=1)
uuid2 = uuid.UUID(int=2)
events = [
{
"type": "on_llm_start",
"run_id": uuid1,
"parent_run_id": None,
},
{
"type": "on_llm_start",
"run_id": uuid1,
"parent_run_id": uuid2,
},
]
new_events = replace_uuids(events)
# Assert original event is unchanged
assert events[0]["run_id"] == uuid1
assert isinstance(new_events, list)
assert len(new_events) == 2
assert new_events[0]["run_id"] != uuid1
assert new_events[1]["run_id"] != uuid2
assert new_events[0]["run_id"] == new_events[1]["run_id"]
assert new_events[0]["run_id"] != new_events[1]["parent_run_id"]
assert new_events[0]["parent_run_id"] is None
+48 -4
View File
@@ -1,3 +1,4 @@
import uuid
from typing import Any
import pytest
@@ -6,13 +7,14 @@ from langchain.schema.messages import (
HumanMessageChunk,
SystemMessage,
)
from langchain.schema.output import ChatGeneration
try:
from pydantic.v1 import BaseModel
except ImportError:
from pydantic import BaseModel
from langserve.serialization import simple_dumps, simple_loads
from langserve.serialization import WellKnownLCSerializer, load_events
@pytest.mark.parametrize(
@@ -39,17 +41,22 @@ from langserve.serialization import simple_dumps, simple_loads
"numbers": [1, 2, 3],
"boom": "Hello, world!",
},
[ChatGeneration(message=HumanMessage(content="Hello"))],
],
)
def test_serialization(data: Any) -> None:
"""There and back again! :)"""
# Test encoding
assert isinstance(simple_dumps(data), str)
lc_serializer = WellKnownLCSerializer()
assert isinstance(lc_serializer.dumps(data), str)
# Translate to python primitives and load back into object
assert lc_serializer.loadd(lc_serializer.dumpd(data)) == data
# Test simple equality (does not include pydantic class names)
assert simple_loads(simple_dumps(data)) == data
assert lc_serializer.loads(lc_serializer.dumps(data)) == data
# Test full representation equality (includes pydantic class names)
assert _get_full_representation(
simple_loads(simple_dumps(data))
lc_serializer.loads(lc_serializer.dumps(data))
) == _get_full_representation(data)
@@ -75,3 +82,40 @@ def _get_full_representation(data: Any) -> Any:
return data.schema()
else:
return data
@pytest.mark.parametrize(
"data,expected",
[
([], []),
(
[
{
"type": "on_llm_start",
"serialized": {},
"prompts": [],
"run_id": str(uuid.UUID(int=2)),
"parent_run_id": str(uuid.UUID(int=1)),
"tags": ["h"],
"metadata": {},
"kwargs": {},
}
],
[
{
"type": "on_llm_start",
"serialized": {},
"prompts": [],
"run_id": uuid.UUID(int=2),
"parent_run_id": uuid.UUID(int=1),
"tags": ["h"],
"metadata": {},
"kwargs": {},
}
],
),
],
)
def test_decode_events(data: Any, expected: Any) -> None:
"""Test decoding events."""
assert load_events(data) == expected
File diff suppressed because it is too large Load Diff
+5 -3
View File
@@ -4,7 +4,7 @@ import pytest
from langchain.prompts import PromptTemplate
from langchain.schema.runnable.utils import ConfigurableField
from langserve.server import _unpack_config
from langserve.server import _unpack_request_config
try:
from pydantic.v1 import BaseModel, ValidationError
@@ -151,7 +151,7 @@ def test_invoke_request_with_runnables() -> None:
Model = create_invoke_request_model("", runnable.input_schema, config)
assert (
_unpack_config(
_unpack_request_config(
Model(
input={"name": "bob"},
).config,
@@ -178,6 +178,8 @@ def test_invoke_request_with_runnables() -> None:
"template": "goodbye {name}",
}
assert _unpack_config(request.config, keys=["configurable"], model=config) == {
assert _unpack_request_config(
request.config, keys=["configurable"], model=config
) == {
"configurable": {"template": "goodbye {name}"},
}
+43 -1
View File
@@ -1,10 +1,13 @@
from typing import Any, List, Mapping, Optional
from typing import Any, Dict, List, Mapping, Optional
from uuid import UUID
from langchain.callbacks.manager import (
AsyncCallbackManagerForLLMRun,
CallbackManagerForLLMRun,
)
from langchain.callbacks.tracers.base import BaseTracer
from langchain.llms.base import LLM
from langsmith.schemas import Run
class FakeListLLM(LLM):
@@ -52,3 +55,42 @@ class FakeListLLM(LLM):
@property
def _identifying_params(self) -> Mapping[str, Any]:
return {"responses": self.responses}
class FakeTracer(BaseTracer):
"""Fake tracer that records LangChain execution.
It replaces run ids with deterministic UUIDs."""
def __init__(self) -> None:
"""Initialize the tracer."""
super().__init__()
self.runs: List[Run] = []
self.uuids_map: Dict[UUID, UUID] = {}
self.uuids_generator = (
UUID(f"00000000-0000-4000-8000-{i:012}", version=4) for i in range(10_000)
)
def _replace_uuid(self, uuid: UUID) -> UUID:
"""Replace a UUID with a deterministic one."""
if uuid not in self.uuids_map:
self.uuids_map[uuid] = next(self.uuids_generator)
return self.uuids_map[uuid]
def _copy_run(self, run: Run) -> Run:
"""Copy a run, replacing UUIDs."""
return run.copy(
update={
"id": self._replace_uuid(run.id),
"parent_run_id": self.uuids_map[run.parent_run_id]
if run.parent_run_id
else None,
"child_runs": [self._copy_run(child) for child in run.child_runs],
"execution_order": None,
"child_execution_order": None,
}
)
def _persist_run(self, run: Run) -> None:
"""Persist a run."""
self.runs.append(self._copy_run(run))