setup postgres and kyseley for most data instead of milvus

This commit is contained in:
Avraham Sakal
2025-07-13 20:14:53 -04:00
parent 511f05af83
commit 7a9f0c956c
27 changed files with 1338 additions and 169 deletions
+2 -2
View File
@@ -10,8 +10,8 @@ export default {
Layout,
// https://vike.dev/head-tags
title: "My Vike App",
description: "Demo showcasing Vike",
title: "Trainable AI",
description: "The Chatbot that Remembers",
passToClient: ["user"],
extends: vikeReact,
-112
View File
@@ -1,112 +0,0 @@
import { JsonInput, Tabs, Textarea } from "@mantine/core";
import { trpc } from "../../trpc/client";
import { create } from "zustand";
import type { Message as UIMessage } from "ai";
import type { OtherParameters, Store } from "./types.js";
const defaultSystemPrompt = `You are a helpful assistant that answers questions based on the provided context. If you don't know the answer, just say that you don't know, don't try to make up an answer.`;
const defaultParameters = {
temperature: 0.5,
max_tokens: 100,
} as OtherParameters;
const useStore = create<Store>()((set) => ({
messages: [],
message: "",
systemPrompt: defaultSystemPrompt,
parameters: defaultParameters,
loading: false,
setMessages: (messages) => set({ messages }),
setMessage: (message) => set({ message }),
setSystemPrompt: (systemPrompt) => set({ systemPrompt }),
setParameters: (parameters) => set({ parameters }),
setLoading: (loading) => set({ loading }),
}));
export default function ChatPage() {
const messages = useStore((state) => state.messages);
const message = useStore((state) => state.message);
const systemPrompt = useStore((state) => state.systemPrompt);
const parameters = useStore((state) => state.parameters);
const loading = useStore((state) => state.loading);
const setMessages = useStore((state) => state.setMessages);
const setMessage = useStore((state) => state.setMessage);
const setSystemPrompt = useStore((state) => state.setSystemPrompt);
const setParameters = useStore((state) => state.setParameters);
const setLoading = useStore((state) => state.setLoading);
return (
<Tabs defaultValue="message">
<Tabs.List>
<Tabs.Tab value="message">Message</Tabs.Tab>
<Tabs.Tab value="system-prompt">System Prompt</Tabs.Tab>
<Tabs.Tab value="parameters">Parameters</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="message">
<Messages messages={messages} />
<Textarea
resize="vertical"
placeholder="Type your message here..."
value={message}
disabled={loading}
onChange={(e) => setMessage(e.target.value)}
onKeyDown={async (e) => {
if (e.key === "Enter") {
e.preventDefault();
const messagesWithNewUserMessage = [
...messages,
{ role: "user" as const, content: message } as UIMessage,
];
setMessages(messagesWithNewUserMessage);
setLoading(true);
const response = await trpc.chat.sendMessage.query({
messages: messagesWithNewUserMessage,
systemPrompt,
parameters,
});
const messagesWithAssistantMessage = [
...messagesWithNewUserMessage,
{ role: "assistant", content: response.text } as UIMessage,
];
setMessages(messagesWithAssistantMessage);
setMessage("");
setLoading(false);
}
}}
/>
</Tabs.Panel>
<Tabs.Panel value="system-prompt">
<Textarea
resize="vertical"
placeholder={defaultSystemPrompt}
value={systemPrompt}
onChange={(e) => setSystemPrompt(e.target.value)}
/>
</Tabs.Panel>
<Tabs.Panel value="parameters">
<JsonInput
resize="vertical"
formatOnBlur
placeholder={JSON.stringify(defaultParameters)}
value={JSON.stringify(parameters)}
onChange={(value) => setParameters(JSON.parse(value))}
/>
</Tabs.Panel>
</Tabs>
);
}
function Messages({
messages,
}: {
messages: Array<UIMessage>;
}) {
return (
<div>
{messages.map((message, index) => (
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
<div key={index}>{message.content}</div>
))}
</div>
);
}
+142
View File
@@ -0,0 +1,142 @@
import { JsonInput, Tabs, Textarea } from "@mantine/core";
import { trpc } from "../../../trpc/client";
import type { Message as UIMessage } from "ai";
import { useEffect } from "react";
import {
defaultParameters,
defaultSystemPrompt,
useStore,
} from "../../../state";
import { usePageContext } from "vike-react/usePageContext";
import { useData } from "vike-react/useData";
import type { Data } from "./+data";
import type { ConversationsId } from "../../../database/generated/public/Conversations";
export default function ChatPage() {
const pageContext = usePageContext();
const conversationId = Number(pageContext.routeParams.id) as ConversationsId;
const conversationTitle = useStore((state) => state.conversationTitle);
const messages = useStore((state) => state.messages);
const message = useStore((state) => state.message);
const systemPrompt = useStore((state) => state.systemPrompt);
const parameters = useStore((state) => state.parameters);
const loading = useStore((state) => state.loading);
const setConversationId = useStore((state) => state.setConversationId);
const setConversationTitle = useStore((state) => state.setConversationTitle);
const setMessages = useStore((state) => state.setMessages);
const setMessage = useStore((state) => state.setMessage);
const setSystemPrompt = useStore((state) => state.setSystemPrompt);
const setParameters = useStore((state) => state.setParameters);
const setLoading = useStore((state) => state.setLoading);
const conversation = useData<Data>();
useEffect(() => {
setConversationId(conversationId);
}, [conversationId, setConversationId]);
useEffect(() => {
if (conversation?.id && conversation?.title) {
setConversationId(conversation.id);
setConversationTitle(conversation.title);
}
}, [
conversation?.id,
conversation?.title,
setConversationId,
setConversationTitle,
]);
return (
<>
<div>
<span>Conversation #{conversationId} - </span>
<input
type="text"
value={conversationTitle}
onChange={(e) => {
setConversationTitle(e.target.value);
}}
onBlur={(e) => {
trpc.chat.updateConversationTitle.mutate({
id: conversationId,
title: e.target.value,
});
}}
/>
</div>
<Tabs defaultValue="message">
<Tabs.List>
<Tabs.Tab value="message">Message</Tabs.Tab>
<Tabs.Tab value="system-prompt">System Prompt</Tabs.Tab>
<Tabs.Tab value="parameters">Parameters</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="message">
<Messages messages={messages} />
<Textarea
resize="vertical"
placeholder="Type your message here..."
value={message}
disabled={loading}
onChange={(e) => setMessage(e.target.value)}
onKeyDown={async (e) => {
if (e.key === "Enter") {
e.preventDefault();
const messagesWithNewUserMessage = [
...messages,
{ role: "user" as const, content: message } as UIMessage,
];
setMessages(messagesWithNewUserMessage);
setLoading(true);
const response = await trpc.chat.sendMessage.query({
messages: messagesWithNewUserMessage,
systemPrompt,
parameters,
});
const messagesWithAssistantMessage = [
...messagesWithNewUserMessage,
{ role: "assistant", content: response.text } as UIMessage,
];
setMessages(messagesWithAssistantMessage);
setMessage("");
setLoading(false);
}
}}
/>
</Tabs.Panel>
<Tabs.Panel value="system-prompt">
<Textarea
resize="vertical"
placeholder={defaultSystemPrompt}
value={systemPrompt}
onChange={(e) => setSystemPrompt(e.target.value)}
/>
</Tabs.Panel>
<Tabs.Panel value="parameters">
<JsonInput
resize="vertical"
formatOnBlur
placeholder={JSON.stringify(defaultParameters)}
value={JSON.stringify(parameters)}
onChange={(value) => setParameters(JSON.parse(value))}
/>
</Tabs.Panel>
</Tabs>
</>
);
}
function Messages({
messages,
}: {
messages: Array<UIMessage>;
}) {
return (
<div>
{messages.map((message, index) => (
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
<div key={index}>{message.content}</div>
))}
</div>
);
}
+13
View File
@@ -0,0 +1,13 @@
import type { PageContextServer } from "vike/types";
import { createCaller } from "../trpc.js";
export type Data = Awaited<ReturnType<typeof data>>;
export const data = async (pageContext: PageContextServer) => {
const { id } = pageContext.routeParams;
const caller = createCaller({});
const conversation = await caller.fetchConversation({
id: Number(id),
});
return conversation;
};
+2 -3
View File
@@ -3,9 +3,8 @@ The system begins with a generic system prompt.
The user begins interacting with the model, perhaps introducing himself. Perhaps the initial UI should contain a pre-filled message as if it's from the model, saying "Hi, I'm {name}. Tell me about yourself or what you want me to do."
Every time the user submits a message, the backend should:
* Save the message to the database under a conversation key for later lookup
* Generate a new message from the model, and add it to the database. The model should be
* Save the message to the database under a conversation id for later lookup
* Generate a new message from the model, and add it to the database. The model should be given the conversation summary thus far, and of course the user's latest message, unmodified. Invite the model to create any tools it needs. The tool needs to be implemented in a language which this system can execute; usually an interpretted language like Python or JavaScript.
* Extract Facts from the user's message, and add them to the database, linking the Facts with the messages they came from. (Yes, this should be done *after* the model response, not before; because when we run a query to find Facts to inject into the context sent to the model, we don't want Facts from the user's current message to be candidates for injection, because we're sending the user's message unadulterated to the model; there's no reason to inject the same Facts that the model is already using to generate its response.)
* Extract Facts from the model's response, and add them to the database, linking the Facts with the messages they came from.
* For each Fact produced in the two fact-extraction steps, generate FactTriggers and add them to the database, linking the FactTriggers with the Facts they came from. A FactTrigger is a natural language phrase that describes a situation in which it would be useful to invoke the Fact. (e.g., "When food preferences are discussed").
+64 -2
View File
@@ -1,20 +1,80 @@
import {
router,
publicProcedure,
createCallerFactory,
Validator,
// Validator
} from "../../trpc/server";
// import { Type as T } from "@sinclair/typebox";
import { createOpenRouter } from "@openrouter/ai-sdk-provider";
import { generateText } from "ai";
import type { Message as UIMessage } from "ai";
import type { OtherParameters } from "./types.js";
import type { OtherParameters } from "../../types.js";
import { env } from "../../server/env.js";
// import { client } from "../../database/milvus";
// import {
// ConsistencyLevelEnum,
// type NumberArrayId,
// } from "@zilliz/milvus2-sdk-node";
import { db } from "../../database/postgres";
import type { ConversationsId } from "../../database/generated/public/Conversations";
import type { UsersId } from "../../database/generated/public/Users";
const openrouter = createOpenRouter({
apiKey: env.OPENROUTER_API_KEY,
});
export const chat = router({
listConversations: publicProcedure.query(async () => {
const rows = await db.selectFrom("conversations").selectAll().execute();
return rows;
}),
fetchConversation: publicProcedure
.input((x) => x as { id: number })
.query(async ({ input: { id } }) => {
const row = await db
.selectFrom("conversations")
.selectAll()
.where("id", "=", id as ConversationsId)
.executeTakeFirst();
return row;
}),
createConversation: publicProcedure.mutation(async () => {
const title = "New Conversation";
const row = await db
.insertInto("conversations")
.values({
title,
user_id: 1 as UsersId,
})
.returningAll()
.executeTakeFirst();
return row;
}),
deleteConversation: publicProcedure
.input((x) => x as { id: number })
.mutation(async ({ input: { id } }) => {
const result = await db
.deleteFrom("conversations")
.where("id", "=", id as ConversationsId)
.execute();
return result;
}),
updateConversationTitle: publicProcedure
.input(
(x) =>
x as {
id: number;
title: string;
},
)
.mutation(async ({ input: { id, title } }) => {
const result = await db
.updateTable("conversations")
.set({ title })
.where("id", "=", id as ConversationsId)
.execute();
return result[0];
}),
sendMessage: publicProcedure
.input(
(x) =>
@@ -38,3 +98,5 @@ export const chat = router({
return response;
}),
});
export const createCaller = createCallerFactory(chat);
-20
View File
@@ -1,20 +0,0 @@
import type { Message as UIMessage } from "ai";
import type { generateText } from "ai";
export type OtherParameters = Omit<
Parameters<typeof generateText>[0],
"model" | "messages" | "abortSignal"
>;
export type Store = {
messages: Array<UIMessage>;
message: string;
systemPrompt: string;
parameters: OtherParameters;
loading: boolean;
setMessages: (messages: Array<UIMessage>) => void;
setMessage: (message: string) => void;
setSystemPrompt: (systemPrompt: string) => void;
setParameters: (parameters: OtherParameters) => void;
setLoading: (loading: boolean) => void;
};