forked from CopilotKit/CopilotKit
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathuse-chat.ts
More file actions
190 lines (171 loc) · 5.55 KB
/
Copy pathuse-chat.ts
File metadata and controls
190 lines (171 loc) · 5.55 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
import { useRef, useState } from "react";
import { Message, Function, FunctionCallHandler, FunctionCall } from "@copilotkit/shared";
import { nanoid } from "nanoid";
import { fetchAndDecodeChatCompletion } from "../utils/fetch-chat-completion";
import { CopilotApiConfig } from "../context";
export type UseChatOptions = {
/**
* The API endpoint that accepts a `{ messages: Message[] }` object and returns
* a stream of tokens of the AI chat response. Defaults to `/api/chat`.
*/
api?: string;
/**
* A unique identifier for the chat. If not provided, a random one will be
* generated. When provided, the `useChat` hook with the same `id` will
* have shared states across components.
*/
id?: string;
/**
* System messages of the chat. Defaults to an empty array.
*/
initialMessages?: Message[];
/**
* Callback function to be called when a function call is received.
* If the function returns a `ChatRequest` object, the request will be sent
* automatically to the API and will be used to update the chat.
*/
onFunctionCall?: FunctionCallHandler;
/**
* HTTP headers to be sent with the API request.
*/
headers?: Record<string, string> | Headers;
/**
* Extra body object to be sent with the API request.
* @example
* Send a `sessionId` to the API along with the messages.
* ```js
* useChat({
* body: {
* sessionId: '123',
* }
* })
* ```
*/
body?: object;
/**
* Function definitions to be sent to the API.
*/
functions?: Function[];
};
export type UseChatHelpers = {
/** Current messages in the chat */
messages: Message[];
/**
* Append a user message to the chat list. This triggers the API call to fetch
* the assistant's response.
* @param message The message to append
*/
append: (message: Message) => Promise<void>;
/**
* Reload the last AI chat response for the given chat history. If the last
* message isn't from the assistant, it will request the API to generate a
* new response.
*/
reload: () => Promise<void>;
/**
* Abort the current request immediately, keep the generated tokens if any.
*/
stop: () => void;
/** The current value of the input */
input: string;
/** setState-powered method to update the input value */
setInput: React.Dispatch<React.SetStateAction<string>>;
/** Whether the API request is in progress */
isLoading: boolean;
};
export type UseChatOptionsWithCopilotConfig = UseChatOptions & {
copilotConfig: CopilotApiConfig;
};
export function useChat(options: UseChatOptionsWithCopilotConfig): UseChatHelpers {
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState("");
const [isLoading, setIsLoading] = useState(false);
const abortControllerRef = useRef<AbortController>();
const runChatCompletion = async (messages: Message[]): Promise<Message> => {
setIsLoading(true);
const assistantMessage: Message = {
id: nanoid(),
createdAt: new Date(),
content: "",
role: "assistant",
};
const abortController = new AbortController();
abortControllerRef.current = abortController;
setMessages([...messages, { ...assistantMessage }]);
const messagesWithContext = [...(options.initialMessages || []), ...messages];
const response = await fetchAndDecodeChatCompletion({
copilotConfig: options.copilotConfig,
messages: messagesWithContext,
functions: options.functions,
headers: options.headers,
signal: abortController.signal,
});
if (!response.events) {
throw new Error("Failed to fetch chat completion");
}
const reader = response.events.getReader();
while (true) {
try {
const { done, value } = await reader.read();
if (done) {
setIsLoading(false);
return { ...assistantMessage };
}
if (value.type === "content") {
assistantMessage.content += value.content;
setMessages([...messages, { ...assistantMessage }]);
} else if (value.type === "function") {
assistantMessage.function_call = {
name: value.name,
arguments: JSON.stringify(value.arguments),
};
setMessages([...messages, { ...assistantMessage }]);
// quit early if we get a function call
setIsLoading(false);
return { ...assistantMessage };
}
} catch (error) {
setIsLoading(false);
throw error;
}
}
};
const runChatCompletionAndHandleFunctionCall = async (messages: Message[]): Promise<void> => {
const message = await runChatCompletion(messages);
if (message.function_call && options.onFunctionCall) {
await options.onFunctionCall(messages, message.function_call as FunctionCall);
}
};
const append = async (message: Message): Promise<void> => {
if (isLoading) {
return;
}
const newMessages = [...messages, message];
setMessages(newMessages);
return runChatCompletionAndHandleFunctionCall(newMessages);
};
const reload = async (): Promise<void> => {
if (isLoading || messages.length === 0) {
return;
}
let newMessages = [...messages];
const lastMessage = messages[messages.length - 1];
if (lastMessage.role === "assistant") {
newMessages = newMessages.slice(0, -1);
}
setMessages(newMessages);
return runChatCompletionAndHandleFunctionCall(newMessages);
};
const stop = (): void => {
abortControllerRef.current?.abort();
};
return {
messages,
append,
reload,
stop,
isLoading,
input,
setInput,
};
}