mirror of
https://github.com/langchain-ai/langserve.git
synced 2026-07-25 13:15:44 -04:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d66567dd1f | |||
| 65d814aa51 | |||
| c2bae658d2 | |||
| 803c587131 | |||
| 5b21b056df | |||
| 59f26055e0 | |||
| 24d804c073 | |||
| 31ad97b9e0 |
@@ -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/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/chat/tuples/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,6 +161,23 @@ 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:
|
||||
@@ -532,7 +549,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/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/chat/tuples/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
|
||||
@@ -604,7 +621,7 @@ Example widget:
|
||||
### Chat Widget
|
||||
|
||||
Look
|
||||
at [widget example](https://github.com/langchain-ai/langserve/tree/main/examples/widgets/server.py).
|
||||
at the [widget example](https://github.com/langchain-ai/langserve/tree/main/examples/widgets/chat/tuples/server.py).
|
||||
|
||||
To define a chat widget, make sure that you pass "type": "chat".
|
||||
|
||||
@@ -616,7 +633,6 @@ 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(
|
||||
...,
|
||||
@@ -655,6 +671,37 @@ 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.
|
||||
|
||||
Executable
+64
@@ -0,0 +1,64 @@
|
||||
#!/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)
|
||||
+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-fbe3df33.js"></script>
|
||||
<script type="module" crossorigin src="/____LANGSERVE_BASE_URL/assets/index-a7c0cdaa.js"></script>
|
||||
<link rel="stylesheet" href="/____LANGSERVE_BASE_URL/assets/index-52e8ab2f.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"packageManager": "yarn@1.22.19",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
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,8 +1,5 @@
|
||||
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,
|
||||
@@ -10,11 +7,10 @@ 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 * as ToggleGroup from "@radix-ui/react-toggle-group";
|
||||
import { MessageFields, ChatMessageInput } from "./ChatMessageInput";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export const chatMessagesTester = rankWith(
|
||||
12,
|
||||
@@ -61,98 +57,48 @@ 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 ?? [];
|
||||
|
||||
useStreamCallback("onSuccess", (ctx) => {
|
||||
useEffect(() => {
|
||||
if (!isJsonSchemaExtra(props.schema)) return;
|
||||
const widget = props.schema.extra.widget;
|
||||
if (!("input" in widget) && !("output" in widget)) return;
|
||||
if (props.schema.extra.widget.type !== "chat") return;
|
||||
setTimeout(() => props.handleChange(props.path, [
|
||||
...data,
|
||||
{ content: "", type: "human" },
|
||||
]), 10);
|
||||
}, []);
|
||||
|
||||
const inputPath = getNormalizedJsonPath(widget.input ?? "");
|
||||
const outputPath = getNormalizedJsonPath(widget.output ?? "");
|
||||
useStreamCallback("onStart", () => {
|
||||
if (!isJsonSchemaExtra(props.schema)) return;
|
||||
if (props.schema.extra.widget.type !== "chat") return;
|
||||
props.handleChange(props.path, [...data, { content: "", type: "ai" }]);
|
||||
});
|
||||
|
||||
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("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 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);
|
||||
}
|
||||
useStreamCallback("onSuccess", () => {
|
||||
if (!isJsonSchemaExtra(props.schema)) return;
|
||||
if (props.schema.extra.widget.type !== "chat") return;
|
||||
props.handleChange(props.path, [...data, { content: "", type: "human" }]);
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -178,174 +124,25 @@ 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 isAiFunctionCall = isOpenAiFunctionCall(
|
||||
message.additional_kwargs?.function_call
|
||||
);
|
||||
const handleChatMessageChange = (field: string, value: any) => {
|
||||
props.handleChange(Paths.compose(msgPath, field), value);
|
||||
};
|
||||
|
||||
const handleChatMessageRemoval = () => {
|
||||
props.handleChange(
|
||||
props.path,
|
||||
data.filter((_, i) => i !== 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, "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>
|
||||
<ChatMessageInput
|
||||
message={message}
|
||||
handleChange={handleChatMessageChange}
|
||||
handleRemoval={handleChatMessageRemoval}
|
||||
path={props.path}
|
||||
key={index}
|
||||
></ChatMessageInput>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
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,6 +10,7 @@ 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);
|
||||
@@ -18,9 +19,10 @@ 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: [], onSuccess: [], onError: [] });
|
||||
}>({ onStart: [], onChunk: [], onSuccess: [], onError: [] });
|
||||
|
||||
const callbacks: StreamCallback = {
|
||||
onStart(...args) {
|
||||
@@ -28,6 +30,11 @@ 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);
|
||||
@@ -44,7 +51,7 @@ export function useAppStreamCallbacks() {
|
||||
}
|
||||
|
||||
export function useStreamCallback<
|
||||
Type extends "onStart" | "onSuccess" | "onError"
|
||||
Type extends "onStart" | "onChunk" | "onSuccess" | "onError"
|
||||
>(type: Type, callback: Exclude<StreamCallback[Type], undefined>) {
|
||||
type CallbackType = Exclude<StreamCallback[Type], undefined>;
|
||||
|
||||
|
||||
@@ -52,6 +52,9 @@ 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;
|
||||
|
||||
@@ -74,6 +77,7 @@ 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,
|
||||
|
||||
Reference in New Issue
Block a user