setup postgres and kyseley for most data instead of milvus

master
Avraham Sakal 3 months ago
parent 511f05af83
commit 7a9f0c956c

@ -0,0 +1,11 @@
const { makeKyselyHook } = require("kanel-kysely");
module.exports = {
connection: "postgres://neondb_owner:npg_sOVmj8vWq2zG@ep-withered-king-adiz9gpi-pooler.c-2.us-east-1.aws.neon.tech:5432/neondb?sslmode=require&channel_binding=true",
enumStyle: "type",
outputPath: "./database/generated",
preRenderHooks: [makeKyselyHook()],
customTypeMap: {
"pg_catalog.timestamp": "string",
}
};

@ -1,10 +0,0 @@
import { usePageContext } from "vike-react/usePageContext";
import { NavLink } from "@mantine/core";
export function Link({ href, label }: { href: string; label: string }) {
const pageContext = usePageContext();
const { urlPathname } = pageContext;
const isActive =
href === "/" ? urlPathname === href : urlPathname.startsWith(href);
return <NavLink href={href} label={label} active={isActive} />;
}

@ -0,0 +1,8 @@
// @generated
// This file is automatically generated by Kanel. Do not modify manually.
import type { default as PublicSchema } from './public/PublicSchema';
type Database = PublicSchema;
export default Database;

@ -0,0 +1,25 @@
// @generated
// This file is automatically generated by Kanel. Do not modify manually.
import type { UsersId } from './Users';
import type { ColumnType, Selectable, Insertable, Updateable } from 'kysely';
/** Identifier type for public.conversations */
export type ConversationsId = number & { __brand: 'public.conversations' };
/** Represents the table public.conversations */
export default interface ConversationsTable {
id: ColumnType<ConversationsId, never, never>;
title: ColumnType<string | null, string | null, string | null>;
created_at: ColumnType<string | null, string | null, string | null>;
user_id: ColumnType<UsersId | null, UsersId | null, UsersId | null>;
}
export type Conversations = Selectable<ConversationsTable>;
export type NewConversations = Insertable<ConversationsTable>;
export type ConversationsUpdate = Updateable<ConversationsTable>;

@ -0,0 +1,32 @@
// @generated
// This file is automatically generated by Kanel. Do not modify manually.
import type { FactsId } from './Facts';
import type { ConversationsId } from './Conversations';
import type { ColumnType, Selectable, Insertable, Updateable } from 'kysely';
/** Identifier type for public.fact_triggers */
export type FactTriggersId = number & { __brand: 'public.fact_triggers' };
/** Represents the table public.fact_triggers */
export default interface FactTriggersTable {
id: ColumnType<FactTriggersId, never, never>;
fact_id: ColumnType<FactsId | null, FactsId | null, FactsId | null>;
trigger_phrase: ColumnType<string | null, string | null, string | null>;
priority_multiplier: ColumnType<number | null, number | null, number | null>;
priority_multiplier_reason: ColumnType<string | null, string | null, string | null>;
scope_conversation_id: ColumnType<ConversationsId | null, ConversationsId | null, ConversationsId | null>;
created_at: ColumnType<string | null, string | null, string | null>;
}
export type FactTriggers = Selectable<FactTriggersTable>;
export type NewFactTriggers = Insertable<FactTriggersTable>;
export type FactTriggersUpdate = Updateable<FactTriggersTable>;

@ -0,0 +1,28 @@
// @generated
// This file is automatically generated by Kanel. Do not modify manually.
import type { UsersId } from './Users';
import type { MessagesId } from './Messages';
import type { ColumnType, Selectable, Insertable, Updateable } from 'kysely';
/** Identifier type for public.facts */
export type FactsId = number & { __brand: 'public.facts' };
/** Represents the table public.facts */
export default interface FactsTable {
id: ColumnType<FactsId, never, never>;
user_id: ColumnType<UsersId | null, UsersId | null, UsersId | null>;
source_message_id: ColumnType<MessagesId | null, MessagesId | null, MessagesId | null>;
content: ColumnType<string | null, string | null, string | null>;
created_at: ColumnType<string | null, string | null, string | null>;
}
export type Facts = Selectable<FactsTable>;
export type NewFacts = Insertable<FactsTable>;
export type FactsUpdate = Updateable<FactsTable>;

@ -0,0 +1,32 @@
// @generated
// This file is automatically generated by Kanel. Do not modify manually.
import type { ConversationsId } from './Conversations';
import type { default as Role } from './Role';
import type { ColumnType, Selectable, Insertable, Updateable } from 'kysely';
/** Identifier type for public.messages */
export type MessagesId = number & { __brand: 'public.messages' };
/** Represents the table public.messages */
export default interface MessagesTable {
id: ColumnType<MessagesId, never, never>;
conversation_id: ColumnType<ConversationsId | null, ConversationsId | null, ConversationsId | null>;
index: ColumnType<number | null, number | null, number | null>;
content: ColumnType<string | null, string | null, string | null>;
running_summary: ColumnType<string | null, string | null, string | null>;
created_at: ColumnType<string | null, string | null, string | null>;
role: ColumnType<Role | null, Role | null, Role | null>;
}
export type Messages = Selectable<MessagesTable>;
export type NewMessages = Insertable<MessagesTable>;
export type MessagesUpdate = Updateable<MessagesTable>;

@ -0,0 +1,23 @@
// @generated
// This file is automatically generated by Kanel. Do not modify manually.
import type { default as UsersTable } from './Users';
import type { default as MessagesTable } from './Messages';
import type { default as ToolsTable } from './Tools';
import type { default as FactTriggersTable } from './FactTriggers';
import type { default as FactsTable } from './Facts';
import type { default as ConversationsTable } from './Conversations';
export default interface PublicSchema {
users: UsersTable;
messages: MessagesTable;
tools: ToolsTable;
fact_triggers: FactTriggersTable;
facts: FactsTable;
conversations: ConversationsTable;
}

@ -0,0 +1,10 @@
// @generated
// This file is automatically generated by Kanel. Do not modify manually.
/** Represents the enum public.role */
type Role =
| 'user'
| 'assistant'
| 'system';
export default Role;

@ -0,0 +1,36 @@
// @generated
// This file is automatically generated by Kanel. Do not modify manually.
import type { UsersId } from './Users';
import type { MessagesId } from './Messages';
import type { ColumnType, Selectable, Insertable, Updateable } from 'kysely';
/** Identifier type for public.tools */
export type ToolsId = number & { __brand: 'public.tools' };
/** Represents the table public.tools */
export default interface ToolsTable {
id: ColumnType<ToolsId, never, never>;
user_id: ColumnType<UsersId | null, UsersId | null, UsersId | null>;
source_message_id: ColumnType<MessagesId | null, MessagesId | null, MessagesId | null>;
name: ColumnType<string | null, string | null, string | null>;
description: ColumnType<string | null, string | null, string | null>;
parameter_schema: ColumnType<unknown | null, unknown | null, unknown | null>;
implementation_language: ColumnType<string | null, string | null, string | null>;
implementation_code: ColumnType<string | null, string | null, string | null>;
created_at: ColumnType<string | null, string | null, string | null>;
}
export type Tools = Selectable<ToolsTable>;
export type NewTools = Insertable<ToolsTable>;
export type ToolsUpdate = Updateable<ToolsTable>;

@ -0,0 +1,28 @@
// @generated
// This file is automatically generated by Kanel. Do not modify manually.
import type { ColumnType, Selectable, Insertable, Updateable } from 'kysely';
/** Identifier type for public.users */
export type UsersId = number & { __brand: 'public.users' };
/** Represents the table public.users */
export default interface UsersTable {
id: ColumnType<UsersId, never, never>;
username: ColumnType<string | null, string | null, string | null>;
password: ColumnType<string | null, string | null, string | null>;
email: ColumnType<string | null, string | null, string | null>;
last_login: ColumnType<string | null, string | null, string | null>;
created_at: ColumnType<string | null, string | null, string | null>;
}
export type Users = Selectable<UsersTable>;
export type NewUsers = Insertable<UsersTable>;
export type UsersUpdate = Updateable<UsersTable>;

@ -24,16 +24,16 @@ async function initialize() {
console.log("Creating collection: facts"); console.log("Creating collection: facts");
await client.createCollection({ await client.createCollection({
collection_name: "facts", collection_name: "facts",
auto_id: true,
fields: [ fields: [
{ {
name: "id", name: "id",
data_type: DataType.Int64, data_type: DataType.Int64,
is_primary_key: true, is_primary_key: true,
autoID: true,
}, },
{ {
name: "user_id", name: "user_id",
data_type: DataType.Int64, data_type: DataType.Int32,
description: description:
"Foreign key linking to the Users Collection. Crucial if you have multiple users.", "Foreign key linking to the Users Collection. Crucial if you have multiple users.",
}, },
@ -78,12 +78,12 @@ async function initialize() {
console.log("Creating collection: fact_triggers"); console.log("Creating collection: fact_triggers");
await client.createCollection({ await client.createCollection({
collection_name: "fact_triggers", collection_name: "fact_triggers",
auto_id: true,
fields: [ fields: [
{ {
name: "id", name: "id",
data_type: DataType.Int64, data_type: DataType.Int64,
is_primary_key: true, is_primary_key: true,
autoID: true,
}, },
{ {
name: "fact_id", name: "fact_id",
@ -140,16 +140,16 @@ async function initialize() {
console.log("Creating collection: conversations"); console.log("Creating collection: conversations");
await client.createCollection({ await client.createCollection({
collection_name: "conversations", collection_name: "conversations",
auto_id: true,
fields: [ fields: [
{ {
name: "id", name: "id",
data_type: DataType.Int64, data_type: DataType.Int64,
is_primary_key: true, is_primary_key: true,
autoID: true,
}, },
{ {
name: "user_id", name: "user_id",
data_type: DataType.Int64, data_type: DataType.Int32,
description: description:
"Foreign key linking to the Users Collection. This is the user who sent this message.", "Foreign key linking to the Users Collection. This is the user who sent this message.",
}, },
@ -186,16 +186,16 @@ async function initialize() {
console.log("Creating collection: conversation_messages"); console.log("Creating collection: conversation_messages");
await client.createCollection({ await client.createCollection({
collection_name: "conversation_messages", collection_name: "conversation_messages",
auto_id: true,
fields: [ fields: [
{ {
name: "id", name: "id",
data_type: DataType.Int64, data_type: DataType.Int64,
is_primary_key: true, is_primary_key: true,
autoID: true,
}, },
{ {
name: "user_id", name: "user_id",
data_type: DataType.Int64, data_type: DataType.Int32,
description: description:
"Foreign key linking to the Users Collection. This is the user who sent this message.", "Foreign key linking to the Users Collection. This is the user who sent this message.",
}, },
@ -268,19 +268,19 @@ async function initialize() {
console.log("Creating collection: tools"); console.log("Creating collection: tools");
await client.createCollection({ await client.createCollection({
collection_name: "tools", collection_name: "tools",
auto_id: true,
fields: [ fields: [
/** Primary key, unique identifier for each fact. */ /** Primary key, unique identifier for each fact. */
{ {
name: "id", name: "id",
data_type: DataType.Int64, data_type: DataType.Int64,
is_primary_key: true, is_primary_key: true,
autoID: true,
}, },
/** Foreign key linking to the Users Collection. /** Foreign key linking to the Users Collection.
* Crucial if you have multiple users. */ * Crucial if you have multiple users. */
{ {
name: "user_id", name: "user_id",
data_type: DataType.Int64, data_type: DataType.Int32,
description: description:
"Foreign key linking to the Users Collection. Crucial if you have multiple users.", "Foreign key linking to the Users Collection. Crucial if you have multiple users.",
}, },
@ -362,12 +362,12 @@ async function initialize() {
// console.log("Creating collection: users"); // console.log("Creating collection: users");
// await client.createCollection({ // await client.createCollection({
// collection_name: "users", // collection_name: "users",
// auto_id: true,
// fields: [ // fields: [
// { // {
// name: "id", // name: "id",
// data_type: DataType.Int64, // data_type: DataType.Int32,
// is_primary_key: true, // is_primary_key: true,
// autoID: true,
// }, // },
// { // {
// name: "username", // name: "username",

@ -0,0 +1,21 @@
import { Pool } from "pg";
import { Kysely, PostgresDialect } from "kysely";
import type Database from "./generated/Database";
export const pool = new Pool({
connectionString:
"postgres://neondb_owner:npg_sOVmj8vWq2zG@ep-withered-king-adiz9gpi-pooler.c-2.us-east-1.aws.neon.tech:5432/neondb?sslmode=require&channel_binding=true",
// channelBinding: require ?
});
const dialect = new PostgresDialect({
pool,
});
// Database interface is passed to Kysely's constructor, and from now on, Kysely
// knows your database structure.
// Dialect is passed to Kysely's constructor, and from now on, Kysely knows how
// to communicate with your database.
export const db = new Kysely<Database>({
dialect,
});

@ -1,15 +1,62 @@
import "@mantine/core/styles.css"; import "@mantine/core/styles.css";
import { AppShell, Burger, Group, Image, MantineProvider } from "@mantine/core"; import {
AppShell,
Burger,
Group,
Image,
MantineProvider,
NavLink,
} from "@mantine/core";
import {
IconHome2,
IconChevronRight,
IconActivity,
IconTrash,
IconCircle,
IconCircleFilled,
IconTrashFilled,
} from "@tabler/icons-react";
import { useDisclosure } from "@mantine/hooks"; import { useDisclosure } from "@mantine/hooks";
import theme from "./theme.js"; import theme from "./theme.js";
import logoUrl from "../assets/logo.svg"; import logoUrl from "../assets/logo.svg";
import { Link } from "../components/Link"; import { useStore } from "../state.js";
import { useEffect } from "react";
import { trpc } from "../trpc/client.js";
import { usePageContext } from "vike-react/usePageContext";
import "./hover.css";
import type { ConversationsId } from "../database/generated/public/Conversations.js";
export default function LayoutDefault({ export default function LayoutDefault({
children, children,
}: { children: React.ReactNode }) { }: { children: React.ReactNode }) {
const pageContext = usePageContext();
const { urlPathname } = pageContext;
const [opened, { toggle }] = useDisclosure(); const [opened, { toggle }] = useDisclosure();
const conversations = useStore((state) => state.conversations);
const setConversations = useStore((state) => state.setConversations);
const addConversation = useStore((state) => state.addConversation);
const removeConversation = useStore((state) => state.removeConversation);
const conversationId = useStore((state) => state.conversationId);
useEffect(() => {
trpc.chat.listConversations.query().then((res) => {
setConversations(res);
});
}, [setConversations]);
// useEffect(() => {
// if (isConversationListExpanded) {
// trpc.chat.listConversations.query().then((res) => {
// setConversations(res);
// });
// }
// }, [isConversationListExpanded]);
function handleDeleteConversation(conversationId: ConversationsId) {
removeConversation(conversationId);
trpc.chat.deleteConversation.mutate({ id: conversationId });
}
return ( return (
<MantineProvider theme={theme}> <MantineProvider theme={theme}>
<AppShell <AppShell
@ -36,10 +83,77 @@ export default function LayoutDefault({
</Group> </Group>
</AppShell.Header> </AppShell.Header>
<AppShell.Navbar p="md"> <AppShell.Navbar p="md">
<Link href="/" label="Welcome" /> <NavLink href="/" label="Welcome" active={urlPathname === "/"} />
<Link href="/todo" label="Todo" /> <NavLink href="/todo" label="Todo" active={urlPathname === "/todo"} />
<Link href="/star-wars" label="Data Fetching" /> <NavLink
<Link href="/chat" label="Chat" /> href="/star-wars"
label="Data Fetching"
active={urlPathname.startsWith("/star-wars")}
/>
<NavLink
key="chat-new"
href="#required-for-focus-management"
label="Chats"
leftSection={<IconActivity size={16} stroke={1.5} />}
rightSection={
<IconChevronRight
size={12}
stroke={1.5}
className="mantine-rotate-rtl"
/>
}
variant="subtle"
active={urlPathname.startsWith("/chat")}
>
{conversations.map((conversation) => (
<NavLink
key={conversation.id}
href={`/chat/${conversation.id}`}
label={conversation.title}
className="hover-container"
leftSection={
<>
<IconCircle
size={16}
stroke={1.5}
className="show-by-default"
/>
<IconCircleFilled
size={16}
stroke={1.5}
className="show-on-hover"
/>
</>
}
rightSection={
<>
<IconTrash
size={16}
stroke={1.5}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
handleDeleteConversation(conversation.id);
}}
className="show-by-default"
/>
<IconTrashFilled
size={16}
stroke={1.5}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
handleDeleteConversation(conversation.id);
}}
className="show-on-hover border-on-hover"
/>
</>
}
variant="subtle"
active={conversation.id === conversationId}
/>
))}
</NavLink>
</AppShell.Navbar> </AppShell.Navbar>
<AppShell.Main> {children} </AppShell.Main> <AppShell.Main> {children} </AppShell.Main>
</AppShell> </AppShell>

@ -0,0 +1,20 @@
.hover-container .show-on-hover {
display: none;
}
.hover-container:hover .show-on-hover {
display: block;
}
.hover-container .show-by-default {
display: block;
}
.hover-container:hover .show-by-default {
display: none;
}
.border-on-hover:hover {
border: 1px solid var(--mantine-color-gray-9);
border-radius: var(--mantine-radius-sm);
}

@ -7,7 +7,8 @@
"format": "biome format --write .", "format": "biome format --write .",
"preview:wrangler": "wrangler pages dev", "preview:wrangler": "wrangler pages dev",
"deploy:wrangler": "wrangler pages deploy", "deploy:wrangler": "wrangler pages deploy",
"deploy": "run-s build deploy:wrangler" "deploy": "run-s build deploy:wrangler",
"generate-types": "kanel"
}, },
"dependencies": { "dependencies": {
"@ai-sdk/react": "^1.2.12", "@ai-sdk/react": "^1.2.12",
@ -18,6 +19,7 @@
"@mantine/hooks": "^8.1.1", "@mantine/hooks": "^8.1.1",
"@openrouter/ai-sdk-provider": "^0.7.2", "@openrouter/ai-sdk-provider": "^0.7.2",
"@sinclair/typebox": "^0.34.37", "@sinclair/typebox": "^0.34.37",
"@tabler/icons-react": "^3.34.0",
"@trpc/client": "^11.4.2", "@trpc/client": "^11.4.2",
"@trpc/server": "^11.4.2", "@trpc/server": "^11.4.2",
"@universal-middleware/core": "^0.4.8", "@universal-middleware/core": "^0.4.8",
@ -27,6 +29,8 @@
"ai": "^4.3.16", "ai": "^4.3.16",
"dotenv": "^17.0.0", "dotenv": "^17.0.0",
"hono": "^4.8.2", "hono": "^4.8.2",
"kysely": "^0.28.2",
"pg": "^8.16.3",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"vike": "^0.4.235", "vike": "^0.4.235",
@ -40,8 +44,11 @@
"@cloudflare/workers-types": "^4.20250620.0", "@cloudflare/workers-types": "^4.20250620.0",
"@hono/vite-dev-server": "^0.19.1", "@hono/vite-dev-server": "^0.19.1",
"@types/node": "^20.19.0", "@types/node": "^20.19.0",
"@types/pg": "^8.15.4",
"@types/react": "^19.1.8", "@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6", "@types/react-dom": "^19.1.6",
"kanel": "^3.14.1",
"kanel-kysely": "^0.7.1",
"npm-run-all2": "^8.0.4", "npm-run-all2": "^8.0.4",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"postcss-preset-mantine": "^1.17.0", "postcss-preset-mantine": "^1.17.0",
@ -52,4 +59,4 @@
"wrangler": "^4.20.5" "wrangler": "^4.20.5"
}, },
"type": "module" "type": "module"
} }

@ -10,8 +10,8 @@ export default {
Layout, Layout,
// https://vike.dev/head-tags // https://vike.dev/head-tags
title: "My Vike App", title: "Trainable AI",
description: "Demo showcasing Vike", description: "The Chatbot that Remembers",
passToClient: ["user"], passToClient: ["user"],
extends: vikeReact, extends: vikeReact,

@ -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>
);
}

@ -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>
);
}

@ -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;
};

@ -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." 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: Every time the user submits a message, the backend should:
* Save the message to the database under a conversation key for later lookup * 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.
* Generate a new message from the model, and add it to the database. The model should be
* 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 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. * 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"). * 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").

@ -1,20 +1,80 @@
import { import {
router, router,
publicProcedure, publicProcedure,
createCallerFactory,
Validator,
// Validator // Validator
} from "../../trpc/server"; } from "../../trpc/server";
// import { Type as T } from "@sinclair/typebox";
import { createOpenRouter } from "@openrouter/ai-sdk-provider"; import { createOpenRouter } from "@openrouter/ai-sdk-provider";
import { generateText } from "ai"; import { generateText } from "ai";
import type { Message as UIMessage } 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 { 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({ const openrouter = createOpenRouter({
apiKey: env.OPENROUTER_API_KEY, apiKey: env.OPENROUTER_API_KEY,
}); });
export const chat = router({ 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 sendMessage: publicProcedure
.input( .input(
(x) => (x) =>
@ -38,3 +98,5 @@ export const chat = router({
return response; return response;
}), }),
}); });
export const createCaller = createCallerFactory(chat);

@ -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;
};

File diff suppressed because it is too large Load Diff

@ -0,0 +1,38 @@
import { create } from "zustand";
import type { OtherParameters, Store } from "./types.js";
import type { ConversationsId } from "./database/generated/public/Conversations.js";
export 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.`;
export const defaultParameters = {
temperature: 0.5,
max_tokens: 100,
} as OtherParameters;
export const useStore = create<Store>()((set) => ({
conversationId: 0 as ConversationsId,
conversationTitle: "",
conversations: [],
messages: [],
message: "",
systemPrompt: defaultSystemPrompt,
parameters: defaultParameters,
loading: false,
setConversationId: (conversationId) => set({ conversationId }),
setConversationTitle: (conversationTitle) => set({ conversationTitle }),
setConversations: (conversations) => set({ conversations }),
addConversation: (conversation) =>
set((state) => ({
conversations: [...state.conversations, conversation],
})),
removeConversation: (conversationId) =>
set((state) => ({
conversations: state.conversations.filter(
(conversation) => conversation.id !== conversationId,
),
})),
setMessages: (messages) => set({ messages }),
setMessage: (message) => set({ message }),
setSystemPrompt: (systemPrompt) => set({ systemPrompt }),
setParameters: (parameters) => set({ parameters }),
setLoading: (loading) => set({ loading }),
}));

@ -32,3 +32,5 @@ export function Validator<T extends TSchema>(schema: T) {
}); });
}; };
} }
export const createCallerFactory = t.createCallerFactory;

@ -0,0 +1,36 @@
import type { Message as UIMessage } from "ai";
import type { generateText } from "ai";
import type {
Conversations,
ConversationsId,
} from "./database/generated/public/Conversations";
export type OtherParameters = Omit<
Parameters<typeof generateText>[0],
"model" | "messages" | "abortSignal"
>;
export type ConversationUI = Conversations & {};
export type Store = {
/** This is a string because Milvus sends it as a string, and the value
* overflows the JS integer anyway. */
conversationId: ConversationsId;
conversationTitle: string;
conversations: Array<ConversationUI>;
messages: Array<UIMessage>;
message: string;
systemPrompt: string;
parameters: OtherParameters;
loading: boolean;
setConversationId: (conversationId: ConversationsId) => void;
setConversationTitle: (conversationTitle: string) => void;
setConversations: (conversations: Array<ConversationUI>) => void;
addConversation: (conversation: ConversationUI) => void;
removeConversation: (conversationId: ConversationsId) => void;
setMessages: (messages: Array<UIMessage>) => void;
setMessage: (message: string) => void;
setSystemPrompt: (systemPrompt: string) => void;
setParameters: (parameters: OtherParameters) => void;
setLoading: (loading: boolean) => void;
};
Loading…
Cancel
Save