Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature plugin (GPTs like action based on function call) #5331

Merged
merged 41 commits into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
c99cd31
add openapi-client-axios
lloydzhou Aug 24, 2024
f5209fc
stash code
lloydzhou Aug 28, 2024
29b5cd9
ts error
lloydzhou Aug 28, 2024
f3f6dc5
stash code
lloydzhou Aug 28, 2024
d212df8
stash code
lloydzhou Aug 28, 2024
f7a5f83
stash code
lloydzhou Aug 28, 2024
d58b99d
stash code
lloydzhou Aug 28, 2024
341a52a
stash code
lloydzhou Aug 28, 2024
7fc0d11
create common function stream for fetchEventSource
lloydzhou Aug 29, 2024
d2cb984
add processToolMessage callback
lloydzhou Aug 29, 2024
571ce11
stash code
lloydzhou Aug 29, 2024
cac99e3
add Plugin page
lloydzhou Aug 30, 2024
271f58d
stash code
lloydzhou Aug 30, 2024
9326ff9
ts error
lloydzhou Aug 30, 2024
2214689
add gapier proxy
lloydzhou Aug 30, 2024
b2965e1
update
lloydzhou Aug 30, 2024
f652f73
plugin add auth config
lloydzhou Sep 2, 2024
877668b
hotfix
lloydzhou Sep 2, 2024
801b625
claude support function call
lloydzhou Sep 2, 2024
078305f
kimi support function call
lloydzhou Sep 2, 2024
6435e7a
update readme
lloydzhou Sep 2, 2024
3ec67f9
add load from url
lloydzhou Sep 2, 2024
2b317f6
add config auth location
lloydzhou Sep 3, 2024
236736d
remove no need code
lloydzhou Sep 3, 2024
4fdd997
hotfix
lloydzhou Sep 3, 2024
d30351e
update readme
lloydzhou Sep 3, 2024
c7bc93b
Merge remote-tracking branch 'connectai/main' into feature/plugin
lloydzhou Sep 3, 2024
0a5522d
update
lloydzhou Sep 3, 2024
7180ed9
hotfix
lloydzhou Sep 3, 2024
6ab6b3d
remove no need code
lloydzhou Sep 3, 2024
04e1ab6
update readme
lloydzhou Sep 4, 2024
f9a047a
using tauri http api run plugin to fixed cors in App
lloydzhou Sep 4, 2024
09aec7b
using tauri http api run plugin to fixed cors in App
lloydzhou Sep 4, 2024
9820193
Merge pull request #6 from ConnectAI-E/feature/plugin-app-cors
lloydzhou Sep 4, 2024
b590d08
disable nextjs proxy, then can using dalle as plugin
lloydzhou Sep 5, 2024
caf50b6
move artifacts into mask settings
lloydzhou Sep 5, 2024
80b8f95
move artifacts into mask settings
lloydzhou Sep 5, 2024
f32dd69
Merge pull request #7 from ConnectAI-E/feature/plugin-artifact
lloydzhou Sep 5, 2024
7c0acc7
hotfix tools empty array
lloydzhou Sep 5, 2024
7455978
default enable artifact
lloydzhou Sep 6, 2024
9275f2d
add awesome plugin repo url
lloydzhou Sep 6, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions app/api/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,7 @@ export async function requestOpenai(req: NextRequest) {
authHeaderName = "Authorization";
}

let path = `${req.nextUrl.pathname}${req.nextUrl.search}`.replaceAll(
"/api/openai/",
"",
);
let path = `${req.nextUrl.pathname}`.replaceAll("/api/openai/", "");

let baseUrl =
(isAzure ? serverConfig.azureUrl : serverConfig.baseUrl) || OPENAI_BASE_URL;
Expand Down
10 changes: 9 additions & 1 deletion app/client/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@ import {
ModelProvider,
ServiceProvider,
} from "../constant";
import { ChatMessage, ModelType, useAccessStore, useChatStore } from "../store";
import {
ChatMessageTool,
ChatMessage,
ModelType,
useAccessStore,
useChatStore,
} from "../store";
import { ChatGPTApi, DalleRequestPayload } from "./platforms/openai";
import { GeminiProApi } from "./platforms/google";
import { ClaudeApi } from "./platforms/anthropic";
Expand Down Expand Up @@ -56,6 +62,8 @@ export interface ChatOptions {
onFinish: (message: string) => void;
onError?: (err: Error) => void;
onController?: (controller: AbortController) => void;
onBeforeTool?: (tool: ChatMessageTool) => void;
onAfterTool?: (tool: ChatMessageTool) => void;
}

export interface LLMUsage {
Expand Down
276 changes: 199 additions & 77 deletions app/client/platforms/openai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,8 @@ export class ChatGPTApi implements LLMApi {
let responseText = "";
let remainText = "";
let finished = false;
let running = false;
let runTools = [];

// animate response to make it looks smooth
function animateResponseText() {
Expand All @@ -276,99 +278,219 @@ export class ChatGPTApi implements LLMApi {
// start animaion
animateResponseText();

// TODO εŽι’θΏ™ι‡Œζ˜―δ»Žι€‰ζ‹©ηš„pluginsδΈ­θŽ·ε–functionεˆ—θ‘¨
const funcs = {
get_current_weather: (args) => {
console.log("call get_current_weather", args);
return "30";
},
};
const finish = () => {
if (!finished) {
console.log("try run tools", runTools.length, finished, running);
if (!running && runTools.length > 0) {
const toolCallMessage = {
role: "assistant",
tool_calls: [...runTools],
};
running = true;
runTools.splice(0, runTools.length); // empty runTools
return Promise.all(
toolCallMessage.tool_calls.map((tool) => {
options?.onBeforeTool(tool);
return Promise.resolve(
funcs[tool.function.name](
JSON.parse(tool.function.arguments),
),
)
.then((content) => {
options?.onAfterTool({
...tool,
content,
isError: false,
});
return content;
})
.catch((e) => {
options?.onAfterTool({ ...tool, isError: true });
return e.toString();
})
.then((content) => ({
role: "tool",
content,
tool_call_id: tool.id,
}));
}),
).then((toolCallResult) => {
console.log("end runTools", toolCallMessage, toolCallResult);
requestPayload["messages"].splice(
requestPayload["messages"].length,
0,
toolCallMessage,
...toolCallResult,
);
setTimeout(() => {
// call again
console.log("start again");
running = false;
chatApi(chatPath, requestPayload); // call fetchEventSource
}, 0);
});
console.log("try run tools", runTools.length, finished);
return;
}
if (running) {
return;
}
lloydzhou marked this conversation as resolved.
Show resolved Hide resolved
finished = true;
options.onFinish(responseText + remainText);
}
};

controller.signal.onabort = finish;

fetchEventSource(chatPath, {
...chatPayload,
async onopen(res) {
clearTimeout(requestTimeoutId);
const contentType = res.headers.get("content-type");
console.log(
"[OpenAI] request response content type: ",
contentType,
);

if (contentType?.startsWith("text/plain")) {
responseText = await res.clone().text();
return finish();
}
function chatApi(chatPath, requestPayload) {
const chatPayload = {
method: "POST",
body: JSON.stringify({
...requestPayload,
// TODO θΏ™ι‡Œζš‚ζ—Άε†™ζ­»ηš„οΌŒεŽι’δ»Žstore.toolsδΈ­ζŒ‰η…§ε½“ε‰sessionδΈ­ι€‰ζ‹©ηš„θŽ·ε–
tools: [
{
type: "function",
function: {
name: "get_current_weather",
description: "Get the current weather",
parameters: {
type: "object",
properties: {
location: {
type: "string",
description:
"The city and country, eg. San Francisco, USA",
},
format: {
type: "string",
enum: ["celsius", "fahrenheit"],
},
},
required: ["location", "format"],
},
},
},
],
}),
signal: controller.signal,
headers: getHeaders(),
};
console.log("chatApi", chatPath, requestPayload, chatPayload);
fetchEventSource(chatPath, {
...chatPayload,
async onopen(res) {
clearTimeout(requestTimeoutId);
const contentType = res.headers.get("content-type");
console.log(
"[OpenAI] request response content type: ",
contentType,
);

if (contentType?.startsWith("text/plain")) {
responseText = await res.clone().text();
return finish();
}

if (
!res.ok ||
!res.headers
.get("content-type")
?.startsWith(EventStreamContentType) ||
res.status !== 200
) {
const responseTexts = [responseText];
let extraInfo = await res.clone().text();
try {
const resJson = await res.clone().json();
extraInfo = prettyObject(resJson);
} catch {}
if (
!res.ok ||
!res.headers
.get("content-type")
?.startsWith(EventStreamContentType) ||
res.status !== 200
) {
const responseTexts = [responseText];
let extraInfo = await res.clone().text();
try {
const resJson = await res.clone().json();
extraInfo = prettyObject(resJson);
} catch {}

if (res.status === 401) {
responseTexts.push(Locale.Error.Unauthorized);
}
if (res.status === 401) {
responseTexts.push(Locale.Error.Unauthorized);
}

if (extraInfo) {
responseTexts.push(extraInfo);
}
if (extraInfo) {
responseTexts.push(extraInfo);
}

responseText = responseTexts.join("\n\n");
responseText = responseTexts.join("\n\n");

return finish();
}
},
onmessage(msg) {
if (msg.data === "[DONE]" || finished) {
return finish();
}
const text = msg.data;
try {
const json = JSON.parse(text);
const choices = json.choices as Array<{
delta: { content: string };
}>;
const delta = choices[0]?.delta?.content;
const textmoderation = json?.prompt_filter_results;

if (delta) {
remainText += delta;
return finish();
}

if (
textmoderation &&
textmoderation.length > 0 &&
ServiceProvider.Azure
) {
const contentFilterResults =
textmoderation[0]?.content_filter_results;
console.log(
`[${ServiceProvider.Azure}] [Text Moderation] flagged categories result:`,
contentFilterResults,
);
},
onmessage(msg) {
if (msg.data === "[DONE]" || finished) {
return finish();
}
} catch (e) {
console.error("[Request] parse error", text, msg);
}
},
onclose() {
finish();
},
onerror(e) {
options.onError?.(e);
throw e;
},
openWhenHidden: true,
});
const text = msg.data;
try {
const json = JSON.parse(text);
const choices = json.choices as Array<{
delta: { content: string };
}>;
console.log("choices", choices);
const delta = choices[0]?.delta?.content;
const tool_calls = choices[0]?.delta?.tool_calls;
const textmoderation = json?.prompt_filter_results;

if (delta) {
remainText += delta;
}
if (tool_calls?.length > 0) {
const index = tool_calls[0]?.index;
const id = tool_calls[0]?.id;
const args = tool_calls[0]?.function?.arguments;
if (id) {
runTools.push({
id,
type: tool_calls[0]?.type,
function: {
name: tool_calls[0]?.function?.name,
arguments: args,
},
});
} else {
runTools[index]["function"]["arguments"] += args;
}
}

console.log("runTools", runTools);

if (
textmoderation &&
textmoderation.length > 0 &&
ServiceProvider.Azure
) {
const contentFilterResults =
textmoderation[0]?.content_filter_results;
console.log(
`[${ServiceProvider.Azure}] [Text Moderation] flagged categories result:`,
contentFilterResults,
);
}
} catch (e) {
console.error("[Request] parse error", text, msg);
}
},
onclose() {
finish();
},
onerror(e) {
options.onError?.(e);
throw e;
},
openWhenHidden: true,
});
lloydzhou marked this conversation as resolved.
Show resolved Hide resolved
}
chatApi(chatPath, requestPayload); // call fetchEventSource
} else {
const res = await fetch(chatPath, chatPayload);
clearTimeout(requestTimeoutId);
Expand Down
17 changes: 16 additions & 1 deletion app/components/chat.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,21 @@
margin-top: 5px;
}

.chat-message-tools {
font-size: 12px;
color: #aaa;
line-height: 1.5;
margin-top: 5px;
.chat-message-tool {
display: inline-flex;
align-items: end;
svg {
margin-left: 5px;
margin-right: 5px;
}
}
}

.chat-message-item {
box-sizing: border-box;
max-width: 100%;
Expand Down Expand Up @@ -630,4 +645,4 @@
.chat-input-send {
bottom: 30px;
}
}
}
Loading
Loading