generate and save facts
This commit is contained in:
@@ -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: [] };
|
||||||
};
|
};
|
||||||
|
|||||||
+67
-9
@@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user