Skip to content

Commit

Permalink
Feat/edit chat history (#171)
Browse files Browse the repository at this point in the history
* ✨feat(chat): add edit button

Edit history title

* 💄 ux(chat): add title to page header

* 🩹 fix(chat): tidy phrases

* ⚡️ ref(chat): move view code to views

and some tidying

* fix(chat): rename edit label to rename_label

---------

Co-authored-by: zilaei <zzilaei@gmail.com>
  • Loading branch information
dannil76 and zilaei authored Oct 10, 2024
1 parent ea7ff75 commit e8ce46c
Show file tree
Hide file tree
Showing 9 changed files with 181 additions and 75 deletions.
41 changes: 0 additions & 41 deletions fai-rag-app/fai-backend/fai_backend/assistant/chat_state.py

This file was deleted.

12 changes: 2 additions & 10 deletions fai-rag-app/fai-backend/fai_backend/assistant/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class LLMClientChatMessage(BaseModel):

class AssistantStreamMessage(BaseModel):
timestamp: str = ""
role: Literal["system", "user", "assistant", "function"]
role: str
content: str
should_format: bool = False

Expand Down Expand Up @@ -52,17 +52,9 @@ class AssistantContext(BaseModel):
rag_output: Optional[str] = None


class ClientChatState(BaseModel):
user: str
chat_id: str
timestamp: str
title: str
delete_label: str = "Delete" # TODO: fix hack for allowing something to show up in list to click on.
history: list[LLMClientChatMessage]


class AssistantChatHistoryModel(Document):
user: str
title: str = ''
assistant: AssistantTemplate
history: list[AssistantStreamMessage] = []

Expand Down
4 changes: 2 additions & 2 deletions fai-rag-app/fai-backend/fai_backend/framework/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
'AnyUI',
)

from fai_backend.assistant.models import ClientChatState
from fai_backend.new_chat.models import ClientChatState
from fai_backend.config import settings
from fai_backend.framework import events as e
from fai_backend.framework.table import DataTable
Expand Down Expand Up @@ -98,7 +98,7 @@ class Button(UIComponent):
class Form(UIComponent):
type: Literal['Form'] = 'Form'
submit_url: str = Field(None, serialization_alias='action')
method: Literal['POST', 'GET'] = 'POST'
method: Literal['POST', 'GET', 'PATCH'] = 'POST'
submit_text: str | None = None
submit_as: Literal['json', 'form'] | None = Field('json', serialization_alias='submitAs')

Expand Down
6 changes: 6 additions & 0 deletions fai-rag-app/fai-backend/fai_backend/new_chat/dependencies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from fai_backend.new_chat.service import ChatStateService
from fai_backend.repositories import chat_history_repo


async def get_chat_state_service():
return ChatStateService(chat_history_repo)
18 changes: 18 additions & 0 deletions fai-rag-app/fai-backend/fai_backend/new_chat/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from pydantic import BaseModel

from fai_backend.assistant.models import LLMClientChatMessage


class ClientChatState(BaseModel):
user: str
chat_id: str
timestamp: str
title: str
delete_label: str = "Delete" # TODO: fix hack for allowing something to show up in list to click on.
rename_label: str = "Rename" # TODO: fix hack for allowing something to show up in list to click on.
history: list[LLMClientChatMessage]


class ChatHistoryEditPayload(BaseModel):
chat_id: str
title: str
55 changes: 34 additions & 21 deletions fai-rag-app/fai-backend/fai_backend/new_chat/routes.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
from typing import Any, Callable

from fastapi import APIRouter, Depends

from fai_backend.assistant.chat_state import ChatStateService, get_chat_state_service
from fai_backend.dependencies import get_page_template_for_logged_in_users, get_project_user
from fai_backend.framework import components as c
from fai_backend.framework import events as e
from fai_backend.framework.display import DisplayAs
from fai_backend.framework.table import DataColumn
from fai_backend.logger.route_class import APIRouter as LoggingAPIRouter
from fai_backend.new_chat.dependencies import get_chat_state_service
from fai_backend.new_chat.service import ChatStateService
from fai_backend.new_chat.models import ChatHistoryEditPayload
from fai_backend.new_chat.views import chat_history_edit_view, chat_history_list_view
from fai_backend.phrase import phrase as _
from fai_backend.projects.dependencies import list_projects_request
from fai_backend.projects.schema import ProjectResponse
Expand Down Expand Up @@ -46,22 +49,7 @@ async def chat_history_view(view=Depends(get_page_template_for_logged_in_users),
return [c.FireEvent(event=e.GoToEvent(url='/login'))]

states = await chat_state_service.get_states(user=user.email)
return view(
[c.DataTable(data=states,
columns=[DataColumn(key='title',
id='title',
display=DisplayAs.link,
on_click=e.GoToEvent(url='/chat/{chat_id}'),
sortable=True,
label=_('title', 'Title')),
DataColumn(key='delete_label',
display=DisplayAs.link,
on_click=e.GoToEvent(url='/chat/delete/{chat_id}'),
label=_('actions', 'Actions'))],
include_view_action=False)],
_('chat_history', 'Chat history')
)

return await chat_history_list_view(view, states)

@router.get('/chat/{chat_id}', response_model=list, response_model_exclude_none=True)
async def chat_view(chat_id: str,
Expand All @@ -72,10 +60,10 @@ async def chat_view(chat_id: str,
chat_history = await chat_state_service.get_state(chat_id)

if chat_history is None or chat_history.user != project_user.email:
return [c.FireEvent(event=e.GoToEvent(url='/login'))]
return [c.FireEvent(event=e.GoToEvent(url='/logout'))]

return view([c.SSEChat(chat_initial_state=chat_history)],
_('chat_history', 'Chat history'))
_('chat_history', f'Chat history ({chat_history.title})'))


@router.get('/chat/delete/{chat_id}', response_model=list, response_model_exclude_none=True)
Expand All @@ -90,3 +78,28 @@ async def chat_delete(chat_id: str,
await chat_state_service.delete_state(chat_id)
print(f'Chat history: {chat_id} deleted')
return [c.FireEvent(event=e.GoToEvent(url='/chat/history'))]


@router.get('/chat/edit/{chat_id}', response_model=list, response_model_exclude_none=True)
async def chat_edit(chat_id: str,
chat_state_service: ChatStateService = Depends(get_chat_state_service),
project_user: ProjectUser = Depends(get_project_user),
view: Callable[[list[Any], str | None], list[Any]] = Depends(
get_page_template_for_logged_in_users)) -> list:

state = await chat_state_service.get_state(chat_id)
if state is None or state.user != project_user.email:
return [c.FireEvent(event=e.GoToEvent(url='/logout'))]

return await chat_history_edit_view(view, state, '/api/chat/edit')


@router.patch('/chat/edit', response_model=list, response_model_exclude_none=True)
async def chat_edit_patch(data: ChatHistoryEditPayload,
chat_state_service: ChatStateService = Depends(get_chat_state_service)) -> list:

old_state = await chat_state_service.get_state(data.chat_id)
updated_state = old_state.model_copy(update={'title': data.title}, deep=True)
await chat_state_service.update_state(data.chat_id, updated_state)

return [c.FireEvent(event=e.GoToEvent(url='/chat/history'))]
56 changes: 56 additions & 0 deletions fai-rag-app/fai-backend/fai_backend/new_chat/service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from typing import List

from fai_backend.assistant.models import (AssistantChatHistoryModel, AssistantStreamMessage, LLMClientChatMessage)
from fai_backend.new_chat.models import ClientChatState
from fai_backend.repositories import ChatHistoryRepository, chat_history_repo
from fai_backend.repository.query.component import AttributeAssignment


class ChatStateService:
def __init__(self, history_repo: ChatHistoryRepository):
self.history_repo = history_repo

async def get_states(self, user: str) -> List[ClientChatState]:
results = await self.history_repo.list(query=AttributeAssignment('user', user))
return [ChatStateService.assistant_chat_history_model_to_chat_state(r) for r in results if len(r.history) > 0]

async def get_state(self, chat_id: str) -> ClientChatState:
history = await self.history_repo.get(chat_id)
return ChatStateService.assistant_chat_history_model_to_chat_state(history)

async def delete_state(self, chat_id: str) -> None:
await self.history_repo.delete(chat_id)

async def update_state(self, chat_id: str, new_state: ClientChatState) -> None:
new_history = await self.chat_state_to_assistant_chat_history_model(new_state)
ignore_keys = ['id', 'timestamp']

await self.history_repo.update(chat_id, {key: new_history.model_dump()[key]
for key in filter(lambda key: key not in ignore_keys,
new_history.model_dump().keys())})

async def chat_state_to_assistant_chat_history_model(self,
chat_state: ClientChatState) -> AssistantChatHistoryModel:
supplementary_history = await self.history_repo.get(chat_state.chat_id)
return AssistantChatHistoryModel(user=chat_state.user,
title=chat_state.title,
assistant=supplementary_history.assistant,
history=[AssistantStreamMessage(
timestamp=a.timestamp,
role=a.source,
content=a.content) for a in chat_state.history])

@staticmethod
def assistant_chat_history_model_to_chat_state(chat_history_model: AssistantChatHistoryModel) -> ClientChatState:
def convert_message(m: AssistantStreamMessage) -> LLMClientChatMessage:
return LLMClientChatMessage(timestamp=m.timestamp, source=m.role, content=m.content)

title = chat_history_model.title
default_title = title if title else chat_history_model.history[0].content[
:30] + '...' # TODO: replace title with pre-generated AI title

return ClientChatState(user=chat_history_model.user,
chat_id=str(chat_history_model.id),
timestamp=chat_history_model.history[0].timestamp,
title=default_title,
history=[convert_message(message) for message in chat_history_model.history])
62 changes: 62 additions & 0 deletions fai-rag-app/fai-backend/fai_backend/new_chat/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from fai_backend.framework import components as c
from fai_backend.framework import events as e
from fai_backend.framework.display import DisplayAs
from fai_backend.framework.table import DataColumn
from fai_backend.new_chat.models import ClientChatState
from fai_backend.phrase import phrase as _


async def chat_history_edit_view(view,
chat_history: ClientChatState,
submit_url: str) -> list:
return view(
[c.Div(components=[
c.Div(components=[
c.Form(
submit_url=submit_url,
method='PATCH',
submit_text=_('update_chat_history_title_button', 'Update title'),
components=[
c.InputField(
name='title',
title=_('input_title_label', 'Edit title'),
placeholder=_('input_title_placeholder', 'Enter new title here'),
required=True,
html_type='text',
),
c.InputField(
name='chat_id',
value=chat_history.chat_id,
hidden=True,
html_type='hidden',
)
],
)
], class_name='card-body'),
], class_name='card')],
_('edit_chat_history_title', f'Chat history - Edit ({chat_history.title})'),
)


async def chat_history_list_view(view, states: list[ClientChatState]) -> list:
return view(
[c.DataTable(data=states,
columns=[DataColumn(key='title',
id='title',
width=100,
display=DisplayAs.link,
on_click=e.GoToEvent(url='/chat/{chat_id}'),
sortable=True,
label=_('title', 'Title')),
DataColumn(key='rename_label',
display=DisplayAs.link,
on_click=e.GoToEvent(url='/chat/edit/{chat_id}'),
label=_('actions', 'Action')),
DataColumn(key='delete_label',
width=1,
display=DisplayAs.link,
on_click=e.GoToEvent(url='/chat/delete/{chat_id}'),
label=_('actions', 'Action'))],
include_view_action=False)],
_('chat_history', 'Chat history')
)
2 changes: 1 addition & 1 deletion fai-rag-app/fai-frontend/src/lib/components/Form.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@
export let className: string | null = ''
export { className as class }
export let action: string | null
export let method: 'post' | 'get' | null
export let method: 'post' | 'get' | 'patch' | null
export let submit_text: string | null = null
export let id: string = action || 'form' + method || 'post'
const attributes = writable({})
Expand Down

0 comments on commit e8ce46c

Please sign in to comment.