mirror of
https://github.com/ItzCrazyKns/Perplexica.git
synced 2025-11-21 20:48:14 +00:00
Compare commits
10 Commits
1614cfa5e5
...
feat/impro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55cf88822d | ||
|
|
c4acc83fd5 | ||
|
|
08feb18197 | ||
|
|
0df0114e76 | ||
|
|
4016b21bdf | ||
|
|
f7a43b3cb9 | ||
|
|
70bcd8c6f1 | ||
|
|
2568088341 | ||
|
|
a494d4c329 | ||
|
|
9b85c63a80 |
15
drizzle/0002_daffy_wrecker.sql
Normal file
15
drizzle/0002_daffy_wrecker.sql
Normal file
@@ -0,0 +1,15 @@
|
||||
PRAGMA foreign_keys=OFF;--> statement-breakpoint
|
||||
CREATE TABLE `__new_messages` (
|
||||
`id` integer PRIMARY KEY NOT NULL,
|
||||
`messageId` text NOT NULL,
|
||||
`chatId` text NOT NULL,
|
||||
`backendId` text NOT NULL,
|
||||
`query` text NOT NULL,
|
||||
`createdAt` text NOT NULL,
|
||||
`responseBlocks` text DEFAULT '[]',
|
||||
`status` text DEFAULT 'answering'
|
||||
);
|
||||
--> statement-breakpoint
|
||||
DROP TABLE `messages`;--> statement-breakpoint
|
||||
ALTER TABLE `__new_messages` RENAME TO `messages`;--> statement-breakpoint
|
||||
PRAGMA foreign_keys=ON;
|
||||
132
drizzle/meta/0002_snapshot.json
Normal file
132
drizzle/meta/0002_snapshot.json
Normal file
@@ -0,0 +1,132 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "1c5eb804-d6b4-48ec-9a8f-75fb729c8e52",
|
||||
"prevId": "6dedf55f-0e44-478f-82cf-14a21ac686f8",
|
||||
"tables": {
|
||||
"chats": {
|
||||
"name": "chats",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"title": {
|
||||
"name": "title",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"focusMode": {
|
||||
"name": "focusMode",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"files": {
|
||||
"name": "files",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'[]'"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"messages": {
|
||||
"name": "messages",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"messageId": {
|
||||
"name": "messageId",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"chatId": {
|
||||
"name": "chatId",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"backendId": {
|
||||
"name": "backendId",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"query": {
|
||||
"name": "query",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"responseBlocks": {
|
||||
"name": "responseBlocks",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'[]'"
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'answering'"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,13 @@
|
||||
"when": 1758863991284,
|
||||
"tag": "0001_wise_rockslide",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 2,
|
||||
"version": "6",
|
||||
"when": 1763732708332,
|
||||
"tag": "0002_daffy_wrecker",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -40,11 +40,15 @@
|
||||
"markdown-to-jsx": "^7.7.2",
|
||||
"next": "^15.2.2",
|
||||
"next-themes": "^0.3.0",
|
||||
"ollama": "^0.6.3",
|
||||
"openai": "^6.9.0",
|
||||
"partial-json": "^0.1.7",
|
||||
"pdf-parse": "^1.1.1",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"react-text-to-speech": "^0.14.5",
|
||||
"react-textarea-autosize": "^8.5.3",
|
||||
"rfc6902": "^5.1.2",
|
||||
"sonner": "^1.4.41",
|
||||
"tailwind-merge": "^2.2.2",
|
||||
"winston": "^3.17.0",
|
||||
|
||||
48
src/lib/agents/search/index.ts
Normal file
48
src/lib/agents/search/index.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { ResearcherOutput, SearchAgentInput } from './types';
|
||||
import SessionManager from '@/lib/session';
|
||||
import Classifier from './classifier';
|
||||
import { WidgetRegistry } from './widgets';
|
||||
import Researcher from './researcher';
|
||||
|
||||
class SearchAgent {
|
||||
async searchAsync(session: SessionManager, input: SearchAgentInput) {
|
||||
const classifier = new Classifier();
|
||||
|
||||
const classification = await classifier.classify({
|
||||
chatHistory: input.chatHistory,
|
||||
enabledSources: input.config.sources,
|
||||
query: input.followUp,
|
||||
llm: input.config.llm,
|
||||
});
|
||||
|
||||
session.emit('data', {
|
||||
type: 'classification',
|
||||
classification: classification,
|
||||
});
|
||||
|
||||
const widgetPromise = WidgetRegistry.executeAll(classification.widgets, {
|
||||
llm: input.config.llm,
|
||||
embedding: input.config.embedding,
|
||||
session: session,
|
||||
});
|
||||
|
||||
let searchPromise: Promise<ResearcherOutput> | null = null;
|
||||
|
||||
if (!classification.skipSearch) {
|
||||
const researcher = new Researcher();
|
||||
searchPromise = researcher.research(session, {
|
||||
chatHistory: input.chatHistory,
|
||||
followUp: input.followUp,
|
||||
classification: classification,
|
||||
config: input.config,
|
||||
});
|
||||
}
|
||||
|
||||
const [widgetOutputs, searchResults] = await Promise.all([
|
||||
widgetPromise,
|
||||
searchPromise,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
export default SearchAgent;
|
||||
19
src/lib/agents/search/researcher/actions/done.ts
Normal file
19
src/lib/agents/search/researcher/actions/done.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import z from 'zod';
|
||||
import { ResearchAction } from '../../types';
|
||||
|
||||
const doneAction: ResearchAction<any> = {
|
||||
name: 'done',
|
||||
description:
|
||||
"Indicates that the research process is complete and no further actions are needed. Use this action when you have gathered sufficient information to answer the user's query.",
|
||||
enabled: (_) => true,
|
||||
schema: z.object({
|
||||
type: z.literal('done'),
|
||||
}),
|
||||
execute: async (params, additionalConfig) => {
|
||||
return {
|
||||
type: 'done',
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default doneAction;
|
||||
8
src/lib/agents/search/researcher/actions/index.ts
Normal file
8
src/lib/agents/search/researcher/actions/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import doneAction from './done';
|
||||
import ActionRegistry from './registry';
|
||||
import webSearchAction from './webSearch';
|
||||
|
||||
ActionRegistry.register(webSearchAction);
|
||||
ActionRegistry.register(doneAction);
|
||||
|
||||
export { ActionRegistry };
|
||||
73
src/lib/agents/search/researcher/actions/registry.ts
Normal file
73
src/lib/agents/search/researcher/actions/registry.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import {
|
||||
ActionConfig,
|
||||
ActionOutput,
|
||||
AdditionalConfig,
|
||||
ClassifierOutput,
|
||||
ResearchAction,
|
||||
} from '../../types';
|
||||
|
||||
class ActionRegistry {
|
||||
private static actions: Map<string, ResearchAction> = new Map();
|
||||
|
||||
static register(action: ResearchAction<any>) {
|
||||
this.actions.set(action.name, action);
|
||||
}
|
||||
|
||||
static get(name: string): ResearchAction | undefined {
|
||||
return this.actions.get(name);
|
||||
}
|
||||
|
||||
static getAvailableActions(config: {
|
||||
classification: ClassifierOutput;
|
||||
}): ResearchAction[] {
|
||||
return Array.from(
|
||||
this.actions.values().filter((action) => action.enabled(config)),
|
||||
);
|
||||
}
|
||||
|
||||
static getAvailableActionsDescriptions(config: {
|
||||
classification: ClassifierOutput;
|
||||
}): string {
|
||||
const availableActions = this.getAvailableActions(config);
|
||||
|
||||
return availableActions
|
||||
.map((action) => `------------\n##${action.name}\n${action.description}`)
|
||||
.join('\n\n');
|
||||
}
|
||||
|
||||
static async execute(
|
||||
name: string,
|
||||
params: any,
|
||||
additionalConfig: AdditionalConfig,
|
||||
) {
|
||||
const action = this.actions.get(name);
|
||||
|
||||
if (!action) {
|
||||
throw new Error(`Action with name ${name} not found`);
|
||||
}
|
||||
|
||||
return action.execute(params, additionalConfig);
|
||||
}
|
||||
|
||||
static async executeAll(
|
||||
actions: ActionConfig[],
|
||||
additionalConfig: AdditionalConfig,
|
||||
): Promise<ActionOutput[]> {
|
||||
const results: ActionOutput[] = [];
|
||||
|
||||
await Promise.all(
|
||||
actions.map(async (actionConfig) => {
|
||||
const output = await this.execute(
|
||||
actionConfig.type,
|
||||
actionConfig.params,
|
||||
additionalConfig,
|
||||
);
|
||||
results.push(output);
|
||||
}),
|
||||
);
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
export default ActionRegistry;
|
||||
54
src/lib/agents/search/researcher/actions/webSearch.ts
Normal file
54
src/lib/agents/search/researcher/actions/webSearch.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import z from 'zod';
|
||||
import { ResearchAction } from '../../types';
|
||||
import { searchSearxng } from '@/lib/searxng';
|
||||
|
||||
const actionSchema = z.object({
|
||||
type: z.literal('web_search'),
|
||||
queries: z
|
||||
.array(z.string())
|
||||
.describe('An array of search queries to perform web searches for.'),
|
||||
});
|
||||
|
||||
const actionDescription = `
|
||||
You have to use this action aggressively to find relevant information from the web to answer user queries. You can combine this action with other actions to gather comprehensive data. Always ensure that you provide accurate and up-to-date information by leveraging web search results.
|
||||
When this action is present, you must use it to obtain current information from the web.
|
||||
|
||||
### How to use:
|
||||
1. For fast search mode, you can use this action once. Make sure to cover all aspects of the user's query in that single search.
|
||||
2. If you're on quality mode, you'll get to use this action up to two times. Use the first search to gather general information, and the second search to fill in any gaps or get more specific details based on the initial findings.
|
||||
3. If you're set on Deep research mode, then you will get to use this action multiple times to gather more information. Use your judgment to decide when additional searches are necessary to provide a thorough and accurate response.
|
||||
|
||||
Input: An array of search queries. Make sure the queries are relevant to the user's request and cover different aspects if necessary. You can include a maximum of 3 queries. Make sure the queries are SEO friendly and not sentences rather keywords which can be used to search a search engine like Google, Bing, etc.
|
||||
`;
|
||||
|
||||
const webSearchAction: ResearchAction<typeof actionSchema> = {
|
||||
name: 'web_search',
|
||||
description: actionDescription,
|
||||
schema: actionSchema,
|
||||
enabled: (config) => config.classification.intents.includes('web_search'),
|
||||
execute: async (input, _) => {
|
||||
let results: Chunk[] = [];
|
||||
|
||||
const search = async (q: string) => {
|
||||
const res = await searchSearxng(q);
|
||||
res.results.forEach((r) => {
|
||||
results.push({
|
||||
content: r.content || r.title,
|
||||
metadata: {
|
||||
title: r.title,
|
||||
url: r.url,
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
await Promise.all(input.queries.map(search));
|
||||
|
||||
return {
|
||||
type: 'search_results',
|
||||
results,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default webSearchAction;
|
||||
113
src/lib/agents/search/researcher/index.ts
Normal file
113
src/lib/agents/search/researcher/index.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import z from 'zod';
|
||||
import {
|
||||
ActionConfig,
|
||||
ActionOutput,
|
||||
ResearcherInput,
|
||||
ResearcherOutput,
|
||||
} from '../types';
|
||||
import { ActionRegistry } from './actions';
|
||||
import { getResearcherPrompt } from '@/lib/prompts/search/researcher';
|
||||
import SessionManager from '@/lib/session';
|
||||
|
||||
class Researcher {
|
||||
async research(
|
||||
session: SessionManager,
|
||||
input: ResearcherInput,
|
||||
): Promise<ResearcherOutput> {
|
||||
let findings: string = '';
|
||||
let actionOutput: ActionOutput[] = [];
|
||||
let maxIteration =
|
||||
input.config.mode === 'fast'
|
||||
? 1
|
||||
: input.config.mode === 'balanced'
|
||||
? 3
|
||||
: 25;
|
||||
|
||||
const availableActions = ActionRegistry.getAvailableActions({
|
||||
classification: input.classification,
|
||||
});
|
||||
|
||||
const schema = z.object({
|
||||
reasoning: z
|
||||
.string()
|
||||
.describe('The reasoning behind choosing the next action.'),
|
||||
action: z
|
||||
.union(availableActions.map((a) => a.schema))
|
||||
.describe('The action to be performed next.'),
|
||||
});
|
||||
|
||||
const availableActionsDescription =
|
||||
ActionRegistry.getAvailableActionsDescriptions({
|
||||
classification: input.classification,
|
||||
});
|
||||
|
||||
for (let i = 0; i < maxIteration; i++) {
|
||||
const researcherPrompt = getResearcherPrompt(availableActionsDescription);
|
||||
|
||||
const res = await input.config.llm.generateObject<z.infer<typeof schema>>(
|
||||
{
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: researcherPrompt,
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: `
|
||||
<research_query>
|
||||
${input.classification.standaloneFollowUp}
|
||||
</research_query>
|
||||
|
||||
<previous_actions>
|
||||
${findings}
|
||||
</previous_actions>
|
||||
`,
|
||||
},
|
||||
],
|
||||
schema,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
if (res.action.type === 'done') {
|
||||
console.log('Research complete - "done" action selected');
|
||||
break;
|
||||
}
|
||||
|
||||
const actionConfig: ActionConfig = {
|
||||
type: res.action.type as string,
|
||||
params: res.action,
|
||||
};
|
||||
|
||||
findings += 'Reasoning: ' + res.reasoning + '\n';
|
||||
findings += `Executing Action: ${actionConfig.type} with params ${JSON.stringify(actionConfig.params)}\n`;
|
||||
|
||||
const actionResult = await ActionRegistry.execute(
|
||||
actionConfig.type,
|
||||
actionConfig.params,
|
||||
{
|
||||
llm: input.config.llm,
|
||||
embedding: input.config.embedding,
|
||||
session: session,
|
||||
},
|
||||
);
|
||||
|
||||
actionOutput.push(actionResult);
|
||||
|
||||
if (actionResult.type === 'search_results') {
|
||||
findings += actionResult.results
|
||||
.map(
|
||||
(r) =>
|
||||
`Title: ${r.metadata.title}\nURL: ${r.metadata.url}\nContent: ${r.content}\n`,
|
||||
)
|
||||
.join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
findings: actionOutput,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default Researcher;
|
||||
@@ -1,7 +1,7 @@
|
||||
import { EventEmitter } from 'stream';
|
||||
import z from 'zod';
|
||||
import BaseLLM from '../../models/base/llm';
|
||||
import BaseEmbedding from '@/lib/models/base/embedding';
|
||||
import SessionManager from '@/lib/session';
|
||||
|
||||
export type SearchSources = 'web' | 'discussions' | 'academic';
|
||||
|
||||
@@ -9,10 +9,11 @@ export type SearchAgentConfig = {
|
||||
sources: SearchSources[];
|
||||
llm: BaseLLM<any>;
|
||||
embedding: BaseEmbedding<any>;
|
||||
mode: 'fast' | 'balanced' | 'deep_research';
|
||||
};
|
||||
|
||||
export type SearchAgentInput = {
|
||||
chatHistory: Message[];
|
||||
chatHistory: ChatTurnMessage[];
|
||||
followUp: string;
|
||||
config: SearchAgentConfig;
|
||||
};
|
||||
@@ -48,7 +49,7 @@ export type ClassifierInput = {
|
||||
llm: BaseLLM<any>;
|
||||
enabledSources: SearchSources[];
|
||||
query: string;
|
||||
chatHistory: Message[];
|
||||
chatHistory: ChatTurnMessage[];
|
||||
};
|
||||
|
||||
export type ClassifierOutput = {
|
||||
@@ -60,6 +61,46 @@ export type ClassifierOutput = {
|
||||
|
||||
export type AdditionalConfig = {
|
||||
llm: BaseLLM<any>;
|
||||
embedding: BaseLLM<any>;
|
||||
emitter: EventEmitter;
|
||||
embedding: BaseEmbedding<any>;
|
||||
session: SessionManager;
|
||||
};
|
||||
|
||||
export type ResearcherInput = {
|
||||
chatHistory: ChatTurnMessage[];
|
||||
followUp: string;
|
||||
classification: ClassifierOutput;
|
||||
config: SearchAgentConfig;
|
||||
};
|
||||
|
||||
export type ResearcherOutput = {
|
||||
findings: ActionOutput[];
|
||||
};
|
||||
|
||||
export type SearchActionOutput = {
|
||||
type: 'search_results';
|
||||
results: Chunk[];
|
||||
};
|
||||
|
||||
export type DoneActionOutput = {
|
||||
type: 'done';
|
||||
};
|
||||
|
||||
export type ActionOutput = SearchActionOutput | DoneActionOutput;
|
||||
|
||||
export interface ResearchAction<
|
||||
TSchema extends z.ZodObject<any> = z.ZodObject<any>,
|
||||
> {
|
||||
name: string;
|
||||
description: string;
|
||||
schema: z.ZodObject<any>;
|
||||
enabled: (config: { classification: ClassifierOutput }) => boolean;
|
||||
execute: (
|
||||
params: z.infer<TSchema>,
|
||||
additionalConfig: AdditionalConfig,
|
||||
) => Promise<ActionOutput>;
|
||||
}
|
||||
|
||||
export type ActionConfig = {
|
||||
type: string;
|
||||
params: Record<string, any>;
|
||||
};
|
||||
|
||||
@@ -18,12 +18,18 @@ db.exec(`
|
||||
`);
|
||||
|
||||
function sanitizeSql(content: string) {
|
||||
return content
|
||||
.split(/\r?\n/)
|
||||
.filter(
|
||||
(l) => !l.trim().startsWith('-->') && !l.includes('statement-breakpoint'),
|
||||
const statements = content
|
||||
.split(/--> statement-breakpoint/g)
|
||||
.map((stmt) =>
|
||||
stmt
|
||||
.split(/\r?\n/)
|
||||
.filter((l) => !l.trim().startsWith('-->'))
|
||||
.join('\n')
|
||||
.trim(),
|
||||
)
|
||||
.join('\n');
|
||||
.filter((stmt) => stmt.length > 0);
|
||||
|
||||
return statements;
|
||||
}
|
||||
|
||||
fs.readdirSync(migrationsFolder)
|
||||
@@ -32,7 +38,7 @@ fs.readdirSync(migrationsFolder)
|
||||
.forEach((file) => {
|
||||
const filePath = path.join(migrationsFolder, file);
|
||||
let content = fs.readFileSync(filePath, 'utf-8');
|
||||
content = sanitizeSql(content);
|
||||
const statements = sanitizeSql(content);
|
||||
|
||||
const migrationName = file.split('_')[0] || file;
|
||||
|
||||
@@ -108,7 +114,12 @@ fs.readdirSync(migrationsFolder)
|
||||
db.exec('DROP TABLE messages;');
|
||||
db.exec('ALTER TABLE messages_with_sources RENAME TO messages;');
|
||||
} else {
|
||||
db.exec(content);
|
||||
// Execute each statement separately
|
||||
statements.forEach((stmt) => {
|
||||
if (stmt.trim()) {
|
||||
db.exec(stmt);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
db.prepare('INSERT OR IGNORE INTO ran_migrations (name) VALUES (?)').run(
|
||||
|
||||
@@ -1,26 +1,22 @@
|
||||
import { sql } from 'drizzle-orm';
|
||||
import { text, integer, sqliteTable } from 'drizzle-orm/sqlite-core';
|
||||
import { Document } from '@langchain/core/documents';
|
||||
|
||||
export const messages = sqliteTable('messages', {
|
||||
id: integer('id').primaryKey(),
|
||||
role: text('type', { enum: ['assistant', 'user', 'source'] }).notNull(),
|
||||
chatId: text('chatId').notNull(),
|
||||
createdAt: text('createdAt')
|
||||
.notNull()
|
||||
.default(sql`CURRENT_TIMESTAMP`),
|
||||
messageId: text('messageId').notNull(),
|
||||
|
||||
content: text('content'),
|
||||
|
||||
sources: text('sources', {
|
||||
mode: 'json',
|
||||
})
|
||||
.$type<Document[]>()
|
||||
chatId: text('chatId').notNull(),
|
||||
backendId: text('backendId').notNull(),
|
||||
query: text('query').notNull(),
|
||||
createdAt: text('createdAt').notNull(),
|
||||
responseBlocks: text('responseBlocks', { mode: 'json' })
|
||||
.$type<Block[]>()
|
||||
.default(sql`'[]'`),
|
||||
status: text({ enum: ['answering', 'completed', 'error'] }).default(
|
||||
'answering',
|
||||
),
|
||||
});
|
||||
|
||||
interface File {
|
||||
interface DBFile {
|
||||
name: string;
|
||||
fileId: string;
|
||||
}
|
||||
@@ -31,6 +27,6 @@ export const chats = sqliteTable('chats', {
|
||||
createdAt: text('createdAt').notNull(),
|
||||
focusMode: text('focusMode').notNull(),
|
||||
files: text('files', { mode: 'json' })
|
||||
.$type<File[]>()
|
||||
.$type<DBFile[]>()
|
||||
.default(sql`'[]'`),
|
||||
});
|
||||
|
||||
241
src/lib/prompts/search/researcher.ts
Normal file
241
src/lib/prompts/search/researcher.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
export const getResearcherPrompt = (
|
||||
actionDesc: string,
|
||||
mode: 'fast' | 'balanced' | 'deep_research',
|
||||
) => {
|
||||
const today = new Date().toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
});
|
||||
|
||||
return `
|
||||
You are an action orchestrator. Your job is to fulfill user requests by selecting and executing appropriate actions - whether that's searching for information, creating calendar events, sending emails, or any other available action.
|
||||
|
||||
Today's date: ${today}
|
||||
|
||||
You are operating in "${mode}" mode. ${
|
||||
mode === 'fast'
|
||||
? 'Prioritize speed - use as few actions as possible to get the needed information quickly.'
|
||||
: mode === 'balanced'
|
||||
? 'Balance speed and depth - use a moderate number of actions to get good information efficiently. Never stop at the first action unless there is no action available or the query is simple.'
|
||||
: 'Conduct deep research - use multiple actions to gather comprehensive information, even if it takes longer.'
|
||||
}
|
||||
|
||||
<available_actions>
|
||||
${actionDesc}
|
||||
</available_actions>
|
||||
|
||||
<core_principle>
|
||||
|
||||
NEVER ASSUME - your knowledge may be outdated. When a user asks about something you're not certain about, go find out. Don't assume it exists or doesn't exist - just look it up directly.
|
||||
|
||||
</core_principle>
|
||||
|
||||
<reasoning_approach>
|
||||
|
||||
Think like a human would. Your reasoning should be natural and show:
|
||||
- What the user is asking for
|
||||
- What you need to find out or do
|
||||
- Your plan to accomplish it
|
||||
|
||||
Keep it to 2-3 natural sentences.
|
||||
|
||||
</reasoning_approach>
|
||||
|
||||
<examples>
|
||||
|
||||
## Example 1: Unknown Subject
|
||||
|
||||
User: "What is Kimi K2?"
|
||||
|
||||
Good reasoning:
|
||||
"I'm not sure what Kimi K2 is - could be an AI model, a product, or something else. Let me look it up to find out what it actually is and get the relevant details."
|
||||
|
||||
Actions: web_search ["Kimi K2", "Kimi K2 AI"]
|
||||
|
||||
## Example 2: Subject You're Uncertain About
|
||||
|
||||
User: "What are the features of GPT-5.1?"
|
||||
|
||||
Good reasoning:
|
||||
"I don't have current information on GPT-5.1 - my knowledge might be outdated. Let me look up GPT-5.1 to see what's available and what features it has."
|
||||
|
||||
Actions: web_search ["GPT-5.1", "GPT-5.1 features", "GPT-5.1 release"]
|
||||
|
||||
Bad reasoning (wastes time on verification):
|
||||
"GPT-5.1 might not exist based on my knowledge. I need to verify if it exists first before looking for features."
|
||||
|
||||
## Example 3: After Actions Return Results
|
||||
|
||||
User: "What are the features of GPT-5.1?"
|
||||
[Previous actions returned information about GPT-5.1]
|
||||
|
||||
Good reasoning:
|
||||
"Got the information I needed about GPT-5.1. The results cover its features and capabilities - I can now provide a complete answer."
|
||||
|
||||
Action: done
|
||||
|
||||
## Example 4: Ambiguous Query
|
||||
|
||||
User: "Tell me about Mercury"
|
||||
|
||||
Good reasoning:
|
||||
"Mercury could refer to several things - the planet, the element, or something else. I'll look up both main interpretations to give a useful answer."
|
||||
|
||||
Actions: web_search ["Mercury planet facts", "Mercury element"]
|
||||
|
||||
## Example 5: Current Events
|
||||
|
||||
User: "What's happening with AI regulation?"
|
||||
|
||||
Good reasoning:
|
||||
"I need current news on AI regulation developments. Let me find the latest updates on this topic."
|
||||
|
||||
Actions: web_search ["AI regulation news 2024", "AI regulation bill latest"]
|
||||
|
||||
## Example 6: Technical Query
|
||||
|
||||
User: "How do I set up authentication in Next.js 14?"
|
||||
|
||||
Good reasoning:
|
||||
"This is a technical implementation question. I'll find the current best practices and documentation for Next.js 14 authentication."
|
||||
|
||||
Actions: web_search ["Next.js 14 authentication guide", "NextAuth.js App Router"]
|
||||
|
||||
## Example 7: Comparison Query
|
||||
|
||||
User: "Prisma vs Drizzle - which should I use?"
|
||||
|
||||
Good reasoning:
|
||||
"Need to find factual comparisons between these ORMs - performance, features, trade-offs. Let me gather objective information."
|
||||
|
||||
Actions: web_search ["Prisma vs Drizzle comparison 2024", "Drizzle ORM performance"]
|
||||
|
||||
## Example 8: Fact-Check
|
||||
|
||||
User: "Is it true you only use 10% of your brain?"
|
||||
|
||||
Good reasoning:
|
||||
"This is a common claim that needs scientific verification. Let me find what the actual research says about this."
|
||||
|
||||
Actions: web_search ["10 percent brain myth science", "brain usage neuroscience"]
|
||||
|
||||
## Example 9: Recent Product
|
||||
|
||||
User: "What are the specs of MacBook Pro M4?"
|
||||
|
||||
Good reasoning:
|
||||
"I need current information on the MacBook Pro M4. Let me look up the latest specs and details."
|
||||
|
||||
Actions: web_search ["MacBook Pro M4 specs", "MacBook Pro M4 specifications Apple"]
|
||||
|
||||
## Example 10: Multi-Part Query
|
||||
|
||||
User: "Population of Tokyo vs New York?"
|
||||
|
||||
Good reasoning:
|
||||
"Need current population stats for both cities. I'll look up the comparison data."
|
||||
|
||||
Actions: web_search ["Tokyo population 2024", "Tokyo vs New York population"]
|
||||
|
||||
## Example 11: Calendar Task
|
||||
|
||||
User: "Add a meeting with John tomorrow at 3pm"
|
||||
|
||||
Good reasoning:
|
||||
"This is a calendar task. I have all the details - meeting with John, tomorrow, 3pm. I'll create the event."
|
||||
|
||||
Action: create_calendar_event with the provided details
|
||||
|
||||
## Example 12: Email Task
|
||||
|
||||
User: "Send an email to sarah@company.com about the project update"
|
||||
|
||||
Good reasoning:
|
||||
"Need to send an email. I have the recipient but need to compose appropriate content about the project update."
|
||||
|
||||
Action: send_email to sarah@company.com with project update content
|
||||
|
||||
## Example 13: Multi-Step Task
|
||||
|
||||
User: "What's the weather in Tokyo and add a reminder to pack an umbrella if it's rainy"
|
||||
|
||||
Good reasoning:
|
||||
"Two things here - first I need to check Tokyo's weather, then based on that I might need to create a reminder. Let me start with the weather lookup."
|
||||
|
||||
Actions: web_search ["Tokyo weather today forecast"]
|
||||
|
||||
## Example 14: Research Then Act
|
||||
|
||||
User: "Find the best Italian restaurant near me and make a reservation for 7pm"
|
||||
|
||||
Good reasoning:
|
||||
"I need to first find top Italian restaurants in the area, then make a reservation. Let me start by finding the options."
|
||||
|
||||
Actions: web_search ["best Italian restaurant near me", "top rated Italian restaurants"]
|
||||
|
||||
</examples>
|
||||
|
||||
<action_guidelines>
|
||||
|
||||
## For Information Queries:
|
||||
- Just look it up - don't overthink whether something exists
|
||||
- Use 1-3 targeted queries
|
||||
- Done when you have useful information to answer with
|
||||
|
||||
## For Task Execution:
|
||||
- Calendar, email, reminders: execute directly with the provided details
|
||||
- If details are missing, note what you need
|
||||
|
||||
## For Multi-Step Requests:
|
||||
- Break it down logically
|
||||
- Complete one part before moving to the next
|
||||
- Some tasks require information before you can act
|
||||
|
||||
## When to Select "done":
|
||||
- You have the information needed to answer
|
||||
- You've completed the requested task
|
||||
- Further actions would be redundant
|
||||
|
||||
</action_guidelines>
|
||||
|
||||
<query_formulation>
|
||||
|
||||
**General subjects:**
|
||||
- ["subject name", "subject name + context"]
|
||||
|
||||
**Current events:**
|
||||
- Include year: "topic 2024", "topic latest news"
|
||||
|
||||
**Technical topics:**
|
||||
- Include versions: "framework v14 guide"
|
||||
- Add context: "documentation", "tutorial", "how to"
|
||||
|
||||
**Comparisons:**
|
||||
- "X vs Y comparison", "X vs Y benchmarks"
|
||||
|
||||
**Keep it simple:**
|
||||
- 1-3 actions per iteration
|
||||
- Don't over-complicate queries
|
||||
|
||||
</query_formulation>
|
||||
|
||||
<mistakes_to_avoid>
|
||||
|
||||
1. **Over-assuming**: Don't assume things exist or don't exist - just look them up
|
||||
|
||||
2. **Verification obsession**: Don't waste actions "verifying existence" - just search for the thing directly
|
||||
|
||||
3. **Endless loops**: If 2-3 actions don't find something, it probably doesn't exist - report that and move on
|
||||
|
||||
4. **Ignoring task context**: If user wants a calendar event, don't just search - create the event
|
||||
|
||||
5. **Overthinking**: Keep reasoning simple and action-focused
|
||||
|
||||
</mistakes_to_avoid>
|
||||
|
||||
<output_format>
|
||||
Reasoning should be 2-3 natural sentences showing your thought process and plan. Then select and configure the appropriate action(s).
|
||||
</output_format>
|
||||
`;
|
||||
};
|
||||
@@ -1,13 +1,20 @@
|
||||
import { EventEmitter } from 'stream';
|
||||
/* todo implement history saving and better artifact typing and handling */
|
||||
import { applyPatch } from 'rfc6902';
|
||||
|
||||
class SessionManager {
|
||||
private static sessions = new Map<string, SessionManager>();
|
||||
readonly id: string;
|
||||
private artifacts = new Map<string, Artifact>();
|
||||
private blocks = new Map<string, Block>();
|
||||
private events: { event: string; data: any }[] = [];
|
||||
private emitter = new EventEmitter();
|
||||
private TTL_MS = 30 * 60 * 1000;
|
||||
|
||||
constructor() {
|
||||
this.id = crypto.randomUUID();
|
||||
constructor(id?: string) {
|
||||
this.id = id ?? crypto.randomUUID();
|
||||
|
||||
setTimeout(() => {
|
||||
SessionManager.sessions.delete(this.id);
|
||||
}, this.TTL_MS);
|
||||
}
|
||||
|
||||
static getSession(id: string): SessionManager | undefined {
|
||||
@@ -18,26 +25,55 @@ class SessionManager {
|
||||
return Array.from(this.sessions.values());
|
||||
}
|
||||
|
||||
static createSession(): SessionManager {
|
||||
const session = new SessionManager();
|
||||
this.sessions.set(session.id, session);
|
||||
return session;
|
||||
}
|
||||
|
||||
removeAllListeners() {
|
||||
this.emitter.removeAllListeners();
|
||||
}
|
||||
|
||||
emit(event: string, data: any) {
|
||||
this.emitter.emit(event, data);
|
||||
this.events.push({ event, data });
|
||||
}
|
||||
|
||||
emitArtifact(artifact: Artifact) {
|
||||
this.artifacts.set(artifact.id, artifact);
|
||||
this.emitter.emit('addArtifact', artifact);
|
||||
emitBlock(block: Block) {
|
||||
this.blocks.set(block.id, block);
|
||||
this.emit('data', {
|
||||
type: 'block',
|
||||
block: block,
|
||||
});
|
||||
}
|
||||
|
||||
appendToArtifact(artifactId: string, data: any) {
|
||||
const artifact = this.artifacts.get(artifactId);
|
||||
if (artifact) {
|
||||
if (typeof artifact.data === 'string') {
|
||||
artifact.data += data;
|
||||
} else if (Array.isArray(artifact.data)) {
|
||||
artifact.data.push(data);
|
||||
} else if (typeof artifact.data === 'object') {
|
||||
Object.assign(artifact.data, data);
|
||||
}
|
||||
this.emitter.emit('updateArtifact', artifact);
|
||||
getBlock(blockId: string): Block | undefined {
|
||||
return this.blocks.get(blockId);
|
||||
}
|
||||
|
||||
updateBlock(blockId: string, patch: any[]) {
|
||||
const block = this.blocks.get(blockId);
|
||||
|
||||
if (block) {
|
||||
applyPatch(block, patch);
|
||||
this.blocks.set(blockId, block);
|
||||
this.emit('data', {
|
||||
type: 'updateBlock',
|
||||
blockId: blockId,
|
||||
patch: patch,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
addListener(event: string, listener: (data: any) => void) {
|
||||
this.emitter.addListener(event, listener);
|
||||
}
|
||||
|
||||
replay() {
|
||||
for (const { event, data } of this.events) {
|
||||
/* Using emitter directly to avoid infinite loop */
|
||||
this.emitter.emit(event, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
type Message = {
|
||||
type ChatTurnMessage = {
|
||||
role: 'user' | 'assistant' | 'system';
|
||||
content: string;
|
||||
};
|
||||
@@ -8,8 +8,64 @@ type Chunk = {
|
||||
metadata: Record<string, any>;
|
||||
};
|
||||
|
||||
type Artifact = {
|
||||
type TextBlock = {
|
||||
id: string;
|
||||
type: string;
|
||||
data: any;
|
||||
type: 'text';
|
||||
data: string;
|
||||
};
|
||||
|
||||
type SourceBlock = {
|
||||
id: string;
|
||||
type: 'source';
|
||||
data: Chunk[];
|
||||
};
|
||||
|
||||
type SuggestionBlock = {
|
||||
id: string;
|
||||
type: 'suggestion';
|
||||
data: string[];
|
||||
};
|
||||
|
||||
type WidgetBlock = {
|
||||
id: string;
|
||||
type: 'widget';
|
||||
data: {
|
||||
widgetType: string;
|
||||
params: Record<string, any>;
|
||||
};
|
||||
};
|
||||
|
||||
type ReasoningResearchBlock = {
|
||||
id: string;
|
||||
reasoning: string;
|
||||
};
|
||||
|
||||
type SearchingResearchBlock = {
|
||||
id: string;
|
||||
searching: string[];
|
||||
};
|
||||
|
||||
type ReadingResearchBlock = {
|
||||
id: string;
|
||||
reading: Chunk[];
|
||||
};
|
||||
|
||||
type ResearchBlockSubStep =
|
||||
| ReasoningResearchBlock
|
||||
| SearchingResearchBlock
|
||||
| ReadingResearchBlock;
|
||||
|
||||
type ResearchBlock = {
|
||||
id: string;
|
||||
type: 'research';
|
||||
data: {
|
||||
subSteps: ResearchBlockSubStep[];
|
||||
};
|
||||
};
|
||||
|
||||
type Block =
|
||||
| TextBlock
|
||||
| SourceBlock
|
||||
| SuggestionBlock
|
||||
| WidgetBlock
|
||||
| ResearchBlock;
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { BaseMessage, isAIMessage } from '@langchain/core/messages';
|
||||
|
||||
const formatChatHistoryAsString = (history: BaseMessage[]) => {
|
||||
const formatChatHistoryAsString = (history: Message[]) => {
|
||||
return history
|
||||
.map(
|
||||
(message) =>
|
||||
`${isAIMessage(message) ? 'AI' : 'User'}: ${message.content}`,
|
||||
`${message.role === 'assistant' ? 'AI' : 'User'}: ${message.content}`,
|
||||
)
|
||||
.join('\n');
|
||||
};
|
||||
|
||||
17
yarn.lock
17
yarn.lock
@@ -3992,6 +3992,13 @@ ollama@^0.5.12:
|
||||
dependencies:
|
||||
whatwg-fetch "^3.6.20"
|
||||
|
||||
ollama@^0.6.3:
|
||||
version "0.6.3"
|
||||
resolved "https://registry.yarnpkg.com/ollama/-/ollama-0.6.3.tgz#b188573dd0ccb3b4759c1f8fa85067cb17f6673c"
|
||||
integrity sha512-KEWEhIqE5wtfzEIZbDCLH51VFZ6Z3ZSa6sIOg/E/tBV8S51flyqBOXi+bRxlOYKDf8i327zG9eSTb8IJxvm3Zg==
|
||||
dependencies:
|
||||
whatwg-fetch "^3.6.20"
|
||||
|
||||
once@^1.3.0, once@^1.3.1, once@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
|
||||
@@ -4126,6 +4133,11 @@ parseley@^0.12.0:
|
||||
leac "^0.6.0"
|
||||
peberminta "^0.9.0"
|
||||
|
||||
partial-json@^0.1.7:
|
||||
version "0.1.7"
|
||||
resolved "https://registry.yarnpkg.com/partial-json/-/partial-json-0.1.7.tgz#b735a89edb3e25f231a3c4caeaae71dc9f578605"
|
||||
integrity sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==
|
||||
|
||||
path-exists@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
|
||||
@@ -4518,6 +4530,11 @@ reusify@^1.0.4:
|
||||
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
|
||||
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
|
||||
|
||||
rfc6902@^5.1.2:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/rfc6902/-/rfc6902-5.1.2.tgz#774262ba7b032ab9abf9eb8e0312927e8f425062"
|
||||
integrity sha512-zxcb+PWlE8PwX0tiKE6zP97THQ8/lHmeiwucRrJ3YFupWEmp25RmFSlB1dNTqjkovwqG4iq+u1gzJMBS3um8mA==
|
||||
|
||||
rgbcolor@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/rgbcolor/-/rgbcolor-1.0.1.tgz#d6505ecdb304a6595da26fa4b43307306775945d"
|
||||
|
||||
Reference in New Issue
Block a user