Skip to content

Commit

Permalink
Merge branch 'fix-database-permissions' into 417-threads-endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
gphorvath authored Jun 12, 2024
2 parents 3ff9851 + 35dc42c commit 8290b9b
Show file tree
Hide file tree
Showing 66 changed files with 4,702 additions and 2,110 deletions.
26 changes: 26 additions & 0 deletions src/leapfrogai_ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,3 +195,29 @@ DEFAULT_MODEL=gpt-3.5-turbo
LEAPFROGAI_API_BASE_URL=https://api.openai.com/v1
LEAPFROGAI_API_KEY=<your-openai-api-key>
```

### Chat Data Flow

The logic for handling regular chat messages and assistant chat messages, along with persisting that data to the database is complex and deserves a detailed explanation.

Our chat page allows the user to send messages to /api/chat ("regular chat") and /api/chat/assistants ("chat with assistant"). The messages are streamed to the client so that text is
progressively displayed on the screen. We use the Vercel [AI SDK](https://sdk.vercel.ai/docs/getting-started/svelte) to handle streaming as well as response cancellation, regeneration, message editing, error handling, and more.

Messages streamed with regular chat, use the "useChat" function.
Assistants use the "useAssistants" function.
These functions do not provide the same features and handle data differently, resulting in several edge cases.

Here are a few of the big issues caused by these differences:

The useChat function does not save messages with the API to the database, we have to handle that on our own.
Messages sent with useAssistants, however, are saved to the database automatically.

Creation timestamps are handled differently depending on if they are streamed responses or if they have been saved to the database.
Streamed messages have timestamps on the "createdAt" field, saved messages have timestamps on the "created_at" field. Sometimes the dates are Date strings, unix seconds, or unix milliseconds.
Since dates can be returned in seconds, we lose some of the precision we would have for sorting the messages if they were returned in milliseconds. Due to this issue, there is logic in place to prevent the
user from sending messages too quickly, ensuring timestamps are unique.

Additionally, streamed messages have temporary ids that do not match the ids messages are assigned when they are saved to the database. This makes editing and deleting messages challenging, so we have to keep track of both streamed
messages and saved messages in client side state in the correct order. We use this state to look up the saved ids and make the appropriate API calls with the permanent ids.

While there are several automated tests for this logic, the edge cases and mocking scenarios are complex. Any modifications to this logic should be thoroughly manually tested.
3,443 changes: 2,037 additions & 1,406 deletions src/leapfrogai_ui/package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/leapfrogai_ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/svelte": "^5.1.0",
"@types/eslint": "^8.56.0",
"@types/lodash": "^4.17.4",
"@types/uuid": "^9.0.8",
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.7.1",
Expand All @@ -47,6 +48,7 @@
"eslint-plugin-svelte": "^2.38.0",
"husky": "^9.0.11",
"jsdom": "^24.0.0",
"lodash": "^4.17.21",
"otpauth": "^9.2.2",
"prettier": "^3.1.1",
"prettier-plugin-svelte": "^3.1.2",
Expand Down
1 change: 1 addition & 0 deletions src/leapfrogai_ui/src/app.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ declare global {
assistant?: LFAssistant;
files?: FileObject[];
}

// interface PageState {}
// interface Platform {}
}
Expand Down
51 changes: 51 additions & 0 deletions src/leapfrogai_ui/src/lib/components/AssistantFileSelect.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<script lang="ts">
import { FileUploaderItem, MultiSelect } from 'carbon-components-svelte';
import { fade } from 'svelte/transition';
import type { FileObject } from 'openai/resources/files';
export let files: FileObject[];
export let selectedFileIds: string[];
</script>

<MultiSelect
label="Choose data sources"
items={files?.map((file) => ({ id: file.id, text: file.filename }))}
direction="top"
bind:selectedIds={selectedFileIds}
/>

<div class="file-item-list">
{#each [...(files || [])]
.filter((f) => selectedFileIds.includes(f.id))
.sort((a, b) => a.filename.localeCompare(b.filename)) as file}
<div transition:fade={{ duration: 70 }}>
<FileUploaderItem
id={file.id}
name={file.filename}
size="small"
status="edit"
style="max-width: 100%"
on:delete={() => {
selectedFileIds = selectedFileIds.filter((id) => id !== file.id);
}}
/>
</div>
{/each}
</div>

<input type="hidden" name="data_sources" bind:value={selectedFileIds} />

<style lang="scss">
.file-item-list {
display: flex;
flex-direction: column;
gap: layout.$spacing-03;
}
:global(.bx--tag) {
// hide tag but keep spacing for multiselect text the same
visibility: hidden;
width: 0rem;
margin-left: 0.25rem;
}
</style>
18 changes: 10 additions & 8 deletions src/leapfrogai_ui/src/lib/components/AssistantForm.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,29 @@
ASSISTANTS_NAME_MAX_LENGTH
} from '$lib/constants';
import { superForm } from 'sveltekit-superforms';
import { Add } from 'carbon-icons-svelte';
import { page } from '$app/stores';
import { beforeNavigate, goto, invalidate } from '$app/navigation';
import { Button, Modal, Slider, TextArea, TextInput } from 'carbon-components-svelte';
import AssistantAvatar from '$components/AssistantAvatar.svelte';
import { yup } from 'sveltekit-superforms/adapters';
import { toastStore } from '$stores';
import InputTooltip from '$components/InputTooltip.svelte';
import { editAssistantInputSchema, assistantInputSchema } from '$lib/schemas/assistants';
import { assistantInputSchema, editAssistantInputSchema } from '$lib/schemas/assistants';
import type { NavigationTarget } from '@sveltejs/kit';
import { onMount } from 'svelte';
import AssistantFileSelect from '$components/AssistantFileSelect.svelte';
export let data;
let isEditMode = $page.url.pathname.includes('edit');
let bypassCancelWarning = false;
let selectedFileIds: string[] = data.form.data.data_sources || [];
const { form, errors, enhance, submitting, isTainted } = superForm(data.form, {
invalidateAll: false,
validators: yup(isEditMode ? editAssistantInputSchema : assistantInputSchema),
onResult({ result }) {
invalidate('/api/assistants');
invalidate('lf:assistants');
if (result.type === 'redirect') {
toastStore.addToast({
kind: 'success',
Expand Down Expand Up @@ -169,11 +170,12 @@
labelText="Data Sources"
tooltipText="Specific files your assistant can search and reference"
/>
<div>
<Button icon={Add} kind="secondary" size="small"
>Add <input name="data_sources" type="hidden" /></Button
>
</div>
<AssistantFileSelect files={data?.files} bind:selectedFileIds />
<input
type="hidden"
name="vectorStoreId"
value={data?.assistant?.tool_resources?.file_search?.vector_store_ids[0] || undefined}
/>

<div>
<Button
Expand Down
7 changes: 5 additions & 2 deletions src/leapfrogai_ui/src/lib/components/AssistantTile.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
import { fade } from 'svelte/transition';
import DynamicPictogram from '$components/DynamicPictogram.svelte';
import { Modal, OverflowMenu, OverflowMenuItem } from 'carbon-components-svelte';
import { toastStore } from '$stores';
import { threadsStore, toastStore } from '$stores';
import { Edit, TrashCan } from 'carbon-icons-svelte';
import type { LFAssistant } from '$lib/types/assistants';
import { NO_SELECTED_ASSISTANT_ID } from '$constants';
export let assistant: LFAssistant;
Expand All @@ -19,11 +20,13 @@
'Content-Type': 'application/json'
}
});
if ($threadsStore.selectedAssistantId === assistant.id)
threadsStore.setSelectedAssistantId(NO_SELECTED_ASSISTANT_ID);
deleteModalOpen = false;
if (res.ok) {
await invalidate('/api/assistants');
await invalidate('lf:assistants');
toastStore.addToast({
kind: 'info',
title: 'Assistant Deleted.',
Expand Down
5 changes: 2 additions & 3 deletions src/leapfrogai_ui/src/lib/components/ChatSidebar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
OverflowMenu,
OverflowMenuItem,
SideNav,
SideNavDivider,
SideNavItems,
SideNavMenu,
SideNavMenuItem,
Expand Down Expand Up @@ -163,7 +162,7 @@
bind:value={searchText}
maxlength={25}
/>
<SideNavDivider />
<hr id="divider" class="divider" />
</div>

<div
Expand Down Expand Up @@ -245,7 +244,7 @@
{/each}
</div>
<div>
<SideNavDivider />
<hr id="divider" class="divider" />
<ImportExport />
</div>
</div>
Expand Down
43 changes: 29 additions & 14 deletions src/leapfrogai_ui/src/lib/components/ChatSidebar.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import stores from '$app/stores';
import { getUnixSeconds, monthNames } from '$helpers/dates';
import * as navigation from '$app/navigation';
import { getMessageText } from '$helpers/threads';
import { NO_SELECTED_ASSISTANT_ID } from '$constants';

const { getStores } = await vi.hoisted(() => import('../../lib/mocks/svelte'));

Expand Down Expand Up @@ -64,7 +65,8 @@ vi.mock('$app/stores', (): typeof stores => {
describe('ChatSidebar', () => {
it('renders threads', async () => {
threadsStore.set({
threads: fakeThreads
threads: fakeThreads,
selectedAssistantId: NO_SELECTED_ASSISTANT_ID
});

render(ChatSidebar);
Expand All @@ -86,7 +88,8 @@ describe('ChatSidebar', () => {
});

threadsStore.set({
threads: [fakeTodayThread, fakeYesterdayThread] // uses date override starting in March
threads: [fakeTodayThread, fakeYesterdayThread], // uses date override starting in March
selectedAssistantId: NO_SELECTED_ASSISTANT_ID
});

render(ChatSidebar);
Expand All @@ -108,7 +111,8 @@ describe('ChatSidebar', () => {
mockDeleteThread();

threadsStore.set({
threads: fakeThreads
threads: fakeThreads,
selectedAssistantId: NO_SELECTED_ASSISTANT_ID
});

render(ChatSidebar);
Expand Down Expand Up @@ -143,7 +147,8 @@ describe('ChatSidebar', () => {
const toastSpy = vi.spyOn(toastStore, 'addToast');

threadsStore.set({
threads: fakeThreads
threads: fakeThreads,
selectedAssistantId: NO_SELECTED_ASSISTANT_ID
});

render(ChatSidebar);
Expand Down Expand Up @@ -174,7 +179,8 @@ describe('ChatSidebar', () => {
mockEditThreadLabel();

threadsStore.set({
threads: fakeThreads
threads: fakeThreads,
selectedAssistantId: NO_SELECTED_ASSISTANT_ID
});

render(ChatSidebar);
Expand All @@ -192,7 +198,8 @@ describe('ChatSidebar', () => {
mockEditThreadLabel();

threadsStore.set({
threads: fakeThreads
threads: fakeThreads,
selectedAssistantId: NO_SELECTED_ASSISTANT_ID
});

render(ChatSidebar);
Expand All @@ -210,7 +217,8 @@ describe('ChatSidebar', () => {
mockEditThreadLabel();

threadsStore.set({
threads: fakeThreads
threads: fakeThreads,
selectedAssistantId: NO_SELECTED_ASSISTANT_ID
});

render(ChatSidebar);
Expand Down Expand Up @@ -239,7 +247,8 @@ describe('ChatSidebar', () => {

const newLabelText = 'new label';
threadsStore.set({
threads: fakeThreads
threads: fakeThreads,
selectedAssistantId: NO_SELECTED_ASSISTANT_ID
});

render(ChatSidebar);
Expand All @@ -257,7 +266,8 @@ describe('ChatSidebar', () => {
it('does not update the thread label when the user presses escape and it removes the text input', async () => {
const newLabelText = 'new label';
threadsStore.set({
threads: fakeThreads
threads: fakeThreads,
selectedAssistantId: NO_SELECTED_ASSISTANT_ID
});

render(ChatSidebar);
Expand All @@ -277,7 +287,8 @@ describe('ChatSidebar', () => {
mockEditThreadLabel();

threadsStore.set({
threads: fakeThreads
threads: fakeThreads,
selectedAssistantId: NO_SELECTED_ASSISTANT_ID
});

render(ChatSidebar);
Expand All @@ -299,7 +310,8 @@ describe('ChatSidebar', () => {
const newLabelText = 'new label';

threadsStore.set({
threads: fakeThreads
threads: fakeThreads,
selectedAssistantId: NO_SELECTED_ASSISTANT_ID
});

render(ChatSidebar);
Expand All @@ -314,7 +326,8 @@ describe('ChatSidebar', () => {
const fakeThread = getFakeThread({ numMessages: 6 });

threadsStore.set({
threads: [fakeThread]
threads: [fakeThread],
selectedAssistantId: NO_SELECTED_ASSISTANT_ID
});

render(ChatSidebar);
Expand All @@ -332,7 +345,8 @@ describe('ChatSidebar', () => {
const fakeThread3 = getFakeThread({ numMessages: 2 });

threadsStore.set({
threads: [fakeThread1, fakeThread2, fakeThread3]
threads: [fakeThread1, fakeThread2, fakeThread3],
selectedAssistantId: NO_SELECTED_ASSISTANT_ID
});

render(ChatSidebar);
Expand All @@ -355,7 +369,8 @@ describe('ChatSidebar', () => {
const fakeThread3 = getFakeThread({ numMessages: 2 });

threadsStore.set({
threads: [fakeThread1, fakeThread2, fakeThread3]
threads: [fakeThread1, fakeThread2, fakeThread3],
selectedAssistantId: NO_SELECTED_ASSISTANT_ID
});

render(ChatSidebar);
Expand Down
Empty file.
Loading

0 comments on commit 8290b9b

Please sign in to comment.