mirror of
https://github.com/langchain-ai/langserve.git
synced 2026-07-25 13:15:44 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bacafee01b |
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"contributors": ["eyurtsev", "hwchase17", "nfcampos", "efriis", "jacoblee93", "dqbd", "kreneskyp", "adarsh-jha-dev", "harris", "baskaryan", "hinthornw", "bracesproul", "jakerachleff", "craigsdennis", "anhi", "169", "LarchLiu", "PaulLockett", "RCMatthias", "jwynia", "majiayu000", "mpskex", "shivachittamuru", "sinashaloudegi", "sowsan", "akira", "lucianotonet", "JGalego", "nat-n"],
|
||||
"contributors": ["eyurtsev", "hwchase17", "nfcampos", "efriis", "jacoblee93", "dqbd", "kreneskyp", "adarsh-jha-dev", "harris", "baskaryan", "hinthornw", "bracesproul", "jakerachleff", "craigsdennis", "anhi", "169", "LarchLiu", "PaulLockett", "RCMatthias", "jwynia", "majiayu000", "mpskex", "shivachittamuru", "sinashaloudegi", "sowsan", "akira", "lucianotonet", "JGalego"],
|
||||
"message": "Thank you for your pull request and welcome to our community. We require contributors to sign our Contributor License Agreement, and we don't seem to have the username {{usersWithoutCLA}} on file. In order for us to review and merge your code, please complete the Individual Contributor License Agreement here https://forms.gle/AQFbtkWRoHXUgipM6 .\n\nThis process is done manually on our side, so after signing the form one of the maintainers will add you to the contributors list.\n\nFor more details about why we have a CLA and other contribution guidelines please see: https://github.com/langchain-ai/langserve/blob/main/CONTRIBUTING.md."
|
||||
}
|
||||
|
||||
@@ -20,8 +20,8 @@ uses [pydantic](https://docs.pydantic.dev/latest/) for data validation.
|
||||
|
||||
In addition, it provides a client that can be used to call into runnables deployed on a
|
||||
server.
|
||||
A JavaScript client is available
|
||||
in [LangChain.js](https://js.langchain.com/docs/ecosystem/langserve).
|
||||
A javascript client is available
|
||||
in [LangChainJS](https://api.js.langchain.com/classes/langchain_runnables_remote.RemoteRunnable.html).
|
||||
|
||||
## Features
|
||||
|
||||
@@ -53,7 +53,7 @@ in [LangChain.js](https://js.langchain.com/docs/ecosystem/langserve).
|
||||
|
||||
We will be releasing a hosted version of LangServe for one-click deployments of
|
||||
LangChain
|
||||
applications. [Sign up here](https://airtable.com/apppQ9p5XuujRl3wJ/shrABpHWdxry8Bacm)
|
||||
applications. [Sign up here](https://airtable.com/app0hN6sd93QcKubv/shrAjst60xXa6quV2)
|
||||
to get on the waitlist.
|
||||
|
||||
## Security
|
||||
@@ -84,7 +84,7 @@ installed. You can install it with `pip install -U langchain-cli`.
|
||||
langchain app new ../path/to/directory
|
||||
```
|
||||
|
||||
## Examples
|
||||
## <a name="examples"></a> Examples & Templates
|
||||
|
||||
Get your LangServe instance started quickly with
|
||||
[LangChain Templates](https://github.com/langchain-ai/langchain/blob/master/templates/README.md).
|
||||
@@ -111,7 +111,7 @@ directory.
|
||||
| **Auth** with `add_routes`: Simple authentication mechanism based on path dependencies. (No useful on its own for implementing per user logic.) | [server](https://github.com/langchain-ai/langserve/tree/main/examples/auth/path_dependencies/server.py) |
|
||||
| **Auth** with `add_routes`: Implement per user logic and auth for endpoints that use per request config modifier. (**Note**: At the moment, does not integrate with OpenAPI docs.) | [server](https://github.com/langchain-ai/langserve/tree/main/examples/auth/per_req_config_modifier/server.py), [client](https://github.com/langchain-ai/langserve/tree/main/examples/auth/per_req_config_modifier/client.ipynb) |
|
||||
| **Auth** with `APIHandler`: Implement per user logic and auth that shows how to search only within user owned documents. | [server](https://github.com/langchain-ai/langserve/tree/main/examples/auth/api_handler/server.py), [client](https://github.com/langchain-ai/langserve/tree/main/examples/auth/api_handler/client.ipynb) |
|
||||
| **Widgets** Different widgets that can be used with playground (file upload and chat) | [server](https://github.com/langchain-ai/langserve/tree/main/examples/widgets/chat/tuples/server.py) |
|
||||
| **Widgets** Different widgets that can be used with playground (file upload and chat) | [server](https://github.com/langchain-ai/langserve/tree/main/examples/widgets/server.py) |
|
||||
| **Widgets** File upload widget used for LangServe playground. | [server](https://github.com/langchain-ai/langserve/tree/main/examples/file_processing/server.py), [client](https://github.com/langchain-ai/langserve/tree/main/examples/file_processing/client.ipynb) |
|
||||
|
||||
## Sample Application
|
||||
@@ -161,23 +161,6 @@ if __name__ == "__main__":
|
||||
uvicorn.run(app, host="localhost", port=8000)
|
||||
```
|
||||
|
||||
If you intend to call your endpoint from the browser, you will also need to set CORS headers.
|
||||
You can use FastAPI's built-in middleware for that:
|
||||
|
||||
```python
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
# Set all CORS enabled origins
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
expose_headers=["*"],
|
||||
)
|
||||
```
|
||||
|
||||
### Docs
|
||||
|
||||
If you've deployed the server above, you can view the generated OpenAPI docs using:
|
||||
@@ -240,7 +223,7 @@ chain.batch([{"topic": "parrots"}, {"topic": "cats"}])
|
||||
In TypeScript (requires LangChain.js version 0.0.166 or later):
|
||||
|
||||
```typescript
|
||||
import { RemoteRunnable } from "@langchain/core/runnables/remote";
|
||||
import {RemoteRunnable} from "langchain/runnables/remote";
|
||||
|
||||
const chain = new RemoteRunnable({
|
||||
url: `http://localhost:8000/joke/`,
|
||||
@@ -549,7 +532,7 @@ Here are a few examples:
|
||||
|
||||
| Description | Links |
|
||||
|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **Widgets** Different widgets that can be used with playground (file upload and chat) | [server](https://github.com/langchain-ai/langserve/tree/main/examples/widgets/chat/tuples/server.py), [client](https://github.com/langchain-ai/langserve/tree/main/examples/widgets/client.ipynb) |
|
||||
| **Widgets** Different widgets that can be used with playground (file upload and chat) | [server](https://github.com/langchain-ai/langserve/tree/main/examples/widgets/server.py), [client](https://github.com/langchain-ai/langserve/tree/main/examples/widgets/client.ipynb) |
|
||||
| **Widgets** File upload widget used for LangServe playground. | [server](https://github.com/langchain-ai/langserve/tree/main/examples/file_processing/server.py), [client](https://github.com/langchain-ai/langserve/tree/main/examples/file_processing/client.ipynb) |
|
||||
|
||||
#### Schema
|
||||
@@ -621,7 +604,7 @@ Example widget:
|
||||
### Chat Widget
|
||||
|
||||
Look
|
||||
at the [widget example](https://github.com/langchain-ai/langserve/tree/main/examples/widgets/chat/tuples/server.py).
|
||||
at [widget example](https://github.com/langchain-ai/langserve/tree/main/examples/widgets/server.py).
|
||||
|
||||
To define a chat widget, make sure that you pass "type": "chat".
|
||||
|
||||
@@ -633,6 +616,7 @@ To define a chat widget, make sure that you pass "type": "chat".
|
||||
Here's a snippet:
|
||||
|
||||
```python
|
||||
|
||||
class ChatHistory(CustomUserType):
|
||||
chat_history: List[Tuple[str, str]] = Field(
|
||||
...,
|
||||
@@ -671,53 +655,21 @@ Example widget:
|
||||
<img src="https://github.com/langchain-ai/langserve/assets/3205522/a71ff37b-a6a9-4857-a376-cf27c41d3ca4" width="50%"/>
|
||||
</p>
|
||||
|
||||
You can also specify a list of messages as your a parameter directly, as shown in this snippet:
|
||||
|
||||
```python
|
||||
prompt = ChatPromptTemplate.from_messages(
|
||||
[
|
||||
("system", "You are a helpful assisstant named Cob."),
|
||||
MessagesPlaceholder(variable_name="messages"),
|
||||
]
|
||||
)
|
||||
|
||||
chain = prompt | ChatAnthropic(model="claude-2")
|
||||
|
||||
|
||||
class MessageListInput(BaseModel):
|
||||
"""Input for the chat endpoint."""
|
||||
messages: List[Union[HumanMessage, AIMessage]] = Field(
|
||||
...,
|
||||
description="The chat messages representing the current conversation.",
|
||||
extra={"widget": {"type": "chat", "input": "messages"}},
|
||||
)
|
||||
|
||||
|
||||
add_routes(
|
||||
app,
|
||||
chain.with_types(input_type=MessageListInput),
|
||||
path="/chat",
|
||||
)
|
||||
```
|
||||
|
||||
See [this sample file](https://github.com/langchain-ai/langserve/tree/main/examples/widgets/chat/message_list/server.py) for an example.
|
||||
|
||||
### Enabling / Disabling Endpoints (LangServe >=0.0.33)
|
||||
|
||||
You can enable / disable which endpoints are exposed when adding routes for a given chain.
|
||||
|
||||
Use `enabled_endpoints` if you want to make sure to never get a new endpoint when upgrading langserve to a newer
|
||||
You can enable / disable which endpoints are exposed. Use `enabled_endpoints` if you
|
||||
want to make sure to never get a new endpoint when upgrading langserve to a newer
|
||||
verison.
|
||||
|
||||
Enable: The code below will only enable `invoke`, `batch` and the
|
||||
corresponding `config_hash` endpoint variants.
|
||||
|
||||
```python
|
||||
add_routes(app, chain, enabled_endpoints=["invoke", "batch", "config_hashes"], path="/mychain")
|
||||
add_routes(app, chain, enabled_endpoints=["invoke", "batch", "config_hashes"])
|
||||
```
|
||||
|
||||
Disable: The code below will disable the playground for the chain
|
||||
|
||||
```python
|
||||
add_routes(app, chain, disabled_endpoints=["playground"], path="/mychain")
|
||||
add_routes(app, chain, disabled_endpoints=["playground"])
|
||||
```
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
"""Client server that interacts with the main server via a remote runnable.
|
||||
|
||||
This server sets up a simple proxy to the main server. It uses the RemoteRunnable
|
||||
to interact with the main server. The main server is expected to be running at
|
||||
http://localhost:8123.
|
||||
|
||||
A client server will likely end up doing something more clever rather than
|
||||
just being a proxy.
|
||||
"""
|
||||
from fastapi import FastAPI
|
||||
|
||||
from langserve import RemoteRunnable, add_routes
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
MAIN_SERVER_URL = (
|
||||
"http://localhost:8123/chat_model/" # <-- URL of the RUNNABLE on the main server
|
||||
)
|
||||
# Type inference is not automatic for remote runnables at the moment,
|
||||
# so you must specify which types are used for the playground to work.
|
||||
remote_runnable = RemoteRunnable(MAIN_SERVER_URL).with_types(input_type=str)
|
||||
|
||||
|
||||
# Let's add an example chain
|
||||
add_routes(
|
||||
app,
|
||||
remote_runnable,
|
||||
path="/proxied",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(app)
|
||||
@@ -1,20 +0,0 @@
|
||||
"""Main server that exposes one or more chains as HTTP endpoints."""
|
||||
from fastapi import FastAPI
|
||||
from langchain_openai import ChatOpenAI
|
||||
|
||||
from langserve import add_routes
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# Let's add an example chain
|
||||
add_routes(
|
||||
app,
|
||||
ChatOpenAI(),
|
||||
path="/chat_model",
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
# Running on port 8123
|
||||
uvicorn.run(app, port=8123)
|
||||
@@ -1,38 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
"""Example LangChain Server that uses a Fast API Router.
|
||||
|
||||
When applications grow, it becomes useful to use FastAPI's Router to organize
|
||||
the routes.
|
||||
|
||||
See more documentation at:
|
||||
|
||||
https://fastapi.tiangolo.com/tutorial/bigger-applications/
|
||||
"""
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from langchain.chat_models import ChatAnthropic, ChatOpenAI
|
||||
|
||||
from langserve import add_routes
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
router = APIRouter(prefix="/models")
|
||||
|
||||
# Invocations to this router will appear in trace logs as /models/openai
|
||||
add_routes(
|
||||
router,
|
||||
ChatOpenAI(),
|
||||
path="/openai",
|
||||
)
|
||||
# Invocations to this router will appear in trace logs as /models/anthropic
|
||||
add_routes(
|
||||
router,
|
||||
ChatAnthropic(),
|
||||
path="/anthropic",
|
||||
)
|
||||
|
||||
app.include_router(router)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(app, host="localhost", port=8000)
|
||||
@@ -1,64 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
"""Example of a simple chatbot that just passes current conversation
|
||||
state back and forth between server and client.
|
||||
"""
|
||||
from typing import List, Union
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from langchain.chat_models import ChatAnthropic
|
||||
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
|
||||
from langchain_core.output_parsers import StrOutputParser
|
||||
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||
|
||||
from langserve import add_routes
|
||||
from langserve.pydantic_v1 import BaseModel, Field
|
||||
|
||||
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=["*"],
|
||||
)
|
||||
|
||||
|
||||
# Declare a chain
|
||||
prompt = ChatPromptTemplate.from_messages(
|
||||
[
|
||||
("system", "You are a helpful assisstant named Cob."),
|
||||
MessagesPlaceholder(variable_name="messages"),
|
||||
]
|
||||
)
|
||||
|
||||
chain = prompt | ChatAnthropic(model="claude-2") | StrOutputParser()
|
||||
|
||||
|
||||
class InputChat(BaseModel):
|
||||
"""Input for the chat endpoint."""
|
||||
|
||||
messages: List[Union[HumanMessage, AIMessage, SystemMessage]] = Field(
|
||||
...,
|
||||
description="The chat messages representing the current conversation.",
|
||||
extra={"widget": {"type": "chat", "input": "messages"}},
|
||||
)
|
||||
|
||||
|
||||
add_routes(
|
||||
app,
|
||||
chain.with_types(input_type=InputChat),
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(app, host="localhost", port=8000)
|
||||
+15
-23
@@ -171,7 +171,7 @@ async def _unpack_request_config(
|
||||
|
||||
|
||||
def _update_config_with_defaults(
|
||||
run_name: str,
|
||||
path: str,
|
||||
incoming_config: RunnableConfig,
|
||||
request: Request,
|
||||
*,
|
||||
@@ -209,7 +209,7 @@ def _update_config_with_defaults(
|
||||
metadata.update(hosted_metadata)
|
||||
|
||||
non_overridable_default_config = RunnableConfig(
|
||||
run_name=run_name,
|
||||
run_name=path,
|
||||
metadata=metadata,
|
||||
)
|
||||
|
||||
@@ -534,16 +534,8 @@ class APIHandler:
|
||||
|
||||
self._config_keys = config_keys
|
||||
|
||||
self._path = path.rstrip("/")
|
||||
self._base_url = prefix + self._path
|
||||
# Setting the run name explicitly
|
||||
# the run name is set to the base url, which takes into account
|
||||
# the prefix (e.g., if there's an APIRouter used) and the path relative
|
||||
# to the router.
|
||||
# If the base path is /foo/bar, the run name will be /foo/bar
|
||||
# and when tracing information is logged, we'll be able to see
|
||||
# traces for the path /foo/bar.
|
||||
self._run_name = self._base_url
|
||||
self._path = path
|
||||
self._base_url = prefix + path
|
||||
self._include_callback_events = include_callback_events
|
||||
self._per_req_config_modifier = per_req_config_modifier
|
||||
self._serializer = WellKnownLCSerializer()
|
||||
@@ -671,7 +663,7 @@ class APIHandler:
|
||||
server_config=server_config,
|
||||
)
|
||||
config = _update_config_with_defaults(
|
||||
self._run_name,
|
||||
self._path,
|
||||
user_provided_config,
|
||||
request,
|
||||
endpoint=endpoint,
|
||||
@@ -822,7 +814,7 @@ class APIHandler:
|
||||
_add_callbacks(config_, [aggregator])
|
||||
final_configs.append(
|
||||
_update_config_with_defaults(
|
||||
self._run_name, config_, request, endpoint="batch"
|
||||
self._path, config_, request, endpoint="batch"
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1244,7 +1236,7 @@ class APIHandler:
|
||||
server_config=server_config,
|
||||
)
|
||||
config = _update_config_with_defaults(
|
||||
self._run_name, user_provided_config, request
|
||||
self._path, user_provided_config, request
|
||||
)
|
||||
|
||||
return self._runnable.get_input_schema(config).schema()
|
||||
@@ -1272,7 +1264,7 @@ class APIHandler:
|
||||
server_config=server_config,
|
||||
)
|
||||
config = _update_config_with_defaults(
|
||||
self._run_name, user_provided_config, request
|
||||
self._path, user_provided_config, request
|
||||
)
|
||||
return self._runnable.get_output_schema(config).schema()
|
||||
|
||||
@@ -1299,7 +1291,7 @@ class APIHandler:
|
||||
server_config=server_config,
|
||||
)
|
||||
config = _update_config_with_defaults(
|
||||
self._run_name, user_provided_config, request
|
||||
self._path, user_provided_config, request
|
||||
)
|
||||
return (
|
||||
self._runnable.with_config(config)
|
||||
@@ -1332,16 +1324,16 @@ class APIHandler:
|
||||
)
|
||||
|
||||
config = _update_config_with_defaults(
|
||||
self._run_name, user_provided_config, request
|
||||
self._path, user_provided_config, request
|
||||
)
|
||||
|
||||
playground_url = (
|
||||
request.scope.get("root_path", "").rstrip("/")
|
||||
+ self._base_url
|
||||
+ "/playground"
|
||||
)
|
||||
feedback_enabled = tracing_is_enabled() and self._enable_feedback_endpoint
|
||||
|
||||
if self._base_url.endswith("/"):
|
||||
playground_url = self._base_url + "playground"
|
||||
else:
|
||||
playground_url = self._base_url + "/playground"
|
||||
|
||||
return await serve_playground(
|
||||
self._runnable.with_config(config),
|
||||
self._runnable.with_config(config).input_schema,
|
||||
|
||||
+51
-51
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -5,7 +5,7 @@
|
||||
<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-a7c0cdaa.js"></script>
|
||||
<script type="module" crossorigin src="/____LANGSERVE_BASE_URL/assets/index-fbe3df33.js"></script>
|
||||
<link rel="stylesheet" href="/____LANGSERVE_BASE_URL/assets/index-52e8ab2f.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"packageManager": "yarn@1.22.19",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
|
||||
@@ -1,189 +0,0 @@
|
||||
import * as ToggleGroup from "@radix-ui/react-toggle-group";
|
||||
import { AutosizeTextarea } from "./AutosizeTextarea";
|
||||
import TrashIcon from "../assets/TrashIcon.svg?react";
|
||||
import CodeIcon from "../assets/CodeIcon.svg?react";
|
||||
import ChatIcon from "../assets/ChatIcon.svg?react";
|
||||
|
||||
export interface MessageFields {
|
||||
content: string;
|
||||
additional_kwargs?: { [key: string]: unknown };
|
||||
name?: string;
|
||||
type?: string;
|
||||
role?: string;
|
||||
}
|
||||
|
||||
export interface ChatMessageInputArgs {
|
||||
handleChange: (field: string, value: any) => void;
|
||||
handleRemoval: () => void;
|
||||
path: string;
|
||||
message: MessageFields;
|
||||
};
|
||||
|
||||
function isOpenAiFunctionCall(
|
||||
x: unknown
|
||||
): x is { name: string; arguments: string } {
|
||||
if (typeof x !== "object" || x == null) return false;
|
||||
if (!("name" in x) || typeof x.name !== "string") return false;
|
||||
if (!("arguments" in x) || typeof x.arguments !== "string") return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export const ChatMessageInput = (props: ChatMessageInputArgs) => {
|
||||
const { message } = props;
|
||||
const isAiFunctionCall = isOpenAiFunctionCall(
|
||||
message.additional_kwargs?.function_call
|
||||
);
|
||||
const type = message.type ?? "chat"
|
||||
return (
|
||||
<div className="control group">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<select
|
||||
className="-ml-1 min-w-[100px]"
|
||||
value={type}
|
||||
onChange={(e) => {
|
||||
props.handleChange(
|
||||
"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>
|
||||
<div className="flex items-center gap-2">
|
||||
{message.type === "ai" && (
|
||||
<ToggleGroup.Root
|
||||
type="single"
|
||||
aria-label="Message Type"
|
||||
className="opacity-0 transition-opacity group-focus-within:opacity-100 group-hover:opacity-100"
|
||||
value={isAiFunctionCall ? "function" : "text"}
|
||||
onValueChange={(value) => {
|
||||
switch (value) {
|
||||
case "function": {
|
||||
props.handleChange(
|
||||
"additional_kwargs",
|
||||
{
|
||||
function_call: {
|
||||
name: "",
|
||||
arguments: "{}",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
case "text": {
|
||||
props.handleChange(
|
||||
"additional_kwargs",
|
||||
{}
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ToggleGroup.Item
|
||||
className="rounded-s border border-divider-700 px-2.5 py-1 data-[state=on]:bg-divider-500/50"
|
||||
value="text"
|
||||
aria-label="Text message"
|
||||
>
|
||||
<ChatIcon className="w-4 h-4" />
|
||||
</ToggleGroup.Item>
|
||||
<ToggleGroup.Item
|
||||
className="rounded-e border border-l-0 border-divider-700 px-2.5 py-1 data-[state=on]:bg-divider-500/50"
|
||||
value="function"
|
||||
aria-label="Function call"
|
||||
>
|
||||
<CodeIcon className="w-4 h-4" />
|
||||
</ToggleGroup.Item>
|
||||
</ToggleGroup.Root>
|
||||
)}
|
||||
|
||||
<button
|
||||
className="p-1 border rounded opacity-0 transition-opacity border-divider-700 group-focus-within:opacity-100 group-hover:opacity-100"
|
||||
onClick={props.handleRemoval}
|
||||
>
|
||||
<TrashIcon className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{type === "chat" && (
|
||||
<input
|
||||
className="mb-1"
|
||||
placeholder="Role"
|
||||
value={message.role ?? ""}
|
||||
onChange={(e) => {
|
||||
props.handleChange(
|
||||
"role",
|
||||
e.target.value
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{type === "function" && (
|
||||
<input
|
||||
className="mb-1"
|
||||
placeholder="Function Name"
|
||||
value={message.name ?? ""}
|
||||
onChange={(e) => {
|
||||
props.handleChange(
|
||||
"name",
|
||||
e.target.value
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{type === "ai" &&
|
||||
isOpenAiFunctionCall(
|
||||
message.additional_kwargs?.function_call
|
||||
) ? (
|
||||
<>
|
||||
<input
|
||||
className="mb-1"
|
||||
placeholder="Function Name"
|
||||
value={
|
||||
message.additional_kwargs?.function_call.name ?? ""
|
||||
}
|
||||
onChange={(e) => {
|
||||
props.handleChange(
|
||||
"additional_kwargs.function_call.name",
|
||||
e.target.value
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<AutosizeTextarea
|
||||
value={
|
||||
message.additional_kwargs?.function_call?.arguments ??
|
||||
""
|
||||
}
|
||||
onChange={(content) => {
|
||||
props.handleChange(
|
||||
"additional_kwargs.function_call.arguments",
|
||||
content
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<AutosizeTextarea
|
||||
value={message.content}
|
||||
onChange={(content) => {
|
||||
props.handleChange(
|
||||
"content",
|
||||
content
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
import { withJsonFormsControlProps } from "@jsonforms/react";
|
||||
import PlusIcon from "../assets/PlusIcon.svg?react";
|
||||
import TrashIcon from "../assets/TrashIcon.svg?react";
|
||||
import CodeIcon from "../assets/CodeIcon.svg?react";
|
||||
import ChatIcon from "../assets/ChatIcon.svg?react";
|
||||
import {
|
||||
rankWith,
|
||||
and,
|
||||
@@ -7,10 +10,11 @@ import {
|
||||
Paths,
|
||||
isControl,
|
||||
} from "@jsonforms/core";
|
||||
import { AutosizeTextarea } from "./AutosizeTextarea";
|
||||
import { useStreamCallback } from "../useStreamCallback";
|
||||
import { getNormalizedJsonPath, traverseNaiveJsonPath } from "../utils/path";
|
||||
import { isJsonSchemaExtra } from "../utils/schema";
|
||||
import { MessageFields, ChatMessageInput } from "./ChatMessageInput";
|
||||
import { useEffect } from "react";
|
||||
import * as ToggleGroup from "@radix-ui/react-toggle-group";
|
||||
|
||||
export const chatMessagesTester = rankWith(
|
||||
12,
|
||||
@@ -57,48 +61,98 @@ export const chatMessagesTester = rankWith(
|
||||
)
|
||||
);
|
||||
|
||||
interface MessageFields {
|
||||
content: string;
|
||||
additional_kwargs?: { [key: string]: unknown };
|
||||
name?: string;
|
||||
type?: string;
|
||||
|
||||
role?: string;
|
||||
}
|
||||
|
||||
function isMessageFields(x: unknown): x is MessageFields {
|
||||
if (typeof x !== "object" || x == null) return false;
|
||||
if (!("content" in x) || typeof x.content !== "string") return false;
|
||||
if (
|
||||
"additional_kwargs" in x &&
|
||||
typeof x.additional_kwargs !== "object" &&
|
||||
x.additional_kwargs != null
|
||||
)
|
||||
return false;
|
||||
if ("name" in x && typeof x.name !== "string" && x.name != null) return false;
|
||||
if ("type" in x && typeof x.type !== "string" && x.type != null) return false;
|
||||
if ("role" in x && typeof x.role !== "string" && x.role != null) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function constructMessage(
|
||||
x: unknown,
|
||||
assumedRole: string
|
||||
): Array<MessageFields> | null {
|
||||
if (typeof x === "string") {
|
||||
return [{ content: x, type: assumedRole }];
|
||||
}
|
||||
|
||||
if (isMessageFields(x)) {
|
||||
return [x];
|
||||
}
|
||||
|
||||
if (Array.isArray(x) && x.every(isMessageFields)) {
|
||||
return x;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function isOpenAiFunctionCall(
|
||||
x: unknown
|
||||
): x is { name: string; arguments: string } {
|
||||
if (typeof x !== "object" || x == null) return false;
|
||||
if (!("name" in x) || typeof x.name !== "string") return false;
|
||||
if (!("arguments" in x) || typeof x.arguments !== "string") return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export const ChatMessagesControlRenderer = withJsonFormsControlProps(
|
||||
(props) => {
|
||||
const data: Array<MessageFields> = props.data ?? [];
|
||||
|
||||
useEffect(() => {
|
||||
useStreamCallback("onSuccess", (ctx) => {
|
||||
if (!isJsonSchemaExtra(props.schema)) return;
|
||||
if (props.schema.extra.widget.type !== "chat") return;
|
||||
setTimeout(() => props.handleChange(props.path, [
|
||||
...data,
|
||||
{ content: "", type: "human" },
|
||||
]), 10);
|
||||
}, []);
|
||||
const widget = props.schema.extra.widget;
|
||||
if (!("input" in widget) && !("output" in widget)) return;
|
||||
|
||||
useStreamCallback("onStart", () => {
|
||||
if (!isJsonSchemaExtra(props.schema)) return;
|
||||
if (props.schema.extra.widget.type !== "chat") return;
|
||||
props.handleChange(props.path, [...data, { content: "", type: "ai" }]);
|
||||
});
|
||||
const inputPath = getNormalizedJsonPath(widget.input ?? "");
|
||||
const outputPath = getNormalizedJsonPath(widget.output ?? "");
|
||||
|
||||
useStreamCallback("onChunk", (_chunk, aggregatedState) => {
|
||||
if (!isJsonSchemaExtra(props.schema)) return;
|
||||
if (props.schema.extra.widget.type !== "chat") return;
|
||||
if (aggregatedState?.final_output !== undefined) {
|
||||
const msgPath = Paths.compose(props.path, `${data.length - 1}`);
|
||||
if ((aggregatedState.final_output as MessageFields)?.type === "AIMessageChunk") {
|
||||
props.handleChange(
|
||||
Paths.compose(msgPath, "content"),
|
||||
(aggregatedState.final_output as MessageFields)?.content
|
||||
);
|
||||
} else if (typeof aggregatedState.final_output === "string") {
|
||||
props.handleChange(
|
||||
Paths.compose(msgPath, "content"),
|
||||
aggregatedState.final_output
|
||||
);
|
||||
}
|
||||
const human = traverseNaiveJsonPath(ctx.input, inputPath);
|
||||
let ai = traverseNaiveJsonPath(ctx.output, outputPath);
|
||||
|
||||
const isSingleOutputKey =
|
||||
ctx.output != null &&
|
||||
Object.keys(ctx.output).length === 1 &&
|
||||
Object.keys(ctx.output)[0] === "output";
|
||||
|
||||
if (isSingleOutputKey) {
|
||||
ai = traverseNaiveJsonPath(ai, ["output", ...outputPath]) ?? ai;
|
||||
}
|
||||
});
|
||||
|
||||
useStreamCallback("onSuccess", () => {
|
||||
if (!isJsonSchemaExtra(props.schema)) return;
|
||||
if (props.schema.extra.widget.type !== "chat") return;
|
||||
props.handleChange(props.path, [...data, { content: "", type: "human" }]);
|
||||
const humanMsg = constructMessage(human, "human");
|
||||
const aiMsg = constructMessage(ai, "ai");
|
||||
|
||||
let newMessages = undefined;
|
||||
if (humanMsg != null) {
|
||||
newMessages ??= [...data];
|
||||
newMessages.push(...humanMsg);
|
||||
}
|
||||
if (aiMsg != null) {
|
||||
newMessages ??= [...data];
|
||||
newMessages.push(...aiMsg);
|
||||
}
|
||||
|
||||
if (newMessages != null) {
|
||||
props.handleChange(props.path, newMessages);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -124,25 +178,174 @@ export const ChatMessagesControlRenderer = withJsonFormsControlProps(
|
||||
<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";
|
||||
|
||||
const handleChatMessageChange = (field: string, value: any) => {
|
||||
props.handleChange(Paths.compose(msgPath, field), value);
|
||||
};
|
||||
const isAiFunctionCall = isOpenAiFunctionCall(
|
||||
message.additional_kwargs?.function_call
|
||||
);
|
||||
|
||||
const handleChatMessageRemoval = () => {
|
||||
props.handleChange(
|
||||
props.path,
|
||||
data.filter((_, i) => i !== index)
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ChatMessageInput
|
||||
message={message}
|
||||
handleChange={handleChatMessageChange}
|
||||
handleRemoval={handleChatMessageRemoval}
|
||||
path={props.path}
|
||||
key={index}
|
||||
></ChatMessageInput>
|
||||
<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>
|
||||
<div className="flex items-center gap-2">
|
||||
{message.type === "ai" && (
|
||||
<ToggleGroup.Root
|
||||
type="single"
|
||||
aria-label="Message Type"
|
||||
className="opacity-0 transition-opacity group-focus-within:opacity-100 group-hover:opacity-100"
|
||||
value={isAiFunctionCall ? "function" : "text"}
|
||||
onValueChange={(value) => {
|
||||
switch (value) {
|
||||
case "function": {
|
||||
props.handleChange(
|
||||
Paths.compose(msgPath, "additional_kwargs"),
|
||||
{
|
||||
function_call: {
|
||||
name: "",
|
||||
arguments: "{}",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
case "text": {
|
||||
props.handleChange(
|
||||
Paths.compose(msgPath, "additional_kwargs"),
|
||||
{}
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ToggleGroup.Item
|
||||
className="rounded-s border border-divider-700 px-2.5 py-1 data-[state=on]:bg-divider-500/50"
|
||||
value="text"
|
||||
aria-label="Text message"
|
||||
>
|
||||
<ChatIcon className="w-4 h-4" />
|
||||
</ToggleGroup.Item>
|
||||
<ToggleGroup.Item
|
||||
className="rounded-e border border-l-0 border-divider-700 px-2.5 py-1 data-[state=on]:bg-divider-500/50"
|
||||
value="function"
|
||||
aria-label="Function call"
|
||||
>
|
||||
<CodeIcon className="w-4 h-4" />
|
||||
</ToggleGroup.Item>
|
||||
</ToggleGroup.Root>
|
||||
)}
|
||||
|
||||
<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>
|
||||
</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
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{type === "ai" &&
|
||||
isOpenAiFunctionCall(
|
||||
message.additional_kwargs?.function_call
|
||||
) ? (
|
||||
<>
|
||||
<input
|
||||
className="mb-1"
|
||||
placeholder="Function Name"
|
||||
value={
|
||||
message.additional_kwargs?.function_call.name ?? ""
|
||||
}
|
||||
onChange={(e) => {
|
||||
props.handleChange(
|
||||
Paths.compose(
|
||||
msgPath,
|
||||
"additional_kwargs.function_call.name"
|
||||
),
|
||||
e.target.value
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<AutosizeTextarea
|
||||
value={
|
||||
message.additional_kwargs?.function_call?.arguments ??
|
||||
""
|
||||
}
|
||||
onChange={(content) => {
|
||||
props.handleChange(
|
||||
Paths.compose(
|
||||
msgPath,
|
||||
"additional_kwargs.function_call.arguments"
|
||||
),
|
||||
content
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<AutosizeTextarea
|
||||
value={message.content}
|
||||
onChange={(content) => {
|
||||
props.handleChange(
|
||||
Paths.compose(msgPath, "content"),
|
||||
content
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import type { Operation } from "fast-json-patch";
|
||||
import type { RunState } from "./useStreamLog";
|
||||
|
||||
export interface StreamCallback {
|
||||
onSuccess?: (ctx: { input: unknown; output: unknown }) => void;
|
||||
onChunk?: (chunk: { ops?: Operation[] }, aggregatedState: RunState | null) => void;
|
||||
onError?: () => void;
|
||||
onStart?: (ctx: { input: unknown }) => void;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import { StreamCallback } from "./types";
|
||||
|
||||
export const AppCallbackContext = createContext<MutableRefObject<{
|
||||
onStart: Exclude<StreamCallback["onStart"], undefined>[];
|
||||
onChunk: Exclude<StreamCallback["onChunk"], undefined>[];
|
||||
onSuccess: Exclude<StreamCallback["onSuccess"], undefined>[];
|
||||
onError: Exclude<StreamCallback["onError"], undefined>[];
|
||||
}> | null>(null);
|
||||
@@ -19,10 +18,9 @@ export function useAppStreamCallbacks() {
|
||||
// callbacks handling
|
||||
const context = useRef<{
|
||||
onStart: Exclude<StreamCallback["onStart"], undefined>[];
|
||||
onChunk: Exclude<StreamCallback["onChunk"], undefined>[];
|
||||
onSuccess: Exclude<StreamCallback["onSuccess"], undefined>[];
|
||||
onError: Exclude<StreamCallback["onError"], undefined>[];
|
||||
}>({ onStart: [], onChunk: [], onSuccess: [], onError: [] });
|
||||
}>({ onStart: [], onSuccess: [], onError: [] });
|
||||
|
||||
const callbacks: StreamCallback = {
|
||||
onStart(...args) {
|
||||
@@ -30,11 +28,6 @@ export function useAppStreamCallbacks() {
|
||||
callback(...args);
|
||||
}
|
||||
},
|
||||
onChunk(...args) {
|
||||
for (const callback of context.current.onChunk) {
|
||||
callback(...args);
|
||||
}
|
||||
},
|
||||
onSuccess(...args) {
|
||||
for (const callback of context.current.onSuccess) {
|
||||
callback(...args);
|
||||
@@ -51,7 +44,7 @@ export function useAppStreamCallbacks() {
|
||||
}
|
||||
|
||||
export function useStreamCallback<
|
||||
Type extends "onStart" | "onChunk" | "onSuccess" | "onError"
|
||||
Type extends "onStart" | "onSuccess" | "onError"
|
||||
>(type: Type, callback: Exclude<StreamCallback[Type], undefined>) {
|
||||
type CallbackType = Exclude<StreamCallback[Type], undefined>;
|
||||
|
||||
|
||||
@@ -52,9 +52,6 @@ export function useStreamLog(callbacks: StreamCallback = {}) {
|
||||
const startRef = useRef(callbacks.onStart);
|
||||
startRef.current = callbacks.onStart;
|
||||
|
||||
const chunkRef = useRef(callbacks.onChunk);
|
||||
chunkRef.current = callbacks.onChunk;
|
||||
|
||||
const successRef = useRef(callbacks.onSuccess);
|
||||
successRef.current = callbacks.onSuccess;
|
||||
|
||||
@@ -77,7 +74,6 @@ export function useStreamLog(callbacks: StreamCallback = {}) {
|
||||
if (msg.event === "data") {
|
||||
innerLatest = reducer(innerLatest, JSON.parse(msg.data)?.ops);
|
||||
setLatest(innerLatest);
|
||||
chunkRef.current?.(JSON.parse(msg.data), innerLatest);
|
||||
}
|
||||
},
|
||||
openWhenHidden: true,
|
||||
|
||||
@@ -826,7 +826,6 @@ def add_routes(
|
||||
include_in_schema=True,
|
||||
tags=route_tags,
|
||||
name=_route_name("stream"),
|
||||
dependencies=dependencies,
|
||||
description=(
|
||||
"This endpoint allows to stream the output of the runnable. "
|
||||
"The endpoint uses a server sent event stream to stream the "
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langserve"
|
||||
version = "0.0.43"
|
||||
version = "0.0.41"
|
||||
description = ""
|
||||
readme = "README.md"
|
||||
authors = ["LangChain"]
|
||||
|
||||
@@ -307,42 +307,6 @@ async def test_serve_playground_with_api_router() -> None:
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
async def test_root_path_on_playground(event_loop: AbstractEventLoop) -> None:
|
||||
"""Test that the playground respects the root_path for requesting assets"""
|
||||
|
||||
for root_path in ("/home/root", "/home/root/"):
|
||||
app = FastAPI(root_path=root_path)
|
||||
add_routes(
|
||||
app,
|
||||
RunnableLambda(lambda foo: "hello"),
|
||||
path="/chat",
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/router")
|
||||
add_routes(
|
||||
router,
|
||||
RunnableLambda(lambda foo: "hello"),
|
||||
path="/chat",
|
||||
)
|
||||
app.include_router(router)
|
||||
|
||||
async_client = AsyncClient(app=app, base_url="http://localhost:9999")
|
||||
|
||||
response = await async_client.get("/chat/playground/index.html")
|
||||
assert response.status_code == 200
|
||||
assert (
|
||||
f'src="{root_path.rstrip("/")}/chat/playground/assets/'
|
||||
in response.content.decode()
|
||||
), "html should contain reference to playground assets with root_path prefix"
|
||||
|
||||
response = await async_client.get("/router/chat/playground/index.html")
|
||||
assert response.status_code == 200
|
||||
assert (
|
||||
f'src="{root_path.rstrip("/")}/router/chat/playground/assets/'
|
||||
in response.content.decode()
|
||||
), "html should contain reference to playground assets with root_path prefix"
|
||||
|
||||
|
||||
async def test_server_async(app: FastAPI) -> None:
|
||||
"""Test the server directly via HTTP requests."""
|
||||
async with get_async_test_client(app, raise_app_exceptions=True) as async_client:
|
||||
|
||||
Reference in New Issue
Block a user