Compare commits

..

1 Commits

Author SHA1 Message Date
Eugene Yurtsev bacafee01b Update examples to examples and templates
Update examples to examples and templates, but add an anchor
2024-02-12 14:11:21 -05:00
19 changed files with 338 additions and 590 deletions
+1 -1
View File
@@ -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."
}
+13 -61
View File
@@ -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)
-20
View File
@@ -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)
-38
View File
@@ -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
View File
@@ -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,
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -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>
-1
View File
@@ -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>
-4
View File
@@ -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,
-1
View File
@@ -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
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langserve"
version = "0.0.43"
version = "0.0.41"
description = ""
readme = "README.md"
authors = ["LangChain"]
-36
View File
@@ -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: