diff --git a/database/lowdb.ts b/database/lowdb.ts index 4e2e228..97ce713 100644 --- a/database/lowdb.ts +++ b/database/lowdb.ts @@ -7,6 +7,14 @@ export type Conversation = { userId: string; }; +export type Fact = { + id: string; + userId: string; + sourceMessageId: string; + content: string; + createdAt: string; +}; + type DB = { conversations: Array; messages: Array<{ @@ -18,11 +26,13 @@ type DB = { createdAt: string; runningSummary?: string; }>; + facts: Array; }; export const db = new Low(new JSONFile("db.json"), { conversations: [], messages: [], + facts: [], }); /** Initialize the database. Sets `db.data` to the default state if the file doesn't exist. */ await db.read(); diff --git a/layouts/LayoutDefault.tsx b/layouts/LayoutDefault.tsx index c148e0a..16f2309 100644 --- a/layouts/LayoutDefault.tsx +++ b/layouts/LayoutDefault.tsx @@ -26,7 +26,6 @@ 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({ children, @@ -54,9 +53,13 @@ export default function LayoutDefault({ // } // }, [isConversationListExpanded]); - function handleDeleteConversation(conversationId: string) { + async function handleDeleteConversation(conversationId: string) { removeConversation(conversationId); - trpc.chat.deleteConversation.mutate({ id: conversationId }); + await trpc.chat.deleteConversation.mutate({ id: conversationId }); + const res = await trpc.chat.createConversation.mutate(); + if (!res?.id) return; + addConversation(res); + await navigate(`/chat/${res.id}`); } return ( diff --git a/pages/chat/@id/+Page.tsx b/pages/chat/@id/+Page.tsx index 29b9d26..624ebed 100644 --- a/pages/chat/@id/+Page.tsx +++ b/pages/chat/@id/+Page.tsx @@ -2,6 +2,7 @@ import { Box, Group, JsonInput, + List, Stack, Tabs, Textarea, @@ -30,6 +31,7 @@ export default function ChatPage() { const message = useStore((state) => state.message); const systemPrompt = useStore((state) => state.systemPrompt); const parameters = useStore((state) => state.parameters); + const facts = useStore((state) => state.facts); const loading = useStore((state) => state.loading); const setConversationId = useStore((state) => state.setConversationId); const setConversationTitle = useStore((state) => state.setConversationTitle); @@ -37,9 +39,14 @@ export default function ChatPage() { const setMessage = useStore((state) => state.setMessage); const setSystemPrompt = useStore((state) => state.setSystemPrompt); const setParameters = useStore((state) => state.setParameters); + const setFacts = useStore((state) => state.setFacts); const setLoading = useStore((state) => state.setLoading); - const { conversation, messages: initialMessages } = useData(); + const { + conversation, + messages: initialMessages, + facts: initialFacts, + } = useData(); useEffect(() => { setConversationId(conversationId); @@ -61,6 +68,10 @@ export default function ChatPage() { setMessages(initialMessages); }, [initialMessages, setMessages]); + useEffect(() => { + setFacts(initialFacts); + }, [initialFacts, setFacts]); + return ( <>
@@ -84,6 +95,7 @@ export default function ChatPage() { Message System Prompt Parameters + Facts @@ -131,6 +143,7 @@ export default function ChatPage() { ]; setMessages(messagesWithAssistantMessage); setMessage(""); + setFacts(response.insertedFacts); setLoading(false); } }} @@ -153,6 +166,13 @@ export default function ChatPage() { onChange={(value) => setParameters(JSON.parse(value))} /> + + + {facts.map((fact) => ( + {fact.content} + ))} + + ); @@ -164,7 +184,6 @@ function Messages({ messages: Array; }) { const theme = useMantineTheme(); - console.log("messages", messages); return ( {messages.map((message, index) => ( @@ -188,7 +207,7 @@ function Messages({
{message.content} - {"runningSummary" in message && ( + {"runningSummary" in message && message.runningSummary && (
Running Summary: diff --git a/pages/chat/@id/+data.ts b/pages/chat/@id/+data.ts index c9f7c67..a88b058 100644 --- a/pages/chat/@id/+data.ts +++ b/pages/chat/@id/+data.ts @@ -12,5 +12,5 @@ export const data = async (pageContext: PageContextServer) => { const messages = await caller.fetchMessages({ conversationId: id, }); - return { conversation, messages }; + return { conversation, messages, facts: [] }; }; diff --git a/pages/chat/trpc.ts b/pages/chat/trpc.ts index 9702428..aafb182 100644 --- a/pages/chat/trpc.ts +++ b/pages/chat/trpc.ts @@ -4,7 +4,7 @@ import { createCallerFactory, } from "../../trpc/server"; import { createOpenRouter } from "@openrouter/ai-sdk-provider"; -import { generateText } from "ai"; +import { generateObject, generateText, jsonSchema } from "ai"; import type { Message as UIMessage } from "ai"; import type { OtherParameters, @@ -17,7 +17,7 @@ import { env } from "../../server/env.js"; // ConsistencyLevelEnum, // type NumberArrayId, // } from "@zilliz/milvus2-sdk-node"; -import { db } from "../../database/lowdb"; +import { db, type Fact } from "../../database/lowdb"; import { nanoid } from "nanoid"; const mainSystemPrompt = ({ @@ -30,6 +30,15 @@ This is a summary of the conversation so far, from your point-of-view (so "I" an ${previousRunningSummary} `; +const factsFromUserMessageSystemPrompt = ({ + previousRunningSummary, +}: { + previousRunningSummary: string; +}) => `Given the following summary of a conversation, coupled with the messages exchanged since that summary was produced, extract new facts that can be gleaned from the conversation. + + ${previousRunningSummary} + +`; const runningSummarySystemPrompt = ({ previousRunningSummary, }: { @@ -69,10 +78,19 @@ export const chat = router({ deleteConversation: publicProcedure .input((x) => x as { id: string }) .mutation(async ({ input: { id } }) => { - await db.data.conversations.splice( + db.data.conversations.splice( db.data.conversations.findIndex((c) => c.id === id), 1, ); + const deletedMessageIds = db.data.messages + .filter((m) => m.conversationId === id) + .map((m) => m.id); + db.data.messages = db.data.messages.filter( + (m) => m.conversationId !== id, + ); + db.data.facts = db.data.facts.filter( + (fact) => !deletedMessageIds.includes(fact.sourceMessageId), + ); db.write(); return { ok: true }; }), @@ -136,7 +154,7 @@ export const chat = router({ index: messages.length - 1, createdAt: new Date().toISOString(), }; - await db.data.messages.push(insertedUserMessage); + db.data.messages.push(insertedUserMessage); // do not db.write() until the end /** Generate a new message from the model, but hold-off on adding it to @@ -172,6 +190,43 @@ export const chat = router({ * 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.) */ + const factsFromUserMessageResponse = await generateObject<{ + facts: Array; + }>({ + model: openrouter("mistralai/mistral-nemo"), + messages: [ + { + role: "system" as const, + content: factsFromUserMessageSystemPrompt({ + previousRunningSummary, + }), + }, + ...messages.slice(previousRunningSummaryIndex + 1), + ], + schema: jsonSchema({ + type: "object", + properties: { + facts: { + type: "array", + items: { + type: "string", + }, + }, + }, + }), + maxSteps: 3, + tools: undefined, + ...parameters, + }); + const insertedFacts: Array = + factsFromUserMessageResponse.object.facts.map((fact) => ({ + id: nanoid(), + userId: "1", + sourceMessageId: insertedUserMessage.id, + content: fact, + createdAt: new Date().toISOString(), + })); + db.data.facts.push(...insertedFacts); /** 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 @@ -246,12 +301,15 @@ export const chat = router({ index: messages.length, createdAt: new Date().toISOString(), }; - await db.data.messages.push(insertedAssistantMessage); + db.data.messages.push(insertedAssistantMessage); + await db.write(); - /** TODO: notify the caller, somehow, that some messages were saved to - * the database and/or were outfitted with runningSummaries, so the - * caller can update its UI state. */ - return { insertedAssistantMessage, insertedUserMessage }; + + return { + insertedAssistantMessage, + insertedUserMessage, + insertedFacts, + }; }, ), }); diff --git a/state.ts b/state.ts index 4cfacec..f27d872 100644 --- a/state.ts +++ b/state.ts @@ -1,6 +1,5 @@ import { create } from "zustand"; import type { OtherParameters, Store } from "./types.js"; -import type { ConversationsId } from "./database/generated/public/Conversations.js"; import { immer } from "zustand/middleware/immer"; 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.`; @@ -17,6 +16,7 @@ export const useStore = create()( message: "", systemPrompt: defaultSystemPrompt, parameters: defaultParameters, + facts: [], loading: false, setConversationId: (conversationId) => set((stateDraft) => { @@ -65,6 +65,10 @@ export const useStore = create()( set((stateDraft) => { stateDraft.parameters = parameters; }), + setFacts: (facts) => + set((stateDraft) => { + stateDraft.facts = facts; + }), setLoading: (loading) => set((stateDraft) => { stateDraft.loading = loading; diff --git a/types.ts b/types.ts index 3ce375e..af8ba38 100644 --- a/types.ts +++ b/types.ts @@ -1,6 +1,6 @@ import type { Message as UIMessage } from "ai"; import type { generateText } from "ai"; -import type { Conversation } from "./database/lowdb.js"; +import type { Conversation, Fact } from "./database/lowdb.js"; export type OtherParameters = Omit< Parameters[0], @@ -18,6 +18,7 @@ export type Store = { message: string; systemPrompt: string; parameters: OtherParameters; + facts: Array; loading: boolean; setConversationId: (conversationId: string) => void; setConversationTitle: (conversationTitle: string) => void; @@ -28,6 +29,7 @@ export type Store = { setMessage: (message: string) => void; setSystemPrompt: (systemPrompt: string) => void; setParameters: (parameters: OtherParameters) => void; + setFacts: (facts: Array) => void; setLoading: (loading: boolean) => void; };