Skip to content

Commit

Permalink
Merge branch 'main' of github.com:langchain-ai/langserve into main
Browse files Browse the repository at this point in the history
  • Loading branch information
Rai220 committed Oct 27, 2023
2 parents 00f1eee + 14eb1a6 commit 7ab8c39
Show file tree
Hide file tree
Showing 17 changed files with 645 additions and 64 deletions.
140 changes: 140 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,3 +244,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

```
2 changes: 0 additions & 2 deletions examples/chain/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
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
Expand Down
156 changes: 156 additions & 0 deletions examples/file_processing/client.ipynb
Original file line number Diff line number Diff line change
@@ -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 you’re 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 you’re 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 you’re reading this you might be using LangServe 🦜🏓!\\n\\nThis is a sample PDF!\\n\\n\\x0c',\n",
" 'If you’re ']"
]
},
"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 added examples/file_processing/sample.pdf
Binary file not shown.
61 changes: 61 additions & 0 deletions examples/file_processing/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""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)
Loading

0 comments on commit 7ab8c39

Please sign in to comment.