generate and save facts

master
Avraham Sakal 3 months ago
parent d7716bd451
commit ee9e9424ab

@ -7,6 +7,14 @@ export type Conversation = {
userId: string; userId: string;
}; };
export type Fact = {
id: string;
userId: string;
sourceMessageId: string;
content: string;
createdAt: string;
};
type DB = { type DB = {
conversations: Array<Conversation>; conversations: Array<Conversation>;
messages: Array<{ messages: Array<{
@ -18,11 +26,13 @@ type DB = {
createdAt: string; createdAt: string;
runningSummary?: string; runningSummary?: string;
}>; }>;
facts: Array<Fact>;
}; };
export const db = new Low<DB>(new JSONFile("db.json"), { export const db = new Low<DB>(new JSONFile("db.json"), {
conversations: [], conversations: [],
messages: [], messages: [],
facts: [],
}); });
/** Initialize the database. Sets `db.data` to the default state if the file doesn't exist. */ /** Initialize the database. Sets `db.data` to the default state if the file doesn't exist. */
await db.read(); await db.read();

@ -26,7 +26,6 @@ import { useEffect } from "react";
import { trpc } from "../trpc/client.js"; import { trpc } from "../trpc/client.js";
import { usePageContext } from "vike-react/usePageContext"; import { usePageContext } from "vike-react/usePageContext";
import "./hover.css"; import "./hover.css";
import type { ConversationsId } from "../database/generated/public/Conversations.js";
export default function LayoutDefault({ export default function LayoutDefault({
children, children,
@ -54,9 +53,13 @@ export default function LayoutDefault({
// } // }
// }, [isConversationListExpanded]); // }, [isConversationListExpanded]);
function handleDeleteConversation(conversationId: string) { async function handleDeleteConversation(conversationId: string) {
removeConversation(conversationId); 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 ( return (

@ -2,6 +2,7 @@ import {
Box, Box,
Group, Group,
JsonInput, JsonInput,
List,
Stack, Stack,
Tabs, Tabs,
Textarea, Textarea,
@ -30,6 +31,7 @@ export default function ChatPage() {
const message = useStore((state) => state.message); const message = useStore((state) => state.message);
const systemPrompt = useStore((state) => state.systemPrompt); const systemPrompt = useStore((state) => state.systemPrompt);
const parameters = useStore((state) => state.parameters); const parameters = useStore((state) => state.parameters);
const facts = useStore((state) => state.facts);
const loading = useStore((state) => state.loading); const loading = useStore((state) => state.loading);
const setConversationId = useStore((state) => state.setConversationId); const setConversationId = useStore((state) => state.setConversationId);
const setConversationTitle = useStore((state) => state.setConversationTitle); const setConversationTitle = useStore((state) => state.setConversationTitle);
@ -37,9 +39,14 @@ export default function ChatPage() {
const setMessage = useStore((state) => state.setMessage); const setMessage = useStore((state) => state.setMessage);
const setSystemPrompt = useStore((state) => state.setSystemPrompt); const setSystemPrompt = useStore((state) => state.setSystemPrompt);
const setParameters = useStore((state) => state.setParameters); const setParameters = useStore((state) => state.setParameters);
const setFacts = useStore((state) => state.setFacts);
const setLoading = useStore((state) => state.setLoading); const setLoading = useStore((state) => state.setLoading);
const { conversation, messages: initialMessages } = useData<Data>(); const {
conversation,
messages: initialMessages,
facts: initialFacts,
} = useData<Data>();
useEffect(() => { useEffect(() => {
setConversationId(conversationId); setConversationId(conversationId);
@ -61,6 +68,10 @@ export default function ChatPage() {
setMessages(initialMessages); setMessages(initialMessages);
}, [initialMessages, setMessages]); }, [initialMessages, setMessages]);
useEffect(() => {
setFacts(initialFacts);
}, [initialFacts, setFacts]);
return ( return (
<> <>
<div> <div>
@ -84,6 +95,7 @@ export default function ChatPage() {
<Tabs.Tab value="message">Message</Tabs.Tab> <Tabs.Tab value="message">Message</Tabs.Tab>
<Tabs.Tab value="system-prompt">System Prompt</Tabs.Tab> <Tabs.Tab value="system-prompt">System Prompt</Tabs.Tab>
<Tabs.Tab value="parameters">Parameters</Tabs.Tab> <Tabs.Tab value="parameters">Parameters</Tabs.Tab>
<Tabs.Tab value="facts">Facts</Tabs.Tab>
</Tabs.List> </Tabs.List>
<Tabs.Panel value="message"> <Tabs.Panel value="message">
<Messages messages={messages} /> <Messages messages={messages} />
@ -131,6 +143,7 @@ export default function ChatPage() {
]; ];
setMessages(messagesWithAssistantMessage); setMessages(messagesWithAssistantMessage);
setMessage(""); setMessage("");
setFacts(response.insertedFacts);
setLoading(false); setLoading(false);
} }
}} }}
@ -153,6 +166,13 @@ export default function ChatPage() {
onChange={(value) => setParameters(JSON.parse(value))} onChange={(value) => setParameters(JSON.parse(value))}
/> />
</Tabs.Panel> </Tabs.Panel>
<Tabs.Panel value="facts">
<List>
{facts.map((fact) => (
<List.Item key={fact.id}>{fact.content}</List.Item>
))}
</List>
</Tabs.Panel>
</Tabs> </Tabs>
</> </>
); );
@ -164,7 +184,6 @@ function Messages({
messages: Array<DraftMessage | CommittedMessage>; messages: Array<DraftMessage | CommittedMessage>;
}) { }) {
const theme = useMantineTheme(); const theme = useMantineTheme();
console.log("messages", messages);
return ( return (
<Stack gap="md" justify="flex-start"> <Stack gap="md" justify="flex-start">
{messages.map((message, index) => ( {messages.map((message, index) => (
@ -188,7 +207,7 @@ function Messages({
</div> </div>
<Markdown>{message.content}</Markdown> <Markdown>{message.content}</Markdown>
</Box> </Box>
{"runningSummary" in message && ( {"runningSummary" in message && message.runningSummary && (
<Box w="75%" bd="dotted" p="md" bdrs="md"> <Box w="75%" bd="dotted" p="md" bdrs="md">
<div> <div>
<strong>Running Summary:</strong> <strong>Running Summary:</strong>

@ -12,5 +12,5 @@ export const data = async (pageContext: PageContextServer) => {
const messages = await caller.fetchMessages({ const messages = await caller.fetchMessages({
conversationId: id, conversationId: id,
}); });
return { conversation, messages }; return { conversation, messages, facts: [] };
}; };

@ -4,7 +4,7 @@ import {
createCallerFactory, createCallerFactory,
} from "../../trpc/server"; } from "../../trpc/server";
import { createOpenRouter } from "@openrouter/ai-sdk-provider"; 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 { Message as UIMessage } from "ai";
import type { import type {
OtherParameters, OtherParameters,
@ -17,7 +17,7 @@ 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/lowdb"; import { db, type Fact } from "../../database/lowdb";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
const mainSystemPrompt = ({ const mainSystemPrompt = ({
@ -30,6 +30,15 @@ This is a summary of the conversation so far, from your point-of-view (so "I" an
${previousRunningSummary} ${previousRunningSummary}
</running_summary> </running_summary>
`; `;
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.
<running_summary>
${previousRunningSummary}
</running_summary>
`;
const runningSummarySystemPrompt = ({ const runningSummarySystemPrompt = ({
previousRunningSummary, previousRunningSummary,
}: { }: {
@ -69,10 +78,19 @@ export const chat = router({
deleteConversation: publicProcedure deleteConversation: publicProcedure
.input((x) => x as { id: string }) .input((x) => x as { id: string })
.mutation(async ({ input: { id } }) => { .mutation(async ({ input: { id } }) => {
await db.data.conversations.splice( db.data.conversations.splice(
db.data.conversations.findIndex((c) => c.id === id), db.data.conversations.findIndex((c) => c.id === id),
1, 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(); db.write();
return { ok: true }; return { ok: true };
}), }),
@ -136,7 +154,7 @@ export const chat = router({
index: messages.length - 1, index: messages.length - 1,
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
}; };
await db.data.messages.push(insertedUserMessage); db.data.messages.push(insertedUserMessage);
// do not db.write() until the end // do not db.write() until the end
/** 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
@ -172,6 +190,43 @@ export const chat = router({
* injection, because we're sending the user's message unadulterated to * 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 * the model; there's no reason to inject the same Facts that the model is
* already using to generate its response.) */ * already using to generate its response.) */
const factsFromUserMessageResponse = await generateObject<{
facts: Array<string>;
}>({
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<Fact> =
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, /** Extract Facts from the model's response, and add them to the database,
* linking the Facts with the messages they came from. */ * linking the Facts with the messages they came from. */
/** For each Fact produced in the two fact-extraction steps, generate /** For each Fact produced in the two fact-extraction steps, generate
@ -246,12 +301,15 @@ export const chat = router({
index: messages.length, index: messages.length,
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
}; };
await db.data.messages.push(insertedAssistantMessage); db.data.messages.push(insertedAssistantMessage);
await db.write(); await db.write();
/** TODO: notify the caller, somehow, that some messages were saved to
* the database and/or were outfitted with runningSummaries, so the return {
* caller can update its UI state. */ insertedAssistantMessage,
return { insertedAssistantMessage, insertedUserMessage }; insertedUserMessage,
insertedFacts,
};
}, },
), ),
}); });

@ -1,6 +1,5 @@
import { create } from "zustand"; import { create } from "zustand";
import type { OtherParameters, Store } from "./types.js"; import type { OtherParameters, Store } from "./types.js";
import type { ConversationsId } from "./database/generated/public/Conversations.js";
import { immer } from "zustand/middleware/immer"; 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.`; 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<Store>()(
message: "", message: "",
systemPrompt: defaultSystemPrompt, systemPrompt: defaultSystemPrompt,
parameters: defaultParameters, parameters: defaultParameters,
facts: [],
loading: false, loading: false,
setConversationId: (conversationId) => setConversationId: (conversationId) =>
set((stateDraft) => { set((stateDraft) => {
@ -65,6 +65,10 @@ export const useStore = create<Store>()(
set((stateDraft) => { set((stateDraft) => {
stateDraft.parameters = parameters; stateDraft.parameters = parameters;
}), }),
setFacts: (facts) =>
set((stateDraft) => {
stateDraft.facts = facts;
}),
setLoading: (loading) => setLoading: (loading) =>
set((stateDraft) => { set((stateDraft) => {
stateDraft.loading = loading; stateDraft.loading = loading;

@ -1,6 +1,6 @@
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 { Conversation } from "./database/lowdb.js"; import type { Conversation, Fact } from "./database/lowdb.js";
export type OtherParameters = Omit< export type OtherParameters = Omit<
Parameters<typeof generateText>[0], Parameters<typeof generateText>[0],
@ -18,6 +18,7 @@ export type Store = {
message: string; message: string;
systemPrompt: string; systemPrompt: string;
parameters: OtherParameters; parameters: OtherParameters;
facts: Array<Fact>;
loading: boolean; loading: boolean;
setConversationId: (conversationId: string) => void; setConversationId: (conversationId: string) => void;
setConversationTitle: (conversationTitle: string) => void; setConversationTitle: (conversationTitle: string) => void;
@ -28,6 +29,7 @@ export type Store = {
setMessage: (message: string) => void; setMessage: (message: string) => void;
setSystemPrompt: (systemPrompt: string) => void; setSystemPrompt: (systemPrompt: string) => void;
setParameters: (parameters: OtherParameters) => void; setParameters: (parameters: OtherParameters) => void;
setFacts: (facts: Array<Fact>) => void;
setLoading: (loading: boolean) => void; setLoading: (loading: boolean) => void;
}; };

Loading…
Cancel
Save