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

Rework user history #446

Merged
merged 26 commits into from
Oct 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9f2a0ac
fix: Fetching conversation request fail due to pydantic v2
alimtunc Oct 4, 2023
2e309b1
feat: Add revalidation options to useApi hook
alimtunc Oct 6, 2023
e9d30cd
refactor: Improve conversation browsing history
alimtunc Oct 4, 2023
0fcb29f
fix: Fix review PR
alimtunc Oct 6, 2023
364cbeb
fix scroll
willydouhard Oct 6, 2023
9c7172d
fix: History not loading more items sometime
alimtunc Oct 6, 2023
8ccee07
refactor: Clean conversation state
alimtunc Oct 6, 2023
7b2d401
fix infinite auth
willydouhard Oct 6, 2023
adb9bc4
Merge branch 'at/CHA-329-rework-user-chat-history' of github.com:Chai…
willydouhard Oct 6, 2023
c3fe16c
fix: Conversations not loaded
alimtunc Oct 6, 2023
0d248db
chat history css
willydouhard Oct 6, 2023
f28650c
fix tasklit
willydouhard Oct 6, 2023
4150356
clean fetch logic
willydouhard Oct 7, 2023
e7ef4f7
reduce batch size
willydouhard Oct 7, 2023
434f8cf
refactor: Move <ConversationsHistoryList/> data logic to <Conversatio…
alimtunc Oct 9, 2023
898f953
prevent conversation request if not logged in
willydouhard Oct 9, 2023
a54e245
fix: History is flicking when loading more
alimtunc Oct 9, 2023
7b035cd
feat/add cloud test (#461)
clementsirieix Oct 9, 2023
5e8b5b2
fix: Conversation alert width is broken + reduce navigation gap
alimtunc Oct 9, 2023
e899dec
refactor: Add pageInfo into recoil ConversationsState
alimtunc Oct 9, 2023
3c6af21
refactor: Use sx props instead styled for drawer of chatHistory
alimtunc Oct 9, 2023
03c76e4
fix: Scroll bar is levitating on bottom of the chat history list
alimtunc Oct 9, 2023
da45c8f
feat: Check if loader is visible on chat history test
alimtunc Oct 9, 2023
ae6aef7
refactor: Rename cloud test to conversations
alimtunc Oct 9, 2023
cbd509f
fix: Revert https://github.com/Chainlit/chainlit/pull/446/commits/898…
alimtunc Oct 9, 2023
38439cf
Reduce perceived latency for users (#463)
clementsirieix Oct 9, 2023
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
8 changes: 4 additions & 4 deletions backend/chainlit/client/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,13 +107,13 @@ class PaginatedResponse(DataClassJsonMixin, Generic[T]):

class Pagination(BaseModel):
first: int
cursor: Optional[str]
cursor: Optional[str] = None


class ConversationFilter(BaseModel):
feedback: Optional[Literal[-1, 0, 1]]
username: Optional[str]
search: Optional[str]
feedback: Optional[Literal[-1, 0, 1]] = None
username: Optional[str] = None
search: Optional[str] = None


class ChainlitGraphQLClient:
Expand Down
27 changes: 18 additions & 9 deletions backend/chainlit/element.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
from chainlit.client.cloud import ChainlitCloudClient
from chainlit.context import context
from chainlit.data import chainlit_client
from chainlit.logger import logger
from chainlit.telemetry import trace_event
from pydantic.dataclasses import Field, dataclass
from syncer import asyncio

mime_types = {
"text": "text/plain",
Expand Down Expand Up @@ -98,18 +100,25 @@ async def persist(self, client: ChainlitCloudClient) -> Optional[ElementDict]:
)
self.url = upload_res["url"]
self.object_key = upload_res["object_key"]
element_dict = await self.with_conversation_id()

asyncio.create_task(self._persist(element_dict))

if not self.persisted:
element_dict = await client.create_element(
await self.with_conversation_id()
)
self.persisted = True
else:
element_dict = await client.update_element(
await self.with_conversation_id()
)
return element_dict

async def _persist(self, element: ElementDict):
if not chainlit_client:
return

try:
if self.persisted:
await chainlit_client.update_element(element)
else:
await chainlit_client.create_element(element)
self.persisted = True
except Exception as e:
logger.error(f"Failed to persist element: {str(e)}")

async def before_emit(self, element: Dict) -> Dict:
return element

Expand Down
56 changes: 40 additions & 16 deletions backend/chainlit/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from chainlit.prompt import Prompt
from chainlit.telemetry import trace_event
from chainlit.types import AskFileResponse, AskFileSpec, AskResponse, AskSpec
from syncer import asyncio


class MessageBase(ABC):
Expand Down Expand Up @@ -43,23 +44,27 @@ async def with_conversation_id(self):

async def _create(self):
msg_dict = await self.with_conversation_id()
if chainlit_client and not self.persisted:
try:
persisted_id = await chainlit_client.create_message(msg_dict)
if persisted_id:
msg_dict["id"] = persisted_id
self.id = persisted_id
self.persisted = True
except Exception as e:
if self.fail_on_persist_error:
raise e
logger.error(f"Failed to persist message: {str(e)}")
asyncio.create_task(self._persist_create(msg_dict))

if not config.features.prompt_playground:
msg_dict.pop("prompt", None)

return msg_dict

async def _persist_create(self, message: MessageDict):
if not chainlit_client or self.persisted:
return

try:
persisted_id = await chainlit_client.create_message(message)

if persisted_id:
self.id = persisted_id
self.persisted = True
except Exception as e:
if self.fail_on_persist_error:
raise e
logger.error(f"Failed to persist message creation: {str(e)}")

async def update(
self,
):
Expand All @@ -69,14 +74,22 @@ async def update(
trace_event("update_message")

msg_dict = self.to_dict()

if chainlit_client and self.id:
await chainlit_client.update_message(self.id, msg_dict)

asyncio.create_task(self._persist_update(msg_dict))
await context.emitter.update_message(msg_dict)

return True

async def _persist_update(self, message: MessageDict):
if not chainlit_client or not self.id:
return

try:
await chainlit_client.update_message(self.id, message)
except Exception as e:
if self.fail_on_persist_error:
raise e
logger.error(f"Failed to persist message update: {str(e)}")

async def remove(self):
"""
Remove a message already sent to the UI.
Expand All @@ -91,6 +104,17 @@ async def remove(self):

return True

async def _persist_remove(self):
if not chainlit_client or not self.id:
return

try:
await chainlit_client.delete_message(self.id)
except Exception as e:
if self.fail_on_persist_error:
raise e
logger.error(f"Failed to persist message deletion: {str(e)}")

async def send(self):
if self.content is None:
self.content = ""
Expand Down
62 changes: 62 additions & 0 deletions cypress/e2e/conversations/.chainlit/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
[project]
# Whether to enable telemetry (default: true). No personal data is collected.
enable_telemetry = true

# List of environment variables to be provided by each user to use the app.
user_env = []

# Duration (in seconds) during which the session is saved when the connection is lost
session_timeout = 3600

# Enable third parties caching (e.g LangChain cache)
cache = false

# Follow symlink for asset mount (see https://github.com/Chainlit/chainlit/issues/317)
# follow_symlink = false

[features]
# Show the prompt playground
prompt_playground = true

[UI]
# Name of the app and chatbot.
name = "Chatbot"

# Description of the app and chatbot. This is used for HTML tags.
# description = ""

# Large size content are by default collapsed for a cleaner ui
default_collapse_content = true

# The default value for the expand messages settings.
default_expand_messages = false

# Hide the chain of thought details from the user in the UI.
hide_cot = false

# Link to your github repo. This will add a github button in the UI's header.
# github = ""

# Override default MUI light theme. (Check theme.ts)
[UI.theme.light]
#background = "#FAFAFA"
#paper = "#FFFFFF"

[UI.theme.light.primary]
#main = "#F80061"
#dark = "#980039"
#light = "#FFE7EB"

# Override default MUI dark theme. (Check theme.ts)
[UI.theme.dark]
#background = "#FAFAFA"
#paper = "#FFFFFF"

[UI.theme.dark.primary]
#main = "#F80061"
#dark = "#980039"
#light = "#FFE7EB"


[meta]
generated_by = "0.6.402"
21 changes: 21 additions & 0 deletions cypress/e2e/conversations/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from typing import Optional

import chainlit as cl


@cl.on_chat_start
async def main():
await cl.Message("Hello, send me a message!").send()


@cl.on_message
async def handle_message():
await cl.Message("Ok!").send()


@cl.password_auth_callback
def auth_callback(username: str, password: str) -> Optional[cl.AppUser]:
if (username, password) == ("admin", "admin"):
return cl.AppUser(username="admin", role="ADMIN", provider="credentials")
else:
return None
124 changes: 124 additions & 0 deletions cypress/e2e/conversations/spec.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { runTestServer } from '../../support/testUtils';

describe('Conversations', () => {
before(() => {
runTestServer(undefined, {
CHAINLIT_API_KEY: 'fake_key',
CHAINLIT_AUTH_SECRET:
'G=>I6me4>E_y,n$_%K%XqbTMKXGQy-jvZ6:1oR>~o8z@DPb*.QY~NkgctmBDg3T-'
});

cy.intercept('POST', '/login', {
statusCode: 200,
body: {
access_token:
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6IkFETUlOIiwidGFncyI6W10sImltYWdlIjpudWxsLCJwcm92aWRlciI6ImNyZWRlbnRpYWxzIiwiZXhwIjoxNjk4MTM5MzgwfQ.VQS_O0Zar1O3BVzJ_bu4_8r-1LW0Mfq2En7sIojzd04',
token_type: 'bearer'
}
}).as('postLogin');

cy.intercept('GET', '/project/settings', {
statusCode: 200,
body: {
ui: {},
userEnv: [],
dataPersistence: true,
markdown: 'foo'
}
}).as('getSettings');

const makeData = (start: number, count: number) =>
Array.from({ length: count }, (_, i) => ({
id: String(start + i),
createdAt: Date.now(),
tags: ['chat'],
elementCount: 0,
messageCount: 3,
appUser: { username: 'admin' },
messages: [{ content: `foo ${start + i}` }]
}));

cy.intercept('POST', '/project/conversations', (req) => {
const { cursor } = req.body.pagination;
const dataCount = cursor ? 3 : 20;
const startId = cursor ? 21 : 1;

req.reply({
statusCode: 200,
body: {
pageInfo: {
hasNextPage: !cursor,
endCursor: cursor ? 'newCursor' : 'someCursor'
},
data: makeData(startId, dataCount)
}
});
}).as('getConversations');

cy.intercept('GET', '/project/conversation/*', (req) => {
const conversationId = req.url.split('/').pop();

req.reply({
statusCode: 200,
body: {
id: conversationId,
createdAt: Date.now(),
tags: ['chat'],
messages: [
{
id: '2b1755ab-f7e3-48fa-9fe1-535595142b96',
isError: false,
parentId: null,
indent: 0,
author: 'Chatbot',
content: `Foo ${conversationId} message`,
waitForAnswer: false,
humanFeedback: 0,
humanFeedbackComment: null,
disableHumanFeedback: false,
language: null,
prompt: null,
authorIsUser: false,
createdAt: 1696844037149
}
],
elements: []
}
});
}).as('getConversation');

cy.intercept('DELETE', '/project/conversation', {
statusCode: 200,
body: {
success: true
}
}).as('deleteConversation');
});

describe('Conversations history', () => {
it('should perform conversations history operations', () => {
// Login to the app
cy.get("[id='email']").type('admin');
cy.get("[id='password']").type('admin{enter}');

// Conversations are being displayed
cy.contains('Foo 1');
cy.contains('Foo 2');

// Scroll chat and fetch new conversations
cy.get('.chat-history-drawer > div').scrollTo('bottom');
cy.get('#chat-history-loader').should('be.visible');
cy.contains('Foo 23');

// Select conversation
cy.get('#conversation-18').click();
cy.get('#conversation-18').should('be.visible');
cy.contains('Foo 18 message');

// Delete conversation
cy.get("[data-testid='DeleteOutlineIcon']").click();
cy.get("[type='button']").contains('Confirm').click();
cy.contains('Conversation deleted!');
});
});
});
9 changes: 7 additions & 2 deletions cypress/support/testUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,15 @@ export function closeHistory() {
cy.get(`body`).click();
}

export function runTestServer(mode: ExecutionMode = undefined) {
export function runTestServer(
mode: ExecutionMode = undefined,
env?: Record<string, string>
) {
const pathItems = Cypress.spec.absolute.split(sep);
const testName = pathItems[pathItems.length - 2];
cy.exec(`pnpm exec ts-node ./cypress/support/run.ts ${testName} ${mode}`);
cy.exec(`pnpm exec ts-node ./cypress/support/run.ts ${testName} ${mode}`, {
env
});
cy.visit('/');
}

Expand Down
Loading