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

feat(onboarding): add suggested questions answer #1390

Merged
merged 6 commits into from
Oct 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions backend/models/databases/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,10 @@ def update_chat_history(self, chat_id: str, user_message: str, assistant: str):
def update_chat(self, chat_id: UUID, updates):
pass

@abstractmethod
def add_question_and_answer(self, chat_id: str, question_and_answer):
pass

@abstractmethod
def update_message_by_id(self, message_id: UUID, updates):
pass
Expand Down
25 changes: 25 additions & 0 deletions backend/models/databases/supabase/chats.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import Optional
from uuid import UUID

from models.chat import Chat
from models.databases.repository import Repository
from pydantic import BaseModel

Expand All @@ -13,6 +14,11 @@ class CreateChatHistory(BaseModel):
brain_id: Optional[UUID]


class QuestionAndAnswer(BaseModel):
question: str
answer: str


class Chats(Repository):
def __init__(self, supabase_client):
self.db = supabase_client
Expand All @@ -30,6 +36,25 @@ def get_chat_by_id(self, chat_id: str):
)
return response

def add_question_and_answer(
self, chat_id: UUID, question_and_answer: QuestionAndAnswer
) -> Optional[Chat]:
response = (
self.db.table("chat_history")
.insert(
{
"chat_id": str(chat_id),
"user_message": question_and_answer.question,
"assistant": question_and_answer.answer,
}
)
.execute()
).data
if len(response) > 0:
response = Chat(response[0])

return None

def get_chat_history(self, chat_id: str):
reponse = (
self.db.from_("chat_history")
Expand Down
13 changes: 13 additions & 0 deletions backend/repository/chat/add_question_and_answer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from typing import Optional
from uuid import UUID

from models import Chat, get_supabase_db
from models.databases.supabase.chats import QuestionAndAnswer


def add_question_and_answer(
chat_id: UUID, question_and_answer: QuestionAndAnswer
) -> Optional[Chat]:
supabase_db = get_supabase_db()

return supabase_db.add_question_and_answer(chat_id, question_and_answer)
20 changes: 19 additions & 1 deletion backend/routes/chat_routes.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import time
from typing import List
from typing import List, Optional
from uuid import UUID
from venv import logger

Expand All @@ -17,6 +17,7 @@
UserUsage,
get_supabase_db,
)
from models.databases.supabase.chats import QuestionAndAnswer
from models.databases.supabase.supabase import SupabaseDB
from repository.brain import get_brain_details
from repository.chat import (
Expand All @@ -28,12 +29,14 @@
get_user_chats,
update_chat,
)
from repository.chat.add_question_and_answer import add_question_and_answer
from repository.chat.get_chat_history_with_notifications import (
ChatItem,
get_chat_history_with_notifications,
)
from repository.notification.remove_chat_notifications import remove_chat_notifications
from repository.user_identity import get_user_identity

from routes.authorizations.brain_authorization import validate_brain_authorization
from routes.authorizations.types import RoleEnum

Expand Down Expand Up @@ -375,3 +378,18 @@ async def get_chat_history_handler(
) -> List[ChatItem]:
# TODO: RBAC with current_user
return get_chat_history_with_notifications(chat_id)


@chat_router.post(
"/chat/{chat_id}/question/answer",
dependencies=[Depends(AuthBearer())],
tags=["Chat"],
)
async def add_question_and_answer_handler(
chat_id: UUID,
question_and_answer: QuestionAndAnswer,
) -> Optional[Chat]:
"""
Add a new question and anwser to the chat.
"""
return add_question_and_answer(chat_id, question_and_answer)
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const OnboardingQuestion = ({
return (
<div
onClick={() => void handleSuggestionClick()}
className="cursor-pointer shadow-md dark:shadow-primary/25 hover:shadow-xl transition-shadow bg-yellow-100 px-3 py-1 rounded-xl border-black/10 dark:border-white/25"
className="cursor-pointer shadow-md dark:shadow-primary/25 hover:shadow-xl transition-shadow bg-onboarding-yellow-bg px-3 py-1 rounded-xl border-black/10 dark:border-white/25"
>
{question}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,90 @@
import { UUID } from "crypto";
import { useParams, useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";

import { useChat } from "@/app/chat/[chatId]/hooks/useChat";
import { ChatMessage } from "@/app/chat/[chatId]/types";
import { useChatApi } from "@/lib/api/chat/useChatApi";
import { useChatContext } from "@/lib/context";
import { getChatNameFromQuestion } from "@/lib/helpers/getChatNameFromQuestion";
import { useOnboarding } from "@/lib/hooks/useOnboarding";
import { useOnboardingTracker } from "@/lib/hooks/useOnboardingTracker";
import { useStreamText } from "@/lib/hooks/useStreamText";

import { QuestionId } from "../../../types";
import { questionIdToTradPath } from "../utils";

// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useOnboardingQuestion = (questionId: QuestionId) => {
const router = useRouter();
const params = useParams();
const { updateOnboarding } = useOnboarding();
const { t } = useTranslation("chat");
const { createChat } = useChatApi();
const { trackOnboardingEvent } = useOnboardingTracker();
const [isAnswerRequested, setIsAnswerRequested] = useState(false);

const onboardingStep = questionIdToTradPath[questionId];
const [chatId, setChatId] = useState(params?.chatId as UUID | undefined);

const onboardingStep = questionIdToTradPath[questionId];
const question = t(`onboarding.${onboardingStep}`);
const answer = t(`onboarding.answer.${onboardingStep}`);
const { updateStreamingHistory } = useChatContext();
const { addQuestionAndAnswer } = useChatApi();
const { lastStream, isDone } = useStreamText({
text: answer,
enabled: isAnswerRequested && chatId !== undefined,
});

const addQuestionAndAnswerToChat = async () => {
if (chatId === undefined) {
return;
}

await addQuestionAndAnswer(chatId, {
question: question,
answer: answer,
});
const shouldUpdateUrl = chatId !== params?.chatId;
if (shouldUpdateUrl) {
router.replace(`/chat/${chatId}`);
}
};

useEffect(() => {
if (!isDone) {
return;
}
void addQuestionAndAnswerToChat();
}, [isDone]);

const { addQuestion } = useChat();
useEffect(() => {
if (chatId === undefined) {
return;
}

if (isAnswerRequested) {
const chatMessage: ChatMessage = {
chat_id: chatId,
message_id: questionId,
user_message: question,
assistant: lastStream,
message_time: Date.now().toLocaleString(),
brain_name: "Quivr",
};
void updateStreamingHistory(chatMessage);
}
}, [isAnswerRequested, question, questionId, lastStream]);

const handleSuggestionClick = async () => {
if (chatId === undefined) {
const newChat = await createChat(getChatNameFromQuestion(question));
setChatId(newChat.chat_id);
}
trackOnboardingEvent(onboardingStep);
await Promise.all([
addQuestion(question),
updateOnboarding({ [questionId]: false }),
]);
setIsAnswerRequested(true);
mamadoudicko marked this conversation as resolved.
Show resolved Hide resolved

await updateOnboarding({ [questionId]: false });
};

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import { QuestionId } from "../../types";

export const questionIdToTradPath: Record<
QuestionId,
keyof Translations["chat"]["onboarding"]
keyof Pick<
Translations["chat"]["onboarding"],
"how_to_use_quivr" | "what_is_brain" | "what_is_quivr"
>
> = {
onboarding_b1: "how_to_use_quivr",
onboarding_b2: "what_is_quivr",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { RiDownloadLine } from "react-icons/ri";

import Button from "@/lib/components/ui/Button";
import { useOnboardingTracker } from "@/lib/hooks/useOnboardingTracker";
import { useStreamText } from "@/lib/hooks/useStreamText";

import { useStreamText } from "./hooks/useStreamText";
import { stepsContainerStyle } from "./styles";
import { MessageRow } from "../QADisplay";

Expand Down
4 changes: 2 additions & 2 deletions frontend/app/chat/[chatId]/hooks/useChat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { CHATS_DATA_KEY } from "@/lib/api/chat/config";
import { useChatApi } from "@/lib/api/chat/useChatApi";
import { useChatContext } from "@/lib/context";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
import { getChatNameFromQuestion } from "@/lib/helpers/getChatNameFromQuestion";
import { useToast } from "@/lib/hooks";
import { useOnboarding } from "@/lib/hooks/useOnboarding";
import { useOnboardingTracker } from "@/lib/hooks/useOnboardingTracker";
Expand Down Expand Up @@ -58,8 +59,7 @@ export const useChat = () => {

//if chatId is not set, create a new chat. Chat name is from the first question
if (currentChatId === undefined) {
const chatName = question.split(" ").slice(0, 3).join(" ");
const chat = await createChat(chatName);
const chat = await createChat(getChatNameFromQuestion(question));
currentChatId = chat.chat_id;
setChatId(currentChatId);
shouldUpdateUrl = true;
Expand Down
20 changes: 20 additions & 0 deletions frontend/lib/api/chat/__tests__/useChatApi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,4 +131,24 @@ describe("useChatApi", () => {
chat_name: chatName,
});
});

it("should call addQuestionAndAnswer with the correct parameters", async () => {
const chatId = "test-chat-id";
const question = "test-question";
const answer = "test-answer";
axiosPostMock.mockReturnValue({ data: {} });
const {
result: {
current: { addQuestionAndAnswer },
},
} = renderHook(() => useChatApi());

await addQuestionAndAnswer(chatId, { question, answer });

expect(axiosPostMock).toHaveBeenCalledTimes(1);
expect(axiosPostMock).toHaveBeenCalledWith(
`/chat/${chatId}/question/answer`,
{ question, answer }
);
});
});
21 changes: 21 additions & 0 deletions frontend/lib/api/chat/addQuestionAndAnswer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { AxiosInstance } from "axios";

import { ChatMessage } from "@/app/chat/[chatId]/types";

export type QuestionAndAnwser = {
question: string;
answer: string;
};

export const addQuestionAndAnswer = async (
chatId: string,
questionAndAnswer: QuestionAndAnwser,
axiosInstance: AxiosInstance
): Promise<ChatMessage> => {
const response = await axiosInstance.post<ChatMessage>(
`/chat/${chatId}/question/answer`,
questionAndAnswer
);

return response.data;
};
8 changes: 8 additions & 0 deletions frontend/lib/api/chat/useChatApi.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { useAxios } from "@/lib/hooks";

import {
addQuestionAndAnswer,
QuestionAndAnwser,
} from "./addQuestionAndAnswer";
import {
addQuestion,
AddQuestionParams,
Expand All @@ -25,5 +29,9 @@ export const useChatApi = () => {
getChatItems: async (chatId: string) => getChatItems(chatId, axiosInstance),
updateChat: async (chatId: string, props: ChatUpdatableProperties) =>
updateChat(chatId, props, axiosInstance),
addQuestionAndAnswer: async (
chatId: string,
questionAndAnswer: QuestionAndAnwser
) => addQuestionAndAnswer(chatId, questionAndAnswer, axiosInstance),
};
};
2 changes: 2 additions & 0 deletions frontend/lib/helpers/getChatNameFromQuestion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const getChatNameFromQuestion = (question: string): string =>
question.split(" ").slice(0, 3).join(" ");
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ export const useStreamText = ({
shouldStream = true,
}: UseStreamTextProps) => {
const [streamingText, setStreamingText] = useState<string>("");
const [currentIndex, setCurrentIndex] = useState(0);
const [lastStreamIndex, setLastStreamIndex] = useState(0);

const isDone = currentIndex === text.length;
const isDone = lastStreamIndex === text.length;

const lastStream = !isDone ? text[lastStreamIndex] : "";

useEffect(() => {
if (!enabled) {
Expand All @@ -25,15 +27,17 @@ export const useStreamText = ({

if (!shouldStream) {
setStreamingText(text);
setCurrentIndex(text.length);
setLastStreamIndex(text.length);

return;
}

const messageInterval = setInterval(() => {
if (currentIndex < text.length) {
setStreamingText((prevText) => prevText + (text[currentIndex] ?? ""));
setCurrentIndex((prevIndex) => prevIndex + 1);
if (lastStreamIndex < text.length) {
setStreamingText(
(prevText) => prevText + (text[lastStreamIndex] ?? "")
);
setLastStreamIndex((prevIndex) => prevIndex + 1);
} else {
clearInterval(messageInterval);
}
Expand All @@ -42,7 +46,7 @@ export const useStreamText = ({
return () => {
clearInterval(messageInterval);
};
}, [text, currentIndex, enabled, shouldStream]);
}, [text, lastStreamIndex, enabled, shouldStream]);

return { streamingText, isDone };
return { streamingText, isDone, lastStream };
};
7 changes: 6 additions & 1 deletion frontend/public/locales/en/chat.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,12 @@
"step_3": "3. Enjoy !",
"how_to_use_quivr": "How to use Quivr ?",
"what_is_quivr": "What is Quivr ?",
"what_is_brain": "What is a brain ?"
"what_is_brain": "What is a brain ?",
"answer":{
"how_to_use_quivr": "Check the documentation https://brain.quivr.app/docs/get_started/intro.html",
"what_is_quivr": "Quivr is a helpful assistant.",
"what_is_brain": "A brain contains knowledge"
}
},
"welcome":"Welcome"
}
Loading
Loading