Skip to content

Commit

Permalink
FS-96 Start new chat (#24)
Browse files Browse the repository at this point in the history
  • Loading branch information
mic-smith authored Nov 14, 2024
1 parent 50fce8c commit fae22d3
Show file tree
Hide file tree
Showing 12 changed files with 108 additions and 36 deletions.
16 changes: 15 additions & 1 deletion backend/src/api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
import os
from azure.storage.blob import BlobServiceClient
from typing import NoReturn
from fastapi import FastAPI, HTTPException, WebSocket, UploadFile
from fastapi import FastAPI, HTTPException, Response, WebSocket, UploadFile
from fastapi.responses import JSONResponse
from fastapi.middleware.cors import CORSMiddleware
from src.session.file_uploads import clear_session_file_uploads
from src.session.redis_session_middleware import reset_session
from src.utils.graph_db_utils import populate_db
from src.utils import Config, test_connection
from src.director import question
Expand Down Expand Up @@ -95,6 +97,18 @@ async def chat(utterance: str):
logger.exception(e)
return JSONResponse(status_code=500, content=chat_fail_response)

@app.delete("/chat")
async def clear_chat():
logger.info("Delete the chat session")
try:
# clear files first as need session data for file keys
clear_session_file_uploads()
reset_session()
return Response(status_code=204)
except Exception as e:
logger.exception(e)
return Response(status_code=500)


@app.get("/suggestions")
async def suggestions():
Expand Down
4 changes: 2 additions & 2 deletions backend/src/session/file_uploads.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ def update_session_file_uploads(file_upload:FileUpload):
redis_client.set(UPLOADS_KEY_PREFIX + file_upload["uploadId"], json.dumps(file_upload))


def clear_session_file_uploads_meta():
logger.info("File Uploads session cleared")
def clear_session_file_uploads():
logger.info("Clearing file uploads from session")

meta_list = get_session(UPLOADS_META_SESSION_KEY, [])

Expand Down
4 changes: 4 additions & 0 deletions backend/src/session/redis_session_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ def set_session(key: str, value):
request: Request = request_context.get()
request.state.session[key] = value

def reset_session():
logger.info("Reset chat session")
request: Request = request_context.get()
request.state.session = {}

def get_redis_session(request: Request):
session_id = request.cookies.get(SESSION_COOKIE_NAME)
Expand Down
11 changes: 11 additions & 0 deletions backend/tests/api/app_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,17 @@ def test_chat_response_failure(mocker):
assert response.status_code == 500
assert response.json() == chat_fail_response

def test_chat_delete(mocker):
mock_reset_session = mocker.patch("src.api.app.reset_session")
mock_clear_files = mocker.patch("src.api.app.clear_session_file_uploads")

response = client.delete("/chat")

mock_clear_files.assert_called_once()
mock_reset_session.assert_called_once()

assert response.status_code == 204


@pytest.mark.asyncio
async def test_lifespan_populates_db(mocker, mock_initial_data) -> None:
Expand Down
6 changes: 3 additions & 3 deletions backend/tests/session/test_file_uploads.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from unittest.mock import patch, MagicMock
from starlette.requests import Request
from starlette.responses import Response
from src.session.file_uploads import (FileUpload, clear_session_file_uploads_meta, get_session_file_upload,
from src.session.file_uploads import (FileUpload, clear_session_file_uploads, get_session_file_upload,
get_session_file_uploads_meta, update_session_file_uploads)

@pytest.fixture
Expand Down Expand Up @@ -73,7 +73,7 @@ def test_clear_session_file_uploads_meta(mocker, mock_redis, mock_request_contex

update_session_file_uploads(file_upload=file)

clear_session_file_uploads_meta()
clear_session_file_uploads()
assert get_session_file_uploads_meta() == []
mock_redis.delete.assert_called_with("file_upload_1234")

Expand All @@ -82,7 +82,7 @@ def test_clear_session_file_uploads_meta(mocker, mock_redis, mock_request_contex
assert get_session_file_uploads_meta() == [ {'filename': 'test.txt', 'uploadId': '1234'},
{'filename': 'test2.txt', 'uploadId': '12345'}]

clear_session_file_uploads_meta()
clear_session_file_uploads()
assert get_session_file_uploads_meta() == []
mock_redis.delete.assert_called_with("file_upload_1234 file_upload_12345")

Expand Down
13 changes: 13 additions & 0 deletions backend/tests/session/test_redis_session_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from starlette.responses import Response
from src.session.redis_session_middleware import (
get_session,
reset_session,
set_session,
get_redis_session
)
Expand Down Expand Up @@ -63,3 +64,15 @@ def test_get_redis_session(mocker, mock_request, mock_redis):

assert result == {"user_id": 1}
mock_redis.get.assert_called_once_with(session_id)

def test_reset_session(mocker, mock_request_context):
mocker.patch("src.session.redis_session_middleware.request_context", mock_request_context)

set_session("key1", "value1")
set_session("key2", "value2")
assert get_session("key1") == "value1"
assert get_session("key2") == "value2"

reset_session()
assert get_session("key1", None) is None
assert get_session("key2", None) is None
19 changes: 15 additions & 4 deletions frontend/src/app.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,29 @@
import React from 'react';
import React, { useEffect } from 'react';
import styles from './app.module.css';
import { Chat } from './components/chat';
import { Input } from './components/input';
import { useMessages } from './useMessages';
import { NavBar } from './components/navbar';

export const App = () => {
const { sendMessage, messages, waiting } = useMessages();
const {
sendMessage,
messages,
waiting,
suggestions,
resetMessages,
initSuggestions,
} = useMessages();

useEffect(() => {
initSuggestions();
}, []);

return (
<div className={styles.container}>
<NavBar />
<NavBar startNewConversation={resetMessages} />
<Chat messages={messages} waiting={waiting} />
<Input sendMessage={sendMessage} waiting={waiting} />
<Input sendMessage={sendMessage} suggestions={suggestions} />
</div>
);
};
6 changes: 3 additions & 3 deletions frontend/src/components/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ import { Suggestions } from './suggestions';

export interface InputProps {
sendMessage: (message: string) => void;
waiting: boolean;
suggestions: string[];
}

export const Input = ({ sendMessage, waiting }: InputProps) => {
export const Input = ({ sendMessage, suggestions }: InputProps) => {
const [userInput, setUserInput] = useState<string>('');

const onChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
Expand Down Expand Up @@ -54,7 +54,7 @@ export const Input = ({ sendMessage, waiting }: InputProps) => {
<img src={RightArrow} />
</button>
</form>
<Suggestions loadPrompt={setUserInput} waiting={waiting} />
<Suggestions loadPrompt={setUserInput} suggestions={suggestions} />
</>
);
};
10 changes: 6 additions & 4 deletions frontend/src/components/navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import ChatIcon from '../icons/chat.svg';
import { Button } from './button';
import logo from '../icons/primary-logo-dark.svg';

export const NavBar = () => {
interface NavbarProps {
startNewConversation: () => void;
}

export const NavBar = ({ startNewConversation }: NavbarProps) => {
return (
<div className={styles.container}>
<div>
Expand All @@ -14,9 +18,7 @@ export const NavBar = () => {
<Button
icon={ChatIcon}
text="Start new chat"
onClick={() => {
console.log('Start new chat');
}}
onClick={startNewConversation}
/>
</div>
</div>
Expand Down
21 changes: 3 additions & 18 deletions frontend/src/components/suggestions.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,12 @@
import React, { useEffect, useState } from 'react';
import React from 'react';
import styles from './suggestions.module.css';
import { getSuggestions } from '../server';

export interface SuggestionsProps {
suggestions: string[];
loadPrompt: (suggestion: string) => void;
waiting: boolean;
}

export const Suggestions = ({ loadPrompt, waiting }: SuggestionsProps) => {
const [suggestions, setSuggestions] = useState<string[]>([]);

useEffect(() => {
const fetchSuggestions = async () => {
if (!waiting) {
const newSuggestions = await getSuggestions();
if (Array.isArray(newSuggestions) && newSuggestions.length > 0) {
setSuggestions(newSuggestions);
}
}
};
fetchSuggestions();
}, [waiting]);

export const Suggestions = ({ loadPrompt, suggestions }: SuggestionsProps) => {
return (
<div className={styles.container}>
{suggestions.map((suggestion, index) => (
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,10 @@ export const getSuggestions = async (): Promise<string[]> => {
return [];
});
};

export const resetChat = async (): Promise<Response> => {
return await fetch(`${process.env.BACKEND_URL}/chat`, {
credentials: 'include',
method: 'DELETE',
});
};
27 changes: 26 additions & 1 deletion frontend/src/useMessages.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useCallback, useState } from 'react';
import { Message, Role } from './components/message';
import { getResponse } from './server';
import { getResponse, getSuggestions, resetChat } from './server';

const starterMessage: Message = {
role: Role.Bot,
Expand All @@ -10,13 +10,24 @@ const starterMessage: Message = {

export interface UseMessagesHook {
sendMessage: (message: string) => void;
resetMessages: () => void;
initSuggestions: () => void;
messages: Message[];
suggestions: string[];
waiting: boolean;
}

export const useMessages = (): UseMessagesHook => {
const [waiting, setWaiting] = useState<boolean>(false);
const [messages, setMessages] = useState<Message[]>([starterMessage]);
const [suggestions, setSuggestions] = useState<string[]>([]);

const fetchSuggestions = useCallback(async () => {
const newSuggestions = await getSuggestions();
if (Array.isArray(newSuggestions) && newSuggestions.length > 0) {
setSuggestions(newSuggestions);
}
}, []);

const appendMessage = useCallback((message: string, role: Role) => {
setMessages((prevMessages) => [
Expand All @@ -32,13 +43,27 @@ export const useMessages = (): UseMessagesHook => {
const response = await getResponse(message);
setWaiting(false);
appendMessage(response.message, Role.Bot);
if (message !== 'healthcheck') {
fetchSuggestions();
}
},
[appendMessage, messages],
);

const resetMessages = useCallback(async () => {
await resetChat();
setWaiting(false);
setMessages([{ ...starterMessage, time: new Date().toLocaleTimeString() }]);
setSuggestions([]);
fetchSuggestions();
}, []);

return {
sendMessage,
messages,
suggestions,
waiting,
resetMessages,
initSuggestions: fetchSuggestions,
};
};

0 comments on commit fae22d3

Please sign in to comment.