Skip to content

Commit

Permalink
refactor/simplify (#49)
Browse files Browse the repository at this point in the history
* refactor: 💡 remove /api/search

* fix: OpenAI Prompt and messages

* chore

* chore: 🤖 remove next.js appDir

* feat: 在对话间使用 system 插入补充信息

* refactor: db operator without Exception
  • Loading branch information
ThaddeusJiang authored Feb 14, 2024
1 parent fa4ff05 commit 8e3aa0f
Show file tree
Hide file tree
Showing 7 changed files with 138 additions and 106 deletions.
31 changes: 22 additions & 9 deletions components/ui/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { IconArrowUp } from "@tabler/icons-react"
import { useMutation, useQueryClient } from "@tanstack/react-query"

import classNames from "classnames"
import dayjs from "dayjs"
import produce from "immer"

import { Message } from "~/types"
Expand Down Expand Up @@ -36,15 +37,17 @@ export function Chat({
})

const answerCreateMutation = useMutation({
mutationFn: async ({ avatar_id, content }: { avatar_id: string; content: string }) => {
mutationFn: async (data: { avatar_id: string; content: string; created_at: string }) => {
const { avatar_id, content, created_at } = data
return fetch("/api/answerCreate", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
avatar_id,
content
content,
created_at
})
}).then((res) => res.json())
}
Expand All @@ -70,14 +73,16 @@ export function Chat({
id: "query_" + crypto.randomUUID(),
from_id: user!.id,
to_id: avatar.id,
content: query
content: query,
created_at: new Date().toISOString()
}

let resMessage = {
id: "answer_" + crypto.randomUUID(),
from_id: avatar.id,
to_id: user!.id,
content: ""
content: "",
created_at: new Date().toISOString()
}

// @ts-ignore FIXME: fix this
Expand All @@ -98,10 +103,16 @@ export function Chat({
queryFrom: user?.id,
queryTo: avatar.id,
query,
messages: (messageListCache?.pages?.[0]?.items || []).slice(-10).map((m) => ({
role: m.from_id === user?.id ? "user" : "assistant",
content: m.content
}))

messages: (messageListCache?.pages?.[0]?.items || [])
.filter((d) => dayjs(d.created_at).isAfter(dayjs().subtract(10, "minutes")))
.slice(-10)
.reverse()
.map((m) => ({
role: m.from_id === user?.id ? "user" : "assistant",
content: m.content,
created_at: m.created_at
}))
})
})

Expand Down Expand Up @@ -139,7 +150,8 @@ export function Chat({

await answerCreateMutation.mutate({
avatar_id: avatar.id,
content: answer
content: answer,
created_at: new Date().toISOString()
})
}

Expand All @@ -165,6 +177,7 @@ export function Chat({

return (
<>
{/* TODO: error message and re-try */}
<form className="form-control relative w-full md:max-w-screen-md" onSubmit={handleSubmit(onSubmit)}>
<div className="relative">
<textarea
Expand Down
3 changes: 0 additions & 3 deletions next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@
const nextConfig = {
reactStrictMode: true,
transpilePackages: ["react-tweet"],
experimental: {
appDir: true
},
images: {
domains: ["source.unsplash.com"]
}
Expand Down
116 changes: 74 additions & 42 deletions pages/api/answer.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import dayjs from "dayjs"
import endent from "endent"

// import { encode } from "gpt-3-encoder";
import { OpenAIMessage } from "~/types"
import { OpenAIStream } from "~/utils/openai"
import { createQueryRecord, getUserDetails, searchRelatedContents } from "~/utils/supabase-admin-v2"
import { getAvatar } from "~/utils/supabase-client"
import { createQueryRecord, getUserDetails, searchEmbeddings } from "~/utils/supabase-only"

var localizedFormat = require("dayjs/plugin/localizedFormat")
dayjs.extend(localizedFormat)

export const config = {
runtime: "edge"
Expand All @@ -21,51 +25,72 @@ async function readStream(stream: ReadableStream): Promise<string> {
}
}

const handler = async (req: Request): Promise<Response> => {
try {
const {
queryFrom,
queryTo,
query,
messages: prevMessages
} = (await req.json()) as {
queryFrom: string
queryTo: string
query: string
messages: { role: string; content: string }[]
}
async function _searchRelatedContents(data: { query: string; queryTo: string }) {
const { query, queryTo } = data
const { data: chunks, error } = await searchRelatedContents({
query,
queryTo
})

const { data: chunks, error } = await searchEmbeddings({
query,
queryTo
})
if (error) {
console.error("Error: searchRelatedContents", error)
return []
}

if (error) {
console.error(error)
return new Response("Error", { status: 500 })
}
return chunks
}

const user = await getUserDetails(queryFrom)
const avatar = await getAvatar(queryTo)
const handler = async (req: Request): Promise<Response> => {
const {
queryFrom,
queryTo,
query,
messages: prevMessages
} = (await req.json()) as {
queryFrom: string
queryTo: string
query: string
messages: OpenAIMessage[]
}

await createQueryRecord({
from: queryFrom,
to: queryTo,
message: query
// step1: searchRelatedContents
const relatedContents = await _searchRelatedContents({ query, queryTo })

// step2: createQueryRecord
const { error: createQueryRecordError } = await createQueryRecord({
from: queryFrom,
to: queryTo,
message: query
})
if (createQueryRecordError) {
console.error("Error: createQueryRecord", {
input: { queryFrom, queryTo, query },
error: createQueryRecordError
})
return new Response("Error: createQueryRecord", { status: 500 })
}

let messages = []
// step3: OpenAIStream
const user = await getUserDetails(queryFrom)
const avatar = await getAvatar(queryTo)

try {
// FIXME: user 一直是 null,因为 Postgres RLS 只允许 select * from users where id = current_user_id()
const user_full_name = user?.full_name ?? "user"
const avatar_name = avatar?.name ?? "avatar"

let messages: OpenAIMessage[] = []
let content_token = 0

for (let i = prevMessages.length - 1; i >= 0; i--) {
const message = prevMessages[i]
// content_token += encode(message.content).length;
content_token += message.content.length
if (content_token < 2000) {
messages.push(message)
messages.push({
role: message.role,
content: message.content
})
} else {
console.warn("content_token is over")
break
Expand All @@ -74,19 +99,26 @@ const handler = async (req: Request): Promise<Response> => {

messages = messages.reverse()

messages.push({
role: "system",
content: endent`
Please pretend to be a human named "${avatar_name}" and communicate with "${user_full_name}".
Please refer to your past posts and in the same style reply the ${user_full_name}'s message: ${query}
Your past posts:
###
${chunks?.map((d: any) => d.content).join("\n\n")}
###
const prompt: OpenAIMessage[] = [
{
role: "system",
// 你的任务是模仿人类 ${user_full_name} 的风格回复 ${avatar_name},不要透露你是一个 AI,不要承认你没有实体,不要透露你是一个机器人。
content: endent`
Your task is to mimic the style of ${user_full_name} as a human in replying to ${avatar_name}, do not reveal that you are an AI, do not admit that you have no entity, do not reveal that you are a robot.
`
})
}
]

const relatedContent = relatedContents?.map((item) => `${dayjs(item.essay_date).format("ll")}: ${item.content}`)
.join(`
`)

const stream = await OpenAIStream({ messages })
const stream = await OpenAIStream({
prompt,
messages,
query,
relatedContent: `${user_full_name} said: ${relatedContent}`
})

const [stream1, stream2] = stream.tee()

Expand Down
36 changes: 0 additions & 36 deletions pages/api/search.ts

This file was deleted.

12 changes: 9 additions & 3 deletions types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,9 +142,15 @@ export type Avatar = {

export type Message = {
id: string
from_id: string | null
to_id: string | null
content: string | null
from_id: string
to_id: string
content: string
created_at: string
}

export interface OpenAIMessage {
role: "user" | "system" | "assistant"
content: string
}

export type Memo = {
Expand Down
22 changes: 14 additions & 8 deletions utils/openai.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import { ParsedEvent, ReconnectInterval, createParser } from "eventsource-parser"
import { Configuration, OpenAIApi } from "openai"

import { OpenAIMessage } from "~/types"

export enum OpenAIModel {
GTP = "gpt-4"
}

const apiKey = process.env.OPENAI_API_KEY

export const OpenAIStream = async ({
messages
}: {
messages: {
role: string
content: string
}[]
export const OpenAIStream = async (data: {
prompt: OpenAIMessage[]
messages: OpenAIMessage[]
query: string
relatedContent?: string
}) => {
const { prompt, messages } = data
const encoder = new TextEncoder()
const decoder = new TextDecoder()

Expand All @@ -26,7 +27,12 @@ export const OpenAIStream = async ({
method: "POST",
body: JSON.stringify({
model: OpenAIModel.GTP,
messages,
messages: [
...prompt,
...messages,
...(data.relatedContent ? [{ role: "system", content: data.relatedContent }] : []),
{ role: "user", content: data.query }
],
max_tokens: 300,
temperature: 0.0,
stream: true
Expand Down
Loading

0 comments on commit 8e3aa0f

Please sign in to comment.