use `lowdb` for faster iteration; use strings for ids and camelCase for field names for consistency

master
Avraham Sakal 3 months ago
parent 1f80ad4ba9
commit 01d2997f77

2
.gitignore vendored

@ -140,3 +140,5 @@ dist
# aws-cdk # aws-cdk
.cdk.staging .cdk.staging
cdk.out cdk.out
db.json

@ -0,0 +1,30 @@
import { Low } from "lowdb";
import { JSONFile } from "lowdb/node";
export type Conversation = {
id: string;
title: string;
userId: string;
};
type DB = {
conversations: Array<Conversation>;
messages: Array<{
id: string;
conversationId: string;
content: string;
role: "user" | "assistant" | "system" | "data";
index: number;
createdAt: string;
runningSummary?: string;
}>;
};
export const db = new Low<DB>(new JSONFile("db.json"), {
conversations: [],
messages: [],
});
/** Initialize the database. Sets `db.data` to the default state if the file doesn't exist. */
await db.read();
/** Write the database to the file, in case it didn't exist before. */
await db.write();

@ -31,6 +31,8 @@
"hono": "^4.8.2", "hono": "^4.8.2",
"immer": "^10.1.1", "immer": "^10.1.1",
"kysely": "^0.28.2", "kysely": "^0.28.2",
"lowdb": "^7.0.1",
"nanoid": "^5.1.5",
"pg": "^8.16.3", "pg": "^8.16.3",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",

@ -17,13 +17,12 @@ import {
import { usePageContext } from "vike-react/usePageContext"; import { usePageContext } from "vike-react/usePageContext";
import { useData } from "vike-react/useData"; import { useData } from "vike-react/useData";
import type { Data } from "./+data"; import type { Data } from "./+data";
import type { ConversationsId } from "../../../database/generated/public/Conversations";
import type { CommittedMessage, DraftMessage } from "../../../types"; import type { CommittedMessage, DraftMessage } from "../../../types";
import Markdown from "react-markdown"; import Markdown from "react-markdown";
export default function ChatPage() { export default function ChatPage() {
const pageContext = usePageContext(); const pageContext = usePageContext();
const conversationId = Number(pageContext.routeParams.id) as ConversationsId; const conversationId = pageContext.routeParams.id;
const conversationTitle = useStore( const conversationTitle = useStore(
(state) => state.conversations.find((c) => c.id === conversationId)?.title, (state) => state.conversations.find((c) => c.id === conversationId)?.title,
); );
@ -126,7 +125,7 @@ export default function ChatPage() {
content: response.insertedAssistantMessage?.content, content: response.insertedAssistantMessage?.content,
index: response.insertedAssistantMessage?.index, index: response.insertedAssistantMessage?.index,
runningSummary: runningSummary:
response.insertedAssistantMessage?.running_summary || response.insertedAssistantMessage?.runningSummary ||
undefined, undefined,
} as CommittedMessage, } as CommittedMessage,
]; ];
@ -185,17 +184,18 @@ function Messages({
bdrs="md" bdrs="md"
> >
<div> <div>
{"index" in message ? message.index : ""} {"index" in message ? message.index : ""} {message.role}
{message.role}
</div> </div>
<Markdown>{message.content}</Markdown> <Markdown>{message.content}</Markdown>
{"runningSummary" in message && ( </Box>
{"runningSummary" in message && (
<Box w="75%" bd="dotted" p="md" bdrs="md">
<div> <div>
<strong>Running Summary:</strong> <strong>Running Summary:</strong>
<Markdown>{message.runningSummary}</Markdown> <Markdown>{message.runningSummary}</Markdown>
</div> </div>
)} </Box>
</Box> )}
</Group> </Group>
))} ))}
</Stack> </Stack>

@ -7,10 +7,10 @@ export const data = async (pageContext: PageContextServer) => {
const { id } = pageContext.routeParams; const { id } = pageContext.routeParams;
const caller = createCaller({}); const caller = createCaller({});
const conversation = await caller.fetchConversation({ const conversation = await caller.fetchConversation({
id: Number(id), id,
}); });
const messages = await caller.fetchMessages({ const messages = await caller.fetchMessages({
conversationId: Number(id), conversationId: id,
}); });
return { conversation, messages }; return { conversation, messages };
}; };

@ -17,9 +17,8 @@ import { env } from "../../server/env.js";
// ConsistencyLevelEnum, // ConsistencyLevelEnum,
// type NumberArrayId, // type NumberArrayId,
// } from "@zilliz/milvus2-sdk-node"; // } from "@zilliz/milvus2-sdk-node";
import { db } from "../../database/postgres"; import { db } from "../../database/lowdb";
import type { ConversationsId } from "../../database/generated/public/Conversations"; import { nanoid } from "nanoid";
import type { UsersId } from "../../database/generated/public/Users";
const mainSystemPrompt = ({ const mainSystemPrompt = ({
systemPrompt, systemPrompt,
@ -47,75 +46,64 @@ const openrouter = createOpenRouter({
export const chat = router({ export const chat = router({
listConversations: publicProcedure.query(async () => { listConversations: publicProcedure.query(async () => {
const rows = await db.selectFrom("conversations").selectAll().execute(); const rows = await db.data.conversations;
return rows; return rows;
}), }),
fetchConversation: publicProcedure fetchConversation: publicProcedure
.input((x) => x as { id: number }) .input((x) => x as { id: string })
.query(async ({ input: { id } }) => { .query(async ({ input: { id } }) => {
const row = await db const row = await db.data.conversations.find((c) => c.id === id);
.selectFrom("conversations")
.selectAll()
.where("id", "=", id as ConversationsId)
.executeTakeFirst();
return row; return row;
}), }),
createConversation: publicProcedure.mutation(async () => { createConversation: publicProcedure.mutation(async () => {
const title = "New Conversation"; const title = "New Conversation";
const row = await db const row = {
.insertInto("conversations") id: nanoid(),
.values({ title,
title, userId: "1",
user_id: 1 as UsersId, };
}) await db.data.conversations.push(row);
.returningAll() db.write();
.executeTakeFirst();
return row; return row;
}), }),
deleteConversation: publicProcedure deleteConversation: publicProcedure
.input((x) => x as { id: number }) .input((x) => x as { id: string })
.mutation(async ({ input: { id } }) => { .mutation(async ({ input: { id } }) => {
const result = await db await db.data.conversations.splice(
.deleteFrom("conversations") db.data.conversations.findIndex((c) => c.id === id),
.where("id", "=", Number(id) as ConversationsId) 1,
.execute(); );
return result; db.write();
return { ok: true };
}), }),
updateConversationTitle: publicProcedure updateConversationTitle: publicProcedure
.input( .input(
(x) => (x) =>
x as { x as {
id: number; id: string;
title: string; title: string;
}, },
) )
.mutation(async ({ input: { id, title } }) => { .mutation(async ({ input: { id, title } }) => {
const result = await db const conversation = await db.data.conversations.find((c) => c.id === id);
.updateTable("conversations") if (!conversation) throw new Error("Conversation not found");
.set({ title }) conversation.title = title;
.where("id", "=", Number(id) as ConversationsId) db.write();
.execute(); return { ok: true };
return result[0];
}), }),
fetchMessages: publicProcedure fetchMessages: publicProcedure
.input((x) => x as { conversationId: number }) .input((x) => x as { conversationId: string })
.query(async ({ input: { conversationId } }) => { .query(async ({ input: { conversationId } }) => {
const rows = await db const rows = await db.data.messages.filter(
.selectFrom("messages") (m) => m.conversationId === conversationId,
.selectAll() );
.where("conversation_id", "=", conversationId as ConversationsId) return rows as Array<CommittedMessage>;
.execute();
return rows.map((row) => ({
...row,
conversationId: conversationId as ConversationsId,
runningSummary: row.running_summary,
})) as Array<CommittedMessage>;
}), }),
sendMessage: publicProcedure sendMessage: publicProcedure
.input( .input(
(x) => (x) =>
x as { x as {
conversationId: number; conversationId: string;
messages: Array<DraftMessage | CommittedMessage>; messages: Array<DraftMessage | CommittedMessage>;
systemPrompt: string; systemPrompt: string;
parameters: OtherParameters; parameters: OtherParameters;
@ -140,17 +128,17 @@ export const chat = router({
.runningSummary as string) .runningSummary as string)
: ""; : "";
/** Save the incoming message to the database. */ /** Save the incoming message to the database. */
const insertedUserMessage = await db const insertedUserMessage: CommittedMessage = {
.insertInto("messages") id: nanoid(),
.values({ conversationId,
conversation_id: conversationId as ConversationsId, content: messages[messages.length - 1].content,
content: messages[messages.length - 1].content, role: "user" as const,
role: "user" as const, index: messages.length - 1,
index: messages.length - 1, createdAt: new Date().toISOString(),
created_at: new Date().toISOString(), };
}) await db.data.messages.push(insertedUserMessage);
.returning(["id", "index"]) // do not db.write() until the end
.executeTakeFirst();
/** Generate a new message from the model, but hold-off on adding it to /** Generate a new message from the model, but hold-off on adding it to
* the database until we produce the associated running-summary, below. * the database until we produce the associated running-summary, below.
* The model should be given the conversation summary thus far, and of * The model should be given the conversation summary thus far, and of
@ -249,18 +237,17 @@ export const chat = router({
tools: undefined, tools: undefined,
...parameters, ...parameters,
}); });
const insertedAssistantMessage = await db const insertedAssistantMessage: CommittedMessage = {
.insertInto("messages") id: nanoid(),
.values({ conversationId,
conversation_id: conversationId as ConversationsId, content: mainResponse.text,
content: mainResponse.text, runningSummary: runningSummaryResponse.text,
running_summary: runningSummaryResponse.text, role: "assistant" as const,
role: "assistant" as const, index: messages.length,
index: messages.length, createdAt: new Date().toISOString(),
created_at: new Date().toISOString(), };
}) await db.data.messages.push(insertedAssistantMessage);
.returningAll() await db.write();
.executeTakeFirst();
/** TODO: notify the caller, somehow, that some messages were saved to /** TODO: notify the caller, somehow, that some messages were saved to
* the database and/or were outfitted with runningSummaries, so the * the database and/or were outfitted with runningSummaries, so the
* caller can update its UI state. */ * caller can update its UI state. */

@ -68,6 +68,12 @@ importers:
kysely: kysely:
specifier: ^0.28.2 specifier: ^0.28.2
version: 0.28.2 version: 0.28.2
lowdb:
specifier: ^7.0.1
version: 7.0.1
nanoid:
specifier: ^5.1.5
version: 5.1.5
pg: pg:
specifier: ^8.16.3 specifier: ^8.16.3
version: 8.16.3 version: 8.16.3
@ -1903,6 +1909,10 @@ packages:
longest-streak@3.1.0: longest-streak@3.1.0:
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
lowdb@7.0.1:
resolution: {integrity: sha512-neJAj8GwF0e8EpycYIDFqEPcx9Qz4GUho20jWFR7YiFeXzF1YMLdxB36PypcTSPMA+4+LvgyMacYhlr18Zlymw==}
engines: {node: '>=18'}
lru-cache@10.4.3: lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
@ -2071,6 +2081,11 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true hasBin: true
nanoid@5.1.5:
resolution: {integrity: sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==}
engines: {node: ^18 || >=20}
hasBin: true
node-releases@2.0.19: node-releases@2.0.19:
resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
@ -2682,6 +2697,10 @@ packages:
stacktracey@2.1.8: stacktracey@2.1.8:
resolution: {integrity: sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==} resolution: {integrity: sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==}
steno@4.0.2:
resolution: {integrity: sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A==}
engines: {node: '>=18'}
stoppable@1.1.0: stoppable@1.1.0:
resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==} resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==}
engines: {node: '>=4', npm: '>=6'} engines: {node: '>=4', npm: '>=6'}
@ -4688,6 +4707,10 @@ snapshots:
longest-streak@3.1.0: {} longest-streak@3.1.0: {}
lowdb@7.0.1:
dependencies:
steno: 4.0.2
lru-cache@10.4.3: {} lru-cache@10.4.3: {}
lru-cache@11.1.0: {} lru-cache@11.1.0: {}
@ -4981,6 +5004,8 @@ snapshots:
nanoid@3.3.11: {} nanoid@3.3.11: {}
nanoid@5.1.5: {}
node-releases@2.0.19: {} node-releases@2.0.19: {}
normalize-range@0.1.2: {} normalize-range@0.1.2: {}
@ -5606,6 +5631,8 @@ snapshots:
as-table: 1.0.55 as-table: 1.0.55
get-source: 2.0.12 get-source: 2.0.12
steno@4.0.2: {}
stoppable@1.1.0: {} stoppable@1.1.0: {}
string-width@4.2.3: string-width@4.2.3:

@ -11,7 +11,7 @@ export const defaultParameters = {
export const useStore = create<Store>()( export const useStore = create<Store>()(
immer((set, get) => ({ immer((set, get) => ({
selectedConversationId: 0 as ConversationsId, selectedConversationId: "",
conversations: [], conversations: [],
messages: [], messages: [],
message: "", message: "",

@ -1,32 +1,29 @@
import type { Message as UIMessage } from "ai"; import type { Message as UIMessage } from "ai";
import type { generateText } from "ai"; import type { generateText } from "ai";
import type { import type { Conversation } from "./database/lowdb.js";
Conversations,
ConversationsId,
} from "./database/generated/public/Conversations";
export type OtherParameters = Omit< export type OtherParameters = Omit<
Parameters<typeof generateText>[0], Parameters<typeof generateText>[0],
"model" | "messages" | "abortSignal" "model" | "messages" | "abortSignal"
>; >;
export type ConversationUI = Conversations & {}; export type ConversationUI = Conversation & {};
export type Store = { export type Store = {
/** This is a string because Milvus sends it as a string, and the value /** This is a string because Milvus sends it as a string, and the value
* overflows the JS integer anyway. */ * overflows the JS integer anyway. */
selectedConversationId: ConversationsId; selectedConversationId: string;
conversations: Array<ConversationUI>; conversations: Array<ConversationUI>;
messages: Array<DraftMessage | CommittedMessage>; messages: Array<DraftMessage | CommittedMessage>;
message: string; message: string;
systemPrompt: string; systemPrompt: string;
parameters: OtherParameters; parameters: OtherParameters;
loading: boolean; loading: boolean;
setConversationId: (conversationId: ConversationsId) => void; setConversationId: (conversationId: string) => void;
setConversationTitle: (conversationTitle: string) => void; setConversationTitle: (conversationTitle: string) => void;
setConversations: (conversations: Array<ConversationUI>) => void; setConversations: (conversations: Array<ConversationUI>) => void;
addConversation: (conversation: ConversationUI) => void; addConversation: (conversation: ConversationUI) => void;
removeConversation: (conversationId: ConversationsId) => void; removeConversation: (conversationId: string) => void;
setMessages: (messages: Array<DraftMessage | CommittedMessage>) => void; setMessages: (messages: Array<DraftMessage | CommittedMessage>) => void;
setMessage: (message: string) => void; setMessage: (message: string) => void;
setSystemPrompt: (systemPrompt: string) => void; setSystemPrompt: (systemPrompt: string) => void;
@ -35,11 +32,12 @@ export type Store = {
}; };
/** The message while it's being typed in the input box. */ /** The message while it's being typed in the input box. */
export type DraftMessage = Omit<UIMessage, "id">; export type DraftMessage = Omit<UIMessage, "id" | "createdAt">;
/** The message after it's been saved to the database. */ /** The message after it's been saved to the database. */
export type CommittedMessage = DraftMessage & { export type CommittedMessage = DraftMessage & {
id: number; id: string;
conversationId: ConversationsId; conversationId: string;
index: number; index: number;
runningSummary?: string; runningSummary?: string;
createdAt: string;
}; };

Loading…
Cancel
Save