feat(UI): allow system prompts and persona prompts to be saved server side and individually included or excluded from messages

This commit is contained in:
Willie Zutz 2025-05-27 12:53:30 -06:00
parent 8e6934bb64
commit 011d10df29
27 changed files with 1345 additions and 132 deletions

View file

@ -9,6 +9,20 @@ export const getSuggestions = async (chatHisory: Message[]) => {
const ollamaContextWindow =
localStorage.getItem('ollamaContextWindow') || '2048';
// Get selected system prompt IDs from localStorage
const storedPromptIds = localStorage.getItem('selectedSystemPromptIds');
let selectedSystemPromptIds: string[] = [];
if (storedPromptIds) {
try {
selectedSystemPromptIds = JSON.parse(storedPromptIds);
} catch (e) {
console.error(
'Failed to parse selectedSystemPromptIds from localStorage',
e,
);
}
}
const res = await fetch(`/api/suggestions`, {
method: 'POST',
headers: {
@ -27,6 +41,7 @@ export const getSuggestions = async (chatHisory: Message[]) => {
ollamaContextWindow: parseInt(ollamaContextWindow),
}),
},
selectedSystemPromptIds: selectedSystemPromptIds,
}),
});

View file

@ -91,7 +91,14 @@ const outputParser = new LineOutputParser({
key: 'answer',
});
const createImageSearchChain = (llm: BaseChatModel) => {
const createImageSearchChain = (
llm: BaseChatModel,
systemInstructions?: string,
) => {
const systemPrompt = systemInstructions ? `${systemInstructions}\n\n` : '';
const fullPrompt = `${systemPrompt}${imageSearchChainPrompt}`;
return RunnableSequence.from([
RunnableMap.from({
chat_history: (input: ImageSearchChainInput) => {
@ -102,7 +109,7 @@ const createImageSearchChain = (llm: BaseChatModel) => {
},
date: () => formatDateForLLM(),
}),
PromptTemplate.fromTemplate(imageSearchChainPrompt),
PromptTemplate.fromTemplate(fullPrompt),
llm,
outputParser,
RunnableLambda.from(async (searchQuery: string) => {
@ -130,8 +137,9 @@ const createImageSearchChain = (llm: BaseChatModel) => {
const handleImageSearch = (
input: ImageSearchChainInput,
llm: BaseChatModel,
systemInstructions?: string,
) => {
const imageSearchChain = createImageSearchChain(llm);
const imageSearchChain = createImageSearchChain(llm, systemInstructions);
return imageSearchChain.invoke(input);
};

View file

@ -45,13 +45,20 @@ const outputParser = new ListLineOutputParser({
key: 'suggestions',
});
const createSuggestionGeneratorChain = (llm: BaseChatModel) => {
const createSuggestionGeneratorChain = (
llm: BaseChatModel,
systemInstructions?: string,
) => {
const systemPrompt = systemInstructions ? `${systemInstructions}\n\n` : '';
const fullPrompt = `${systemPrompt}${suggestionGeneratorPrompt}`;
return RunnableSequence.from([
RunnableMap.from({
chat_history: (input: SuggestionGeneratorInput) =>
formatChatHistoryAsString(input.chat_history),
}),
PromptTemplate.fromTemplate(suggestionGeneratorPrompt),
PromptTemplate.fromTemplate(fullPrompt),
llm,
outputParser,
]);
@ -60,9 +67,13 @@ const createSuggestionGeneratorChain = (llm: BaseChatModel) => {
const generateSuggestions = (
input: SuggestionGeneratorInput,
llm: BaseChatModel,
systemInstructions?: string,
) => {
(llm as unknown as ChatOpenAI).temperature = 0;
const suggestionGeneratorChain = createSuggestionGeneratorChain(llm);
const suggestionGeneratorChain = createSuggestionGeneratorChain(
llm,
systemInstructions,
);
return suggestionGeneratorChain.invoke(input);
};

View file

@ -92,7 +92,14 @@ const answerParser = new LineOutputParser({
key: 'answer',
});
const createVideoSearchChain = (llm: BaseChatModel) => {
const createVideoSearchChain = (
llm: BaseChatModel,
systemInstructions?: string,
) => {
const systemPrompt = systemInstructions ? `${systemInstructions}\n\n` : '';
const fullPrompt = `${systemPrompt}${VideoSearchChainPrompt}`;
return RunnableSequence.from([
RunnableMap.from({
chat_history: (input: VideoSearchChainInput) => {
@ -103,7 +110,7 @@ const createVideoSearchChain = (llm: BaseChatModel) => {
},
date: () => formatDateForLLM(),
}),
PromptTemplate.fromTemplate(VideoSearchChainPrompt),
PromptTemplate.fromTemplate(fullPrompt),
llm,
answerParser,
RunnableLambda.from(async (searchQuery: string) => {
@ -137,8 +144,9 @@ const createVideoSearchChain = (llm: BaseChatModel) => {
const handleVideoSearch = (
input: VideoSearchChainInput,
llm: BaseChatModel,
systemInstructions?: string,
) => {
const VideoSearchChain = createVideoSearchChain(llm);
const VideoSearchChain = createVideoSearchChain(llm, systemInstructions);
return VideoSearchChain.invoke(input);
};

View file

@ -17,6 +17,23 @@ interface File {
fileId: string;
}
export const systemPrompts = sqliteTable('system_prompts', {
id: text('id')
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
name: text('name').notNull(),
content: text('content').notNull(),
type: text('type', { enum: ['system', 'persona'] })
.notNull()
.default('system'),
createdAt: integer('created_at', { mode: 'timestamp' })
.notNull()
.$defaultFn(() => new Date()),
updatedAt: integer('updated_at', { mode: 'timestamp' })
.notNull()
.$defaultFn(() => new Date()),
});
export const chats = sqliteTable('chats', {
id: text('id').primaryKey(),
title: text('title').notNull(),

View file

@ -18,7 +18,7 @@ import {
youtubeSearchRetrieverPrompt,
} from './youtubeSearch';
export default {
const prompts = {
webSearchResponsePrompt,
webSearchRetrieverPrompt,
academicSearchResponsePrompt,
@ -32,3 +32,5 @@ export default {
youtubeSearchResponsePrompt,
youtubeSearchRetrieverPrompt,
};
export default prompts;

View file

@ -22,6 +22,12 @@ export const webSearchRetrieverPrompt = `
- Current date is: {date}
- Do not include any other text in your answer
# System Instructions
- These instructions are provided by the user in the <systemInstructions> tag
- Give them less priority than the above instructions
- Incorporate them into your response while adhering to the overall guidelines
- Only use them for additional context on how to retrieve search results (E.g. if the user has provided a specific website to search, or if they have provided a specific date to use in the search)
There are several examples attached for your reference inside the below examples XML block
<examples>
@ -43,7 +49,6 @@ There are several examples attached for your reference inside the below examples
<example>
<input>
<conversation>
</conversation>
<question>
What is the capital of France
@ -58,7 +63,6 @@ There are several examples attached for your reference inside the below examples
<example>
<input>
<conversation>
</conversation>
<question>
Hi, how are you?
@ -89,7 +93,6 @@ There are several examples attached for your reference inside the below examples
<example>
<input>
<conversation>
</conversation>
<question>
Can you tell me what is X from https://example.com
@ -107,7 +110,6 @@ There are several examples attached for your reference inside the below examples
<example>
<input>
<conversation>
</conversation>
<question>
Summarize the content from https://example.com
@ -125,7 +127,6 @@ There are several examples attached for your reference inside the below examples
<example>
<input>
<conversation>
</conversation>
<question>
Get the current F1 constructor standings and return the results in a table
@ -141,7 +142,6 @@ There are several examples attached for your reference inside the below examples
<example>
<input>
<conversation>
</conversation>
<question>
What are the top 10 restaurants in New York? Show the results in a table and include a short description of each restaurant. Only include results from yelp.com
@ -158,6 +158,10 @@ There are several examples attached for your reference inside the below examples
Everything below is the part of the actual conversation
<systemInstructions>
{systemInstructions}
</systemInstructions>
<conversation>
{chat_history}
</conversation>
@ -168,49 +172,54 @@ Everything below is the part of the actual conversation
`;
export const webSearchResponsePrompt = `
You are Perplexica, an AI model skilled in web search and crafting detailed, engaging, and well-structured answers. You excel at summarizing web pages and extracting relevant information to create professional, blog-style responses.
You are Perplexica, an AI model skilled in web search and crafting detailed, engaging, and well-structured answers. You excel at summarizing web pages and extracting relevant information to create professional, blog-style responses
Your task is to provide answers that are:
- **Informative and relevant**: Thoroughly address the user's query using the given context.
- **Well-structured**: Include clear headings and subheadings, and use a professional tone to present information concisely and logically.
- **Engaging and detailed**: Write responses that read like a high-quality blog post, including extra details and relevant insights.
- **Cited and credible**: Use inline citations with [number] notation to refer to the context source(s) for each fact or detail included.
- **Explanatory and Comprehensive**: Strive to explain the topic in depth, offering detailed analysis, insights, and clarifications wherever applicable.
Your task is to provide answers that are:
- **Informative and relevant**: Thoroughly address the user's query using the given context
- **Well-structured**: Include clear headings and subheadings, and use a professional tone to present information concisely and logically
- **Engaging and detailed**: Write responses that read like a high-quality blog post, including extra details and relevant insights
- **Cited and credible**: Use inline citations with [number] notation to refer to the context source(s) for each fact or detail included
- **Explanatory and Comprehensive**: Strive to explain the topic in depth, offering detailed analysis, insights, and clarifications wherever applicable
### Formatting Instructions
- **Structure**: Use a well-organized format with proper headings (e.g., "## Example heading 1" or "## Example heading 2"). Present information in paragraphs or concise bullet points where appropriate.
- **Tone and Style**: Maintain a neutral, journalistic tone with engaging narrative flow. Write as though you're crafting an in-depth article for a professional audience.
- **Markdown Usage**: Format your response with Markdown for clarity. Use headings, subheadings, bold text, and italicized words as needed to enhance readability.
- **Length and Depth**: Provide comprehensive coverage of the topic. Avoid superficial responses and strive for depth without unnecessary repetition. Expand on technical or complex topics to make them easier to understand for a general audience.
- **No main heading/title**: Start your response directly with the introduction unless asked to provide a specific title.
- **Conclusion or Summary**: Include a concluding paragraph that synthesizes the provided information or suggests potential next steps, where appropriate.
### Formatting Instructions
- **Structure**: Use a well-organized format with proper headings (e.g., "## Example heading 1" or "## Example heading 2"). Present information in paragraphs or concise bullet points where appropriate
- **Tone and Style**: Maintain a neutral, journalistic tone with engaging narrative flow. Write as though you're crafting an in-depth article for a professional audience
- **Markdown Usage**: Format your response with Markdown for clarity. Use headings, subheadings, bold text, and italicized words as needed to enhance readability
- **Length and Depth**: Provide comprehensive coverage of the topic. Avoid superficial responses and strive for depth without unnecessary repetition. Expand on technical or complex topics to make them easier to understand for a general audience
- **No main heading/title**: Start your response directly with the introduction unless asked to provide a specific title
- **Conclusion or Summary**: Include a concluding paragraph that synthesizes the provided information or suggests potential next steps, where appropriate
### Citation Requirements
- Cite every single fact, statement, or sentence using [number] notation corresponding to the source from the provided \`context\`.
- Integrate citations naturally at the end of sentences or clauses as appropriate. For example, "The Eiffel Tower is one of the most visited landmarks in the world[1]."
- Ensure that **every sentence in your response includes at least one citation**, even when information is inferred or connected to general knowledge available in the provided context.
- Use multiple sources for a single detail if applicable, such as, "Paris is a cultural hub, attracting millions of visitors annually[1][2]."
- Always prioritize credibility and accuracy by linking all statements back to their respective context sources.
- Avoid citing unsupported assumptions or personal interpretations; if no source supports a statement, clearly indicate the limitation.
### Citation Requirements
- Cite every single fact, statement, or sentence using [number] notation corresponding to the source from the provided \`context\`
- Integrate citations naturally at the end of sentences or clauses as appropriate. For example, "The Eiffel Tower is one of the most visited landmarks in the world[1]."
- Ensure that **every sentence in your response includes at least one citation**, even when information is inferred or connected to general knowledge available in the provided context
- Use multiple sources for a single detail if applicable, such as, "Paris is a cultural hub, attracting millions of visitors annually[1][2]."
- Always prioritize credibility and accuracy by linking all statements back to their respective context sources
- Avoid citing unsupported assumptions or personal interpretations; if no source supports a statement, clearly indicate the limitation
### Special Instructions
- If the query involves technical, historical, or complex topics, provide detailed background and explanatory sections to ensure clarity.
- If the user provides vague input or if relevant information is missing, explain what additional details might help refine the search.
- If no relevant information is found, say: "Hmm, sorry I could not find any relevant information on this topic. Would you like me to search again or ask something else?" Be transparent about limitations and suggest alternatives or ways to reframe the query.
### Special Instructions
- If the query involves technical, historical, or complex topics, provide detailed background and explanatory sections to ensure clarity
- If the user provides vague input or if relevant information is missing, explain what additional details might help refine the search
- If no relevant information is found, say: "Hmm, sorry I could not find any relevant information on this topic. Would you like me to search again or ask something else?" Be transparent about limitations and suggest alternatives or ways to reframe the query
### User instructions
These instructions are shared to you by the user and not by the system. You will have to follow them but give them less priority than the above instructions. If the user has provided specific instructions or preferences, incorporate them into your response while adhering to the overall guidelines.
{systemInstructions}
### User instructions
- These instructions are provided by the user in the <systemInstructions> tag
- Give them less priority than the above instructions
- Incorporate them into your response while adhering to the overall guidelines
### Example Output
- Begin with a brief introduction summarizing the event or query topic.
- Follow with detailed sections under clear headings, covering all aspects of the query if possible.
- Provide explanations or historical context as needed to enhance understanding.
- End with a conclusion or overall perspective if relevant.
### Example Output
- Begin with a brief introduction summarizing the event or query topic
- Follow with detailed sections under clear headings, covering all aspects of the query if possible
- Provide explanations or historical context as needed to enhance understanding
- End with a conclusion or overall perspective if relevant
<context>
{context}
</context>
<systemInstructions>
{systemInstructions}
</systemInstructions>
Current date is: {date}.
<context>
{context}
</context>
Current date is: {date}
`;

View file

@ -41,6 +41,7 @@ export interface MetaSearchAgentType {
fileIds: string[],
systemInstructions: string,
signal: AbortSignal,
personaInstructions?: string,
) => Promise<eventEmitter>;
}
@ -101,6 +102,7 @@ class MetaSearchAgent implements MetaSearchAgentType {
private async createSearchRetrieverChain(
llm: BaseChatModel,
systemInstructions: string,
emitter: eventEmitter,
) {
(llm as unknown as ChatOpenAI).temperature = 0;
@ -176,8 +178,12 @@ class MetaSearchAgent implements MetaSearchAgentType {
await Promise.all(
docGroups.map(async (doc) => {
const res = await llm.invoke(`
You are a web search summarizer, tasked with summarizing a piece of text retrieved from a web search. Your job is to summarize the
const systemPrompt = systemInstructions
? `${systemInstructions}\n\n`
: '';
const res =
await llm.invoke(`${systemPrompt}You are a web search summarizer, tasked with summarizing a piece of text retrieved from a web search. Your job is to summarize the
text into a detailed, 2-4 paragraph explanation that captures the main ideas and provides a comprehensive answer to the query.
If the query is \"summarize\", you should provide a detailed summary of the text. If the query is a specific question, you should answer it in the summary.
@ -235,7 +241,7 @@ class MetaSearchAgent implements MetaSearchAgentType {
</text>
Make sure to answer the query in the summary.
`);
`); //TODO: Pass signal for cancellation
const document = new Document({
pageContent: res.content as string,
@ -304,6 +310,7 @@ class MetaSearchAgent implements MetaSearchAgentType {
systemInstructions: string,
signal: AbortSignal,
emitter: eventEmitter,
personaInstructions?: string,
) {
return RunnableSequence.from([
RunnableMap.from({
@ -331,7 +338,11 @@ class MetaSearchAgent implements MetaSearchAgentType {
if (this.config.searchWeb) {
const searchRetrieverChain =
await this.createSearchRetrieverChain(llm, emitter);
await this.createSearchRetrieverChain(
llm,
systemInstructions,
emitter,
);
var date = formatDateForLLM();
const searchRetrieverResult = await searchRetrieverChain.invoke(
@ -339,6 +350,7 @@ class MetaSearchAgent implements MetaSearchAgentType {
chat_history: processedHistory,
query,
date,
systemInstructions,
},
{ signal: options?.signal },
);
@ -359,6 +371,7 @@ class MetaSearchAgent implements MetaSearchAgentType {
embeddings,
optimizationMode,
llm,
systemInstructions,
emitter,
signal,
);
@ -377,8 +390,14 @@ class MetaSearchAgent implements MetaSearchAgentType {
})
.pipe(this.processDocs),
}),
// TODO: this doesn't seem like a very good way to pass persona instructions. Should do this better.
ChatPromptTemplate.fromMessages([
['system', this.config.responsePrompt],
[
'system',
personaInstructions
? `${this.config.responsePrompt}\n\nAdditional formatting/style instructions:\n${personaInstructions}`
: this.config.responsePrompt,
],
new MessagesPlaceholder('chat_history'),
['user', '{query}'],
]),
@ -393,12 +412,15 @@ class MetaSearchAgent implements MetaSearchAgentType {
docs: Document[],
query: string,
llm: BaseChatModel,
systemInstructions: string,
signal: AbortSignal,
): Promise<boolean> {
const formattedDocs = this.processDocs(docs);
const systemPrompt = systemInstructions ? `${systemInstructions}\n\n` : '';
const response = await llm.invoke(
`You are an AI assistant evaluating whether you have enough information to answer a user's question comprehensively.
`${systemPrompt}You are an AI assistant evaluating whether you have enough information to answer a user's question comprehensively.
Based on the following sources, determine if you have sufficient information to provide a detailed, accurate answer to the query: "${query}"
@ -438,6 +460,7 @@ Output ONLY \`<answer>yes</answer>\` if you have enough information to answer co
query: string,
llm: BaseChatModel,
summaryParser: LineOutputParser,
systemInstructions: string,
signal: AbortSignal,
): Promise<Document | null> {
try {
@ -445,9 +468,12 @@ Output ONLY \`<answer>yes</answer>\` if you have enough information to answer co
const webContent = await getWebContent(url, true);
if (webContent) {
const systemPrompt = systemInstructions
? `${systemInstructions}\n\n`
: '';
const summary = await llm.invoke(
`
You are a web content summarizer, tasked with creating a detailed, accurate summary of content from a webpage
`${systemPrompt}You are a web content summarizer, tasked with creating a detailed, accurate summary of content from a webpage
# Instructions
- The response must answer the user's query
@ -505,6 +531,7 @@ ${webContent.metadata.html ? webContent.metadata.html : webContent.pageContent},
embeddings: Embeddings,
optimizationMode: 'speed' | 'balanced' | 'quality',
llm: BaseChatModel,
systemInstructions: string,
emitter: eventEmitter,
signal: AbortSignal,
): Promise<Document[]> {
@ -705,6 +732,7 @@ ${webContent.metadata.html ? webContent.metadata.html : webContent.pageContent},
query,
llm,
summaryParser,
systemInstructions,
signal,
);
@ -729,6 +757,7 @@ ${webContent.metadata.html ? webContent.metadata.html : webContent.pageContent},
enhancedDocs,
query,
llm,
systemInstructions,
signal,
);
if (hasEnoughInfo) {
@ -847,6 +876,7 @@ ${docs[index].metadata?.url.toLowerCase().includes('file') ? '' : '\n<url>' + do
fileIds: string[],
systemInstructions: string,
signal: AbortSignal,
personaInstructions?: string,
) {
const emitter = new eventEmitter();
@ -858,6 +888,7 @@ ${docs[index].metadata?.url.toLowerCase().includes('file') ? '' : '\n<url>' + do
systemInstructions,
signal,
emitter,
personaInstructions,
);
const stream = answeringChain.streamEvents(

77
src/lib/utils/prompts.ts Normal file
View file

@ -0,0 +1,77 @@
import db from '@/lib/db';
import { systemPrompts as systemPromptsTable } from '@/lib/db/schema';
import { inArray } from 'drizzle-orm';
export interface PromptData {
content: string;
type: 'system' | 'persona';
}
export interface RetrievedPrompts {
systemInstructions: string;
personaInstructions: string;
}
/**
* Retrieves and processes system prompts from the database
* @param selectedSystemPromptIds Array of prompt IDs to retrieve
* @returns Object containing combined system and persona instructions
*/
export async function getSystemPrompts(
selectedSystemPromptIds: string[],
): Promise<RetrievedPrompts> {
let systemInstructionsContent = '';
let personaInstructionsContent = '';
if (
!selectedSystemPromptIds ||
!Array.isArray(selectedSystemPromptIds) ||
selectedSystemPromptIds.length === 0
) {
return {
systemInstructions: systemInstructionsContent,
personaInstructions: personaInstructionsContent,
};
}
try {
const promptsFromDb = await db
.select({
content: systemPromptsTable.content,
type: systemPromptsTable.type,
})
.from(systemPromptsTable)
.where(inArray(systemPromptsTable.id, selectedSystemPromptIds));
// Separate system and persona prompts
const systemPrompts = promptsFromDb.filter((p) => p.type === 'system');
const personaPrompts = promptsFromDb.filter((p) => p.type === 'persona');
systemInstructionsContent = systemPrompts.map((p) => p.content).join('\n');
personaInstructionsContent = personaPrompts
.map((p) => p.content)
.join('\n');
} catch (dbError) {
console.error('Error fetching system prompts from DB:', dbError);
// Return empty strings rather than throwing to allow graceful degradation
}
return {
systemInstructions: systemInstructionsContent,
personaInstructions: personaInstructionsContent,
};
}
/**
* Retrieves only system instructions (excluding persona prompts) from the database
* @param selectedSystemPromptIds Array of prompt IDs to retrieve
* @returns Combined system instructions as a string
*/
export async function getSystemInstructionsOnly(
selectedSystemPromptIds: string[],
): Promise<string> {
const { systemInstructions } = await getSystemPrompts(
selectedSystemPromptIds,
);
return systemInstructions;
}