re-organize files
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { Card, Textarea } from "@mantine/core";
|
||||
import { useState } from "react";
|
||||
import { useTRPCClient } from "../../trpc/client";
|
||||
import { useTRPCClient } from "../trpc-client";
|
||||
|
||||
export default function ChatPage() {
|
||||
const [inputMessage, setInputMessage] = useState("");
|
||||
|
||||
@@ -28,7 +28,7 @@ import type {
|
||||
DraftMessage,
|
||||
OtherParameters,
|
||||
SendMessageStatus,
|
||||
} from "../../../types";
|
||||
} from "@/types";
|
||||
import Markdown from "react-markdown";
|
||||
import {
|
||||
IconTrash,
|
||||
@@ -37,10 +37,9 @@ import {
|
||||
IconX,
|
||||
IconLoaderQuarter,
|
||||
} from "@tabler/icons-react";
|
||||
import { useTRPC, useTRPCClient } from "../../../trpc/client";
|
||||
import { useTRPC, useTRPCClient } from "../../trpc-client";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { nanoid } from "nanoid";
|
||||
import type { Conversation } from "../../../database/common";
|
||||
import type { Conversation } from "@/database/common";
|
||||
|
||||
export default function ChatPage() {
|
||||
const pageContext = usePageContext();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { PageContextServer } from "vike/types";
|
||||
import { createCaller } from "../trpc.js";
|
||||
import { createCaller } from "../../../server/trpc/chat.js";
|
||||
import { getDb } from "../../../database/postgres.js";
|
||||
import { getOpenrouter } from "../provider.js";
|
||||
import { getOpenrouter } from "../../../server/provider.js";
|
||||
import { env } from "../../../server/env.js";
|
||||
|
||||
export type Data = Awaited<ReturnType<typeof data>>;
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import {
|
||||
router,
|
||||
publicProcedure,
|
||||
createCallerFactory,
|
||||
} from "../../trpc/server";
|
||||
|
||||
export const conversations = router({
|
||||
fetchAll: publicProcedure.query(async ({ ctx: { db, jwt } }) => {
|
||||
console.log("jwt", jwt);
|
||||
return await db.conversations.findAll({ userId: jwt?.id as string });
|
||||
}),
|
||||
fetchOne: publicProcedure
|
||||
.input((x) => x as { id: string })
|
||||
.query(async ({ input: { id }, ctx: { db } }) => {
|
||||
return await db.conversations.findById(id);
|
||||
}),
|
||||
start: publicProcedure.mutation(async ({ ctx: { db, jwt } }) => {
|
||||
const row = {
|
||||
title: "New Conversation",
|
||||
userId: jwt?.id as string,
|
||||
};
|
||||
return await db.conversations.create(row);
|
||||
}),
|
||||
deleteOne: publicProcedure
|
||||
.input((x) => x as { id: string })
|
||||
.mutation(async ({ input: { id }, ctx: { db } }) => {
|
||||
await db.conversations.delete(id);
|
||||
return { ok: true };
|
||||
}),
|
||||
updateTitle: publicProcedure
|
||||
.input(
|
||||
(x) =>
|
||||
x as {
|
||||
id: string;
|
||||
title: string;
|
||||
}
|
||||
)
|
||||
.mutation(async ({ input: { id, title }, ctx: { db } }) => {
|
||||
await db.conversations.update(id, { title });
|
||||
return { ok: true };
|
||||
}),
|
||||
fetchMessages: publicProcedure
|
||||
.input((x) => x as { conversationId: string })
|
||||
.query(async ({ input: { conversationId }, ctx: { db } }) => {
|
||||
return await db.conversations.fetchMessages(conversationId);
|
||||
}),
|
||||
});
|
||||
|
||||
export const createCaller = createCallerFactory(conversations);
|
||||
@@ -1,161 +0,0 @@
|
||||
import {
|
||||
router,
|
||||
publicProcedure,
|
||||
createCallerFactory,
|
||||
} from "../../trpc/server.js";
|
||||
import type { DraftMessage } from "../../types.js";
|
||||
import { MODEL_NAME } from "./provider.js";
|
||||
import { generateObject, generateText, jsonSchema } from "ai";
|
||||
import type { Fact } from "../../database/common.js";
|
||||
|
||||
const factTriggersSystemPrompt = ({
|
||||
previousRunningSummary,
|
||||
messagesSincePreviousRunningSummary,
|
||||
mainResponseContent,
|
||||
}: {
|
||||
previousRunningSummary: string;
|
||||
messagesSincePreviousRunningSummary: Array<DraftMessage>;
|
||||
mainResponseContent: string;
|
||||
}) => `You are an expert at idenitfying situations when facts are useful.
|
||||
|
||||
You will be given a summary of a conversation, and the messages exchanged since that summary was produced.
|
||||
|
||||
Then you will be given a fact that was extracted from that conversation, and you will need to identify a natural language phrase that describes a situation in which it would be useful to invoke the fact.
|
||||
|
||||
The facts will be used to enrich context of conversations with AI assistants: upon each turn, a semantic database will be searched to see whether the current situation in the conversation matches any situations that are deemed to render the fact useful, and the fact will be injected into the context of the conversation.
|
||||
|
||||
Your task is to produce a list of triggers for the fact.
|
||||
|
||||
* You should not extract any facts that are already in the summary.
|
||||
* The user should be referred to as "the user" in the fact text.
|
||||
* The user's pronouns should be either he or she, NOT "they" or "them", because these triggers will be read by an AI assistant to give it context; and excessive use of "they" or "them" will make what they refer to unclear or ambiguous.
|
||||
* The assistant should be referred to as "I" or "me", because these triggers will be read by an AI assistant to give it context.
|
||||
|
||||
<running_summary>
|
||||
${previousRunningSummary}
|
||||
</running_summary>
|
||||
|
||||
${messagesSincePreviousRunningSummary.map(
|
||||
(message) =>
|
||||
`<${message.role}_message>${message.parts
|
||||
.filter((p) => p.type === "text")
|
||||
.map((p) => p.text)
|
||||
.join("\n")}</${message.role}_message>`
|
||||
)}
|
||||
<assistant_response>
|
||||
${mainResponseContent}
|
||||
</assistant_response>
|
||||
`;
|
||||
|
||||
const factTriggersUserPrompt = ({
|
||||
factContent,
|
||||
}: {
|
||||
factContent: string;
|
||||
}) => `<fact_content>
|
||||
${factContent}
|
||||
</fact_content>
|
||||
|
||||
Generate a list of situations in which the fact is useful.`;
|
||||
|
||||
export const factTriggers = router({
|
||||
fetchByFactId: publicProcedure
|
||||
.input((x) => x as { factId: string })
|
||||
.query(async ({ input: { factId }, ctx: { db } }) => {
|
||||
return db.factTriggers.findByFactId(factId);
|
||||
}),
|
||||
fetchByConversationId: publicProcedure
|
||||
.input((x) => x as { conversationId: string })
|
||||
.query(async ({ input: { conversationId }, ctx: { db } }) => {
|
||||
return await db.factTriggers.findByConversationId(conversationId);
|
||||
}),
|
||||
deleteOne: publicProcedure
|
||||
.input(
|
||||
(x) =>
|
||||
x as {
|
||||
factTriggerId: string;
|
||||
}
|
||||
)
|
||||
.mutation(async ({ input: { factTriggerId }, ctx: { db } }) => {
|
||||
await db.factTriggers.delete(factTriggerId);
|
||||
return { ok: true };
|
||||
}),
|
||||
update: publicProcedure
|
||||
.input(
|
||||
(x) =>
|
||||
x as {
|
||||
factTriggerId: string;
|
||||
content: string;
|
||||
}
|
||||
)
|
||||
.mutation(async ({ input: { factTriggerId, content }, ctx: { db } }) => {
|
||||
db.factTriggers.update(factTriggerId, { content });
|
||||
return { ok: true };
|
||||
}),
|
||||
generateFromFact: publicProcedure
|
||||
.input(
|
||||
(x) =>
|
||||
x as {
|
||||
previousRunningSummary: string;
|
||||
messagesSincePreviousRunningSummary: Array<DraftMessage>;
|
||||
mainResponseContent: string;
|
||||
fact: Fact;
|
||||
}
|
||||
)
|
||||
.mutation(
|
||||
async ({
|
||||
input: {
|
||||
previousRunningSummary,
|
||||
messagesSincePreviousRunningSummary,
|
||||
mainResponseContent,
|
||||
fact,
|
||||
},
|
||||
ctx: { openrouter },
|
||||
}) => {
|
||||
const factTriggers = await generateObject({
|
||||
model: openrouter(MODEL_NAME),
|
||||
messages: [
|
||||
{
|
||||
role: "system" as const,
|
||||
content: factTriggersSystemPrompt({
|
||||
previousRunningSummary,
|
||||
messagesSincePreviousRunningSummary,
|
||||
mainResponseContent,
|
||||
}),
|
||||
},
|
||||
{
|
||||
role: "user" as const,
|
||||
content: factTriggersUserPrompt({
|
||||
factContent: fact.content,
|
||||
}),
|
||||
},
|
||||
],
|
||||
schema: jsonSchema<{
|
||||
factTriggers: Array<string>;
|
||||
}>({
|
||||
type: "object",
|
||||
properties: {
|
||||
factTriggers: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ["factTriggers"],
|
||||
}),
|
||||
maxRetries: 0,
|
||||
// tools: undefined,
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
return {
|
||||
object: {
|
||||
factTriggers: [] as Array<string>,
|
||||
},
|
||||
};
|
||||
});
|
||||
return factTriggers;
|
||||
}
|
||||
),
|
||||
});
|
||||
|
||||
export const createCaller = createCallerFactory(factTriggers);
|
||||
@@ -1,150 +0,0 @@
|
||||
import {
|
||||
router,
|
||||
publicProcedure,
|
||||
createCallerFactory,
|
||||
} from "../../trpc/server.js";
|
||||
import type { DraftMessage } from "../../types.js";
|
||||
import { MODEL_NAME, openrouter } from "./provider.js";
|
||||
import { generateObject, generateText, jsonSchema } from "ai";
|
||||
|
||||
const factsFromNewMessagesSystemPrompt = ({
|
||||
previousRunningSummary,
|
||||
messagesSincePreviousRunningSummary,
|
||||
}: {
|
||||
previousRunningSummary: string;
|
||||
messagesSincePreviousRunningSummary: Array<DraftMessage>;
|
||||
}) => `You are an expert at extracting facts from conversations.
|
||||
|
||||
An AI assistant is in the middle of a conversation whose data is given below. The data consists of a summary of a conversation, and optionally some messages exchanged since that summary was produced. The user will provide you with *new* messages.
|
||||
|
||||
Your task is to extract *new* facts that can be gleaned from the *new* messages that the user sends.
|
||||
|
||||
* You should not extract any facts that are already in the summary.
|
||||
* The user should be referred to as "the user" in the fact text.
|
||||
* The user's pronouns should be either he or she, NOT "they" or "them", because this summary will be read by an AI assistant to give it context; and excessive use of "they" or "them" will make what they refer to unclear or ambiguous.
|
||||
* The assistant should be referred to as "I" or "me", because these facts will be read by an AI assistant to give it context.
|
||||
|
||||
<running_summary>
|
||||
${previousRunningSummary}
|
||||
</running_summary>
|
||||
|
||||
${messagesSincePreviousRunningSummary.map(
|
||||
(message) =>
|
||||
`<${message.role}_message>${message.parts
|
||||
.filter((p) => p.type === "text")
|
||||
.map((p) => p.text)
|
||||
.join("\n")}</${message.role}_message>`
|
||||
)}
|
||||
`;
|
||||
|
||||
const factsFromNewMessagesUserPrompt = ({
|
||||
newMessages,
|
||||
}: {
|
||||
newMessages: Array<DraftMessage>;
|
||||
}) =>
|
||||
`${newMessages.map(
|
||||
(message) =>
|
||||
`<${message.role}_message>${message.parts
|
||||
.filter((p) => p.type === "text")
|
||||
.map((p) => p.text)
|
||||
.join("\n")}</${message.role}_message>`
|
||||
)}
|
||||
|
||||
Extract new facts from these messages.`;
|
||||
|
||||
export const facts = router({
|
||||
fetchByConversationId: publicProcedure
|
||||
.input((x) => x as { conversationId: string })
|
||||
.query(async ({ input: { conversationId }, ctx: { db } }) => {
|
||||
return await db.facts.findByConversationId(conversationId);
|
||||
}),
|
||||
deleteOne: publicProcedure
|
||||
.input(
|
||||
(x) =>
|
||||
x as {
|
||||
factId: string;
|
||||
}
|
||||
)
|
||||
.mutation(async ({ input: { factId }, ctx: { db } }) => {
|
||||
await db.facts.delete(factId);
|
||||
return { ok: true };
|
||||
}),
|
||||
update: publicProcedure
|
||||
.input(
|
||||
(x) =>
|
||||
x as {
|
||||
factId: string;
|
||||
content: string;
|
||||
}
|
||||
)
|
||||
.mutation(async ({ input: { factId, content }, ctx: { db } }) => {
|
||||
await db.facts.update(factId, { content });
|
||||
return { ok: true };
|
||||
}),
|
||||
extractFromNewMessages: publicProcedure
|
||||
.input(
|
||||
(x) =>
|
||||
x as {
|
||||
previousRunningSummary: string;
|
||||
/** will *not* have facts extracted */
|
||||
messagesSincePreviousRunningSummary: Array<DraftMessage>;
|
||||
/** *will* have facts extracted */
|
||||
newMessages: Array<DraftMessage>;
|
||||
}
|
||||
)
|
||||
.query(
|
||||
async ({
|
||||
input: {
|
||||
previousRunningSummary,
|
||||
messagesSincePreviousRunningSummary,
|
||||
newMessages,
|
||||
},
|
||||
ctx: { openrouter },
|
||||
}) => {
|
||||
const factsFromUserMessageResponse = await generateObject({
|
||||
model: openrouter(MODEL_NAME),
|
||||
messages: [
|
||||
{
|
||||
role: "system" as const,
|
||||
content: factsFromNewMessagesSystemPrompt({
|
||||
previousRunningSummary,
|
||||
messagesSincePreviousRunningSummary,
|
||||
}),
|
||||
},
|
||||
{
|
||||
role: "user" as const,
|
||||
content: factsFromNewMessagesUserPrompt({
|
||||
newMessages,
|
||||
}),
|
||||
},
|
||||
],
|
||||
schema: jsonSchema<{
|
||||
facts: Array<string>;
|
||||
}>({
|
||||
type: "object",
|
||||
properties: {
|
||||
facts: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ["facts"],
|
||||
}),
|
||||
temperature: 0.4,
|
||||
maxRetries: 0,
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
return {
|
||||
object: {
|
||||
facts: [] as Array<string>,
|
||||
},
|
||||
};
|
||||
});
|
||||
return factsFromUserMessageResponse;
|
||||
}
|
||||
),
|
||||
});
|
||||
|
||||
export const createCaller = createCallerFactory(facts);
|
||||
@@ -1,104 +0,0 @@
|
||||
import {
|
||||
router,
|
||||
publicProcedure,
|
||||
createCallerFactory,
|
||||
} from "../../trpc/server";
|
||||
import { MODEL_NAME } from "./provider.js";
|
||||
import { generateObject, generateText, jsonSchema } from "ai";
|
||||
import type { DraftMessage } from "../../types.js";
|
||||
|
||||
const runningSummarySystemPrompt = ({
|
||||
previousRunningSummary,
|
||||
}: {
|
||||
previousRunningSummary: string;
|
||||
}) => `You are an expert at summarizing conversations.
|
||||
|
||||
You will be given a summary of a conversation, and the messages exchanged since that summary was produced.
|
||||
|
||||
Your task is to produce a new summary of the conversation.
|
||||
|
||||
* The user should be referred to as "the user" in the summary.
|
||||
* The user's pronouns should be either he or she, NOT "they" or "them", because this summary will be read by an AI assistant to give it context; and excessive use of "they" or "them" will make what they refer to unclear or ambiguous.
|
||||
* The assistant should be referred to as "I" or "me", because this summary will be read by an AI assistant to give it context.
|
||||
* The new summary may omit details present in the old summary, provided that the messages that were exchanged since that summary was produced indictae that those details are becoming less relevant to the continuation of the conversation.
|
||||
|
||||
<running_summary>
|
||||
${previousRunningSummary}
|
||||
</running_summary>
|
||||
`;
|
||||
|
||||
const runningSummaryUserPrompt = ({
|
||||
messagesSincePreviousRunningSummary,
|
||||
mainResponseContent,
|
||||
}: {
|
||||
messagesSincePreviousRunningSummary: Array<DraftMessage>;
|
||||
mainResponseContent: string;
|
||||
}) =>
|
||||
`${messagesSincePreviousRunningSummary.map(
|
||||
(message) =>
|
||||
`<${message.role}_message>${message.parts
|
||||
.filter((p) => p.type === "text")
|
||||
.map((p) => p.text)
|
||||
.join("\n")}</${message.role}_message>`
|
||||
)}
|
||||
<assistant_response>
|
||||
${mainResponseContent}
|
||||
</assistant_response>
|
||||
|
||||
Generate a new running summary of the conversation.`;
|
||||
|
||||
export const messages = router({
|
||||
fetchByConversationId: publicProcedure
|
||||
.input((x) => x as { conversationId: string })
|
||||
.query(async ({ input: { conversationId }, ctx: { db } }) => {
|
||||
return await db.messages.findByConversationId(conversationId);
|
||||
}),
|
||||
deleteOne: publicProcedure
|
||||
.input((x) => x as { id: string })
|
||||
.mutation(async ({ input: { id }, ctx: { db } }) => {
|
||||
await db.messages.delete(id);
|
||||
return { success: true };
|
||||
}),
|
||||
generateRunningSummary: publicProcedure
|
||||
.input(
|
||||
(x) =>
|
||||
x as {
|
||||
previousRunningSummary: string;
|
||||
messagesSincePreviousRunningSummary: Array<DraftMessage>;
|
||||
mainResponseContent: string;
|
||||
}
|
||||
)
|
||||
.mutation(
|
||||
async ({
|
||||
input: {
|
||||
previousRunningSummary,
|
||||
messagesSincePreviousRunningSummary,
|
||||
mainResponseContent,
|
||||
},
|
||||
ctx: { openrouter },
|
||||
}) => {
|
||||
const runningSummaryResponse = await generateText({
|
||||
model: openrouter(MODEL_NAME),
|
||||
messages: [
|
||||
{
|
||||
role: "system" as const,
|
||||
content: runningSummarySystemPrompt({
|
||||
previousRunningSummary,
|
||||
}),
|
||||
},
|
||||
{
|
||||
role: "user" as const,
|
||||
content: runningSummaryUserPrompt({
|
||||
messagesSincePreviousRunningSummary,
|
||||
mainResponseContent,
|
||||
}),
|
||||
},
|
||||
],
|
||||
tools: undefined,
|
||||
});
|
||||
return runningSummaryResponse;
|
||||
}
|
||||
),
|
||||
});
|
||||
|
||||
export const createCaller = createCallerFactory(messages);
|
||||
@@ -1,14 +0,0 @@
|
||||
import { createOpenRouter } from "@openrouter/ai-sdk-provider";
|
||||
import { env } from "../../server/env.js";
|
||||
export const openrouter = createOpenRouter({
|
||||
apiKey: env.OPENROUTER_API_KEY,
|
||||
});
|
||||
|
||||
export function getOpenrouter(OPENROUTER_API_KEY: string) {
|
||||
return createOpenRouter({
|
||||
apiKey: OPENROUTER_API_KEY,
|
||||
});
|
||||
}
|
||||
export const MODEL_NAME = "mistralai/mistral-nemo";
|
||||
// export const MODEL_NAME = "z-ai/glm-4.5-air";
|
||||
// export const MODEL_NAME = "openai/gpt-5-mini";
|
||||
@@ -1,286 +0,0 @@
|
||||
import {
|
||||
router,
|
||||
publicProcedure,
|
||||
createCallerFactory,
|
||||
} from "../../trpc/server.js";
|
||||
import { generateObject, generateText, jsonSchema, streamText } from "ai";
|
||||
import type {
|
||||
OtherParameters,
|
||||
CommittedMessage,
|
||||
DraftMessage,
|
||||
} from "../../types.js";
|
||||
// import { client } from "../../database/milvus";
|
||||
// import {
|
||||
// ConsistencyLevelEnum,
|
||||
// type NumberArrayId,
|
||||
// } from "@zilliz/milvus2-sdk-node";
|
||||
import { conversations } from "./conversations.js";
|
||||
import { messages } from "./messages.js";
|
||||
import { facts, createCaller as createCallerFacts } from "./facts.js";
|
||||
import { createCaller as createCallerMessages } from "./messages.js";
|
||||
import { createCaller as createCallerFactTriggers } from "./fact-triggers.js";
|
||||
import { factTriggers } from "./fact-triggers.js";
|
||||
import { MODEL_NAME } from "./provider.js";
|
||||
import type { Fact, FactTrigger } from "../../database/common.js";
|
||||
|
||||
const mainSystemPrompt = ({
|
||||
systemPrompt,
|
||||
previousRunningSummary,
|
||||
}: {
|
||||
systemPrompt: string;
|
||||
previousRunningSummary: string;
|
||||
}) => `${systemPrompt}
|
||||
|
||||
This is a summary of the conversation so far, from your point-of-view (so "I" and "me" refer to you):
|
||||
<running_summary>
|
||||
${previousRunningSummary}
|
||||
</running_summary>
|
||||
`;
|
||||
|
||||
export const chat = router({
|
||||
conversations,
|
||||
messages,
|
||||
facts,
|
||||
factTriggers,
|
||||
streamMessage: publicProcedure
|
||||
.input(
|
||||
(x) =>
|
||||
x as {
|
||||
message: string;
|
||||
}
|
||||
)
|
||||
.subscription(async function* ({ input, signal, ctx: { openrouter } }) {
|
||||
const result = streamText({
|
||||
model: openrouter(MODEL_NAME),
|
||||
messages: [{ role: "user" as const, content: input.message }],
|
||||
abortSignal: signal,
|
||||
});
|
||||
for await (const chunk of result.textStream) {
|
||||
yield chunk;
|
||||
}
|
||||
}),
|
||||
sendMessage: publicProcedure
|
||||
.input(
|
||||
(x) =>
|
||||
x as {
|
||||
conversationId: string;
|
||||
messages: Array<DraftMessage | CommittedMessage>;
|
||||
systemPrompt: string;
|
||||
parameters: OtherParameters;
|
||||
}
|
||||
)
|
||||
.subscription(async function* ({
|
||||
input: { conversationId, messages, systemPrompt, parameters },
|
||||
ctx,
|
||||
}) {
|
||||
const { db, openrouter, jwt } = ctx;
|
||||
const factsCaller = createCallerFacts(ctx);
|
||||
const messagesCaller = createCallerMessages(ctx);
|
||||
const factTriggerCaller = createCallerFactTriggers(ctx);
|
||||
/** TODO: Save all unsaved messages (i.e. those without an `id`) to the
|
||||
* database. Is this dangerous? Can an attacker just send a bunch of
|
||||
* messages, omitting the ids, causing me to save a bunch of them to the
|
||||
* database? I guess it's no worse than starting new converations, which
|
||||
* anyone can freely do. */
|
||||
const previousRunningSummaryIndex = messages.findLastIndex(
|
||||
(message) =>
|
||||
typeof (message as CommittedMessage).runningSummary !== "undefined"
|
||||
);
|
||||
const previousRunningSummary =
|
||||
previousRunningSummaryIndex >= 0
|
||||
? ((messages[previousRunningSummaryIndex] as CommittedMessage)
|
||||
.runningSummary as string)
|
||||
: "";
|
||||
const messagesSincePreviousRunningSummary = messages.slice(
|
||||
previousRunningSummaryIndex + 1
|
||||
);
|
||||
|
||||
// Emit status update
|
||||
yield {
|
||||
status: "saving_user_message",
|
||||
message: "Saving user message...",
|
||||
} as const;
|
||||
|
||||
/** Save the incoming message to the database. */
|
||||
const insertedUserMessage = await db.messages.create({
|
||||
conversationId,
|
||||
// content: messages[messages.length - 1].content,
|
||||
// role: "user" as const,
|
||||
...messages[messages.length - 1],
|
||||
index: messages.length - 1,
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
// Emit status update
|
||||
yield {
|
||||
status: "generating_response",
|
||||
message: "Generating AI response...",
|
||||
} as const;
|
||||
|
||||
/** Generate a new message from the model, but hold-off on adding it to
|
||||
* the database until we produce the associated running-summary, below.
|
||||
* 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. */
|
||||
const mainResponse = await generateText({
|
||||
model: openrouter(MODEL_NAME),
|
||||
messages: [
|
||||
previousRunningSummary === ""
|
||||
? {
|
||||
role: "system" as const,
|
||||
content: systemPrompt,
|
||||
}
|
||||
: {
|
||||
role: "system" as const,
|
||||
content: mainSystemPrompt({
|
||||
systemPrompt,
|
||||
previousRunningSummary,
|
||||
}),
|
||||
},
|
||||
...messagesSincePreviousRunningSummary.map((m) => ({
|
||||
role: m.role,
|
||||
content: m.parts
|
||||
.filter((p) => p.type === "text")
|
||||
.map((p) => p.text)
|
||||
.join(""),
|
||||
})),
|
||||
],
|
||||
tools: undefined,
|
||||
...parameters,
|
||||
});
|
||||
|
||||
// Emit status update
|
||||
yield {
|
||||
status: "extracting_facts_from_user",
|
||||
message: "Extracting facts from user message...",
|
||||
} as const;
|
||||
|
||||
/** 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.) */
|
||||
const factsFromUserMessageResponse =
|
||||
await factsCaller.extractFromNewMessages({
|
||||
previousRunningSummary,
|
||||
messagesSincePreviousRunningSummary: [],
|
||||
newMessages: messagesSincePreviousRunningSummary,
|
||||
});
|
||||
const insertedFactsFromUserMessage = await db.facts.createMany(
|
||||
factsFromUserMessageResponse.object.facts.map((fact) => ({
|
||||
userId: jwt?.id as string,
|
||||
sourceMessageId: insertedUserMessage.id,
|
||||
content: fact,
|
||||
}))
|
||||
);
|
||||
|
||||
// Emit status update
|
||||
yield {
|
||||
status: "generating_summary",
|
||||
message: "Generating conversation summary...",
|
||||
} as const;
|
||||
|
||||
/** Produce a running summary of the conversation, and save that along
|
||||
* with the model's response to the database. The new running summary is
|
||||
* based on the previous running summary combined with the all messages
|
||||
* since that summary was produced. */
|
||||
const runningSummaryResponse =
|
||||
await messagesCaller.generateRunningSummary({
|
||||
messagesSincePreviousRunningSummary,
|
||||
mainResponseContent: mainResponse.text,
|
||||
previousRunningSummary,
|
||||
});
|
||||
const insertedAssistantMessage = await db.messages.create({
|
||||
conversationId,
|
||||
// content: mainResponse.text,
|
||||
parts: [{ type: "text", text: mainResponse.text }],
|
||||
runningSummary: runningSummaryResponse.text,
|
||||
role: "assistant" as const,
|
||||
index: messages.length,
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
// Emit status update
|
||||
yield {
|
||||
status: "extracting_facts_from_assistant",
|
||||
message: "Extracting facts from assistant response...",
|
||||
} as const;
|
||||
|
||||
/** Extract Facts from the model's response, and add them to the database,
|
||||
* linking the Facts with the messages they came from. */
|
||||
const factsFromAssistantMessageResponse =
|
||||
await factsCaller.extractFromNewMessages({
|
||||
previousRunningSummary,
|
||||
messagesSincePreviousRunningSummary,
|
||||
newMessages: [
|
||||
{
|
||||
role: "assistant" as const,
|
||||
// content: mainResponse.text,
|
||||
parts: [{ type: "text", text: mainResponse.text }],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const insertedFactsFromAssistantMessage = await db.facts.createMany(
|
||||
factsFromAssistantMessageResponse.object.facts.map((factContent) => ({
|
||||
userId: jwt?.id as string,
|
||||
sourceMessageId: insertedAssistantMessage.id,
|
||||
content: factContent,
|
||||
createdAt: new Date().toISOString(),
|
||||
}))
|
||||
);
|
||||
|
||||
const insertedFacts = [
|
||||
...insertedFactsFromUserMessage,
|
||||
...insertedFactsFromAssistantMessage,
|
||||
];
|
||||
|
||||
// Emit status update
|
||||
yield {
|
||||
status: "generating_fact_triggers",
|
||||
message: "Generating fact triggers...",
|
||||
} as const;
|
||||
|
||||
/** 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 (const fact of insertedFacts) {
|
||||
const factTriggers = await factTriggerCaller.generateFromFact({
|
||||
mainResponseContent: mainResponse.text,
|
||||
previousRunningSummary,
|
||||
messagesSincePreviousRunningSummary,
|
||||
fact,
|
||||
});
|
||||
const insertedFactTriggers: Array<Omit<FactTrigger, "id">> =
|
||||
factTriggers.object.factTriggers.map((factTrigger) => ({
|
||||
sourceFactId: fact.id,
|
||||
content: factTrigger,
|
||||
priorityMultiplier: 1,
|
||||
priorityMultiplierReason: "",
|
||||
scopeConversationId: conversationId,
|
||||
createdAt: new Date().toISOString(),
|
||||
}));
|
||||
await db.factTriggers.createMany(insertedFactTriggers);
|
||||
}
|
||||
|
||||
// Emit final result
|
||||
yield {
|
||||
status: "completed",
|
||||
message: "Completed!",
|
||||
result: {
|
||||
insertedAssistantMessage,
|
||||
insertedUserMessage,
|
||||
insertedFacts,
|
||||
},
|
||||
} as const;
|
||||
}),
|
||||
});
|
||||
|
||||
export const createCaller = createCallerFactory(chat);
|
||||
@@ -0,0 +1,15 @@
|
||||
// import { createTRPCProxyClient, httpBatchLink } from "@trpc/client";
|
||||
import { createTRPCContext } from "@trpc/tanstack-react-query";
|
||||
import type { AppRouter } from "../server/trpc/router.js";
|
||||
|
||||
export const { TRPCProvider, useTRPC, useTRPCClient } =
|
||||
createTRPCContext<AppRouter>();
|
||||
|
||||
// export const trpc = createTRPCProxyClient<AppRouter>({
|
||||
// links: [
|
||||
// httpBatchLink({
|
||||
// url: "/api/trpc",
|
||||
// methodOverride: "POST",
|
||||
// }),
|
||||
// ],
|
||||
// });
|
||||
Reference in New Issue
Block a user