mirror of
https://github.com/ItzCrazyKns/Perplexica.git
synced 2025-12-25 04:58:15 +00:00
Compare commits
21 Commits
fc0c444b6a
...
3d1d164f68
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d1d164f68 | ||
|
|
a99702d837 | ||
|
|
60675955e4 | ||
|
|
a6ff94d030 | ||
|
|
748ee4d3c2 | ||
|
|
1f3bf8da32 | ||
|
|
8d471ac40e | ||
|
|
40b25a487b | ||
|
|
3949748bbd | ||
|
|
56e47d6c39 | ||
|
|
fd745577d6 | ||
|
|
86ea3cde7e | ||
|
|
aeb90cb137 | ||
|
|
6473e51fde | ||
|
|
c7c327a7bb | ||
|
|
0688630863 | ||
|
|
0b9e193ed1 | ||
|
|
8d1b04e05f | ||
|
|
ff4cf98b50 | ||
|
|
13ae0b9451 | ||
|
|
0cfa01422c |
22
package.json
22
package.json
@@ -14,26 +14,16 @@
|
|||||||
"@headlessui/react": "^2.2.0",
|
"@headlessui/react": "^2.2.0",
|
||||||
"@headlessui/tailwindcss": "^0.2.2",
|
"@headlessui/tailwindcss": "^0.2.2",
|
||||||
"@huggingface/transformers": "^3.7.5",
|
"@huggingface/transformers": "^3.7.5",
|
||||||
"@iarna/toml": "^2.2.5",
|
|
||||||
"@icons-pack/react-simple-icons": "^12.3.0",
|
"@icons-pack/react-simple-icons": "^12.3.0",
|
||||||
"@langchain/anthropic": "^1.0.1",
|
|
||||||
"@langchain/community": "^1.0.3",
|
|
||||||
"@langchain/core": "^1.0.5",
|
|
||||||
"@langchain/google-genai": "^1.0.1",
|
|
||||||
"@langchain/groq": "^1.0.1",
|
|
||||||
"@langchain/langgraph": "^1.0.1",
|
|
||||||
"@langchain/ollama": "^1.0.1",
|
|
||||||
"@langchain/openai": "^1.1.1",
|
|
||||||
"@langchain/textsplitters": "^1.0.0",
|
|
||||||
"@tailwindcss/typography": "^0.5.12",
|
"@tailwindcss/typography": "^0.5.12",
|
||||||
|
"@types/jspdf": "^2.0.0",
|
||||||
"axios": "^1.8.3",
|
"axios": "^1.8.3",
|
||||||
"better-sqlite3": "^11.9.1",
|
"better-sqlite3": "^11.9.1",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
"drizzle-orm": "^0.40.1",
|
"drizzle-orm": "^0.40.1",
|
||||||
"framer-motion": "^12.23.25",
|
"framer-motion": "^12.23.25",
|
||||||
"html-to-text": "^9.0.5",
|
"js-tiktoken": "^1.0.21",
|
||||||
"jspdf": "^3.0.1",
|
"jspdf": "^3.0.4",
|
||||||
"langchain": "^1.0.4",
|
|
||||||
"lightweight-charts": "^5.0.9",
|
"lightweight-charts": "^5.0.9",
|
||||||
"lucide-react": "^0.556.0",
|
"lucide-react": "^0.556.0",
|
||||||
"mammoth": "^1.9.1",
|
"mammoth": "^1.9.1",
|
||||||
@@ -41,10 +31,11 @@
|
|||||||
"mathjs": "^15.1.0",
|
"mathjs": "^15.1.0",
|
||||||
"next": "^16.0.7",
|
"next": "^16.0.7",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
|
"officeparser": "^5.2.2",
|
||||||
"ollama": "^0.6.3",
|
"ollama": "^0.6.3",
|
||||||
"openai": "^6.9.0",
|
"openai": "^6.9.0",
|
||||||
"partial-json": "^0.1.7",
|
"partial-json": "^0.1.7",
|
||||||
"pdf-parse": "^1.1.1",
|
"pdf-parse": "^2.4.5",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
"react-text-to-speech": "^0.14.5",
|
"react-text-to-speech": "^0.14.5",
|
||||||
@@ -53,15 +44,12 @@
|
|||||||
"sonner": "^1.4.41",
|
"sonner": "^1.4.41",
|
||||||
"tailwind-merge": "^2.2.2",
|
"tailwind-merge": "^2.2.2",
|
||||||
"turndown": "^7.2.2",
|
"turndown": "^7.2.2",
|
||||||
"winston": "^3.17.0",
|
|
||||||
"yahoo-finance2": "^3.10.2",
|
"yahoo-finance2": "^3.10.2",
|
||||||
"yet-another-react-lightbox": "^3.17.2",
|
"yet-another-react-lightbox": "^3.17.2",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/better-sqlite3": "^7.6.12",
|
"@types/better-sqlite3": "^7.6.12",
|
||||||
"@types/html-to-text": "^9.0.4",
|
|
||||||
"@types/jspdf": "^2.0.0",
|
|
||||||
"@types/node": "^24.8.1",
|
"@types/node": "^24.8.1",
|
||||||
"@types/pdf-parse": "^1.1.4",
|
"@types/pdf-parse": "^1.1.4",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import crypto from 'crypto';
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import ModelRegistry from '@/lib/models/registry';
|
import ModelRegistry from '@/lib/models/registry';
|
||||||
import { ModelWithProvider } from '@/lib/models/types';
|
import { ModelWithProvider } from '@/lib/models/types';
|
||||||
@@ -206,6 +205,7 @@ export const POST = async (req: Request) => {
|
|||||||
embedding: embedding,
|
embedding: embedding,
|
||||||
sources: ['web'],
|
sources: ['web'],
|
||||||
mode: body.optimizationMode,
|
mode: body.optimizationMode,
|
||||||
|
fileIds: body.files,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export const POST = async (req: Request) => {
|
|||||||
llm: llm,
|
llm: llm,
|
||||||
sources: ['web', 'discussions', 'academic'],
|
sources: ['web', 'discussions', 'academic'],
|
||||||
mode: 'balanced',
|
mode: 'balanced',
|
||||||
|
fileIds: []
|
||||||
},
|
},
|
||||||
followUp: body.query,
|
followUp: body.query,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import generateSuggestions from '@/lib/agents/suggestions';
|
import generateSuggestions from '@/lib/agents/suggestions';
|
||||||
import ModelRegistry from '@/lib/models/registry';
|
import ModelRegistry from '@/lib/models/registry';
|
||||||
import { ModelWithProvider } from '@/lib/models/types';
|
import { ModelWithProvider } from '@/lib/models/types';
|
||||||
import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages';
|
|
||||||
|
|
||||||
interface SuggestionsGenerationBody {
|
interface SuggestionsGenerationBody {
|
||||||
chatHistory: any[];
|
chatHistory: any[];
|
||||||
|
|||||||
@@ -1,40 +1,16 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import fs from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
import crypto from 'crypto';
|
|
||||||
import { PDFLoader } from '@langchain/community/document_loaders/fs/pdf';
|
|
||||||
import { DocxLoader } from '@langchain/community/document_loaders/fs/docx';
|
|
||||||
import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters';
|
|
||||||
import { Document } from '@langchain/core/documents';
|
|
||||||
import ModelRegistry from '@/lib/models/registry';
|
import ModelRegistry from '@/lib/models/registry';
|
||||||
import { Chunk } from '@/lib/types';
|
import UploadManager from '@/lib/uploads/manager';
|
||||||
|
|
||||||
interface FileRes {
|
|
||||||
fileName: string;
|
|
||||||
fileExtension: string;
|
|
||||||
fileId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const uploadDir = path.join(process.cwd(), 'uploads');
|
|
||||||
|
|
||||||
if (!fs.existsSync(uploadDir)) {
|
|
||||||
fs.mkdirSync(uploadDir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
const splitter = new RecursiveCharacterTextSplitter({
|
|
||||||
chunkSize: 500,
|
|
||||||
chunkOverlap: 100,
|
|
||||||
});
|
|
||||||
|
|
||||||
export async function POST(req: Request) {
|
export async function POST(req: Request) {
|
||||||
try {
|
try {
|
||||||
const formData = await req.formData();
|
const formData = await req.formData();
|
||||||
|
|
||||||
const files = formData.getAll('files') as File[];
|
const files = formData.getAll('files') as File[];
|
||||||
const embedding_model = formData.get('embedding_model_key') as string;
|
const embeddingModel = formData.get('embedding_model_key') as string;
|
||||||
const embedding_model_provider = formData.get('embedding_model_provider_id') as string;
|
const embeddingModelProvider = formData.get('embedding_model_provider_id') as string;
|
||||||
|
|
||||||
if (!embedding_model || !embedding_model_provider) {
|
if (!embeddingModel || !embeddingModelProvider) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ message: 'Missing embedding model or provider' },
|
{ message: 'Missing embedding model or provider' },
|
||||||
{ status: 400 },
|
{ status: 400 },
|
||||||
@@ -43,81 +19,13 @@ export async function POST(req: Request) {
|
|||||||
|
|
||||||
const registry = new ModelRegistry();
|
const registry = new ModelRegistry();
|
||||||
|
|
||||||
const model = await registry.loadEmbeddingModel(embedding_model_provider, embedding_model);
|
const model = await registry.loadEmbeddingModel(embeddingModelProvider, embeddingModel);
|
||||||
|
|
||||||
|
const uploadManager = new UploadManager({
|
||||||
|
embeddingModel: model,
|
||||||
|
})
|
||||||
|
|
||||||
const processedFiles: FileRes[] = [];
|
const processedFiles = await uploadManager.processFiles(files);
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
files.map(async (file: any) => {
|
|
||||||
const fileExtension = file.name.split('.').pop();
|
|
||||||
if (!['pdf', 'docx', 'txt'].includes(fileExtension!)) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ message: 'File type not supported' },
|
|
||||||
{ status: 400 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const uniqueFileName = `${crypto.randomBytes(16).toString('hex')}.${fileExtension}`;
|
|
||||||
const filePath = path.join(uploadDir, uniqueFileName);
|
|
||||||
|
|
||||||
const buffer = Buffer.from(await file.arrayBuffer());
|
|
||||||
fs.writeFileSync(filePath, new Uint8Array(buffer));
|
|
||||||
|
|
||||||
let docs: any[] = [];
|
|
||||||
if (fileExtension === 'pdf') {
|
|
||||||
const loader = new PDFLoader(filePath);
|
|
||||||
docs = await loader.load();
|
|
||||||
} else if (fileExtension === 'docx') {
|
|
||||||
const loader = new DocxLoader(filePath);
|
|
||||||
docs = await loader.load();
|
|
||||||
} else if (fileExtension === 'txt') {
|
|
||||||
const text = fs.readFileSync(filePath, 'utf-8');
|
|
||||||
docs = [
|
|
||||||
new Document({ pageContent: text, metadata: { title: file.name } }),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
const splitted = await splitter.splitDocuments(docs);
|
|
||||||
|
|
||||||
const extractedDataPath = filePath.replace(/\.\w+$/, '-extracted.json');
|
|
||||||
fs.writeFileSync(
|
|
||||||
extractedDataPath,
|
|
||||||
JSON.stringify({
|
|
||||||
title: file.name,
|
|
||||||
contents: splitted.map((doc) => doc.pageContent),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const chunks: Chunk[] = splitted.map((doc) => {
|
|
||||||
return {
|
|
||||||
content: doc.pageContent,
|
|
||||||
metadata: doc.metadata,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const embeddings = await model.embedChunks(
|
|
||||||
chunks
|
|
||||||
);
|
|
||||||
|
|
||||||
const embeddingsDataPath = filePath.replace(
|
|
||||||
/\.\w+$/,
|
|
||||||
'-embeddings.json',
|
|
||||||
);
|
|
||||||
fs.writeFileSync(
|
|
||||||
embeddingsDataPath,
|
|
||||||
JSON.stringify({
|
|
||||||
title: file.name,
|
|
||||||
embeddings,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
processedFiles.push({
|
|
||||||
fileName: file.name,
|
|
||||||
fileExtension: fileExtension,
|
|
||||||
fileId: uniqueFileName.replace(/\.\w+$/, ''),
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
files: processedFiles,
|
files: processedFiles,
|
||||||
|
|||||||
@@ -16,9 +16,12 @@ import { useChat } from '@/lib/hooks/useChat';
|
|||||||
const getStepIcon = (step: ResearchBlockSubStep) => {
|
const getStepIcon = (step: ResearchBlockSubStep) => {
|
||||||
if (step.type === 'reasoning') {
|
if (step.type === 'reasoning') {
|
||||||
return <Brain className="w-4 h-4" />;
|
return <Brain className="w-4 h-4" />;
|
||||||
} else if (step.type === 'searching') {
|
} else if (step.type === 'searching' || step.type === 'upload_searching') {
|
||||||
return <Search className="w-4 h-4" />;
|
return <Search className="w-4 h-4" />;
|
||||||
} else if (step.type === 'search_results') {
|
} else if (
|
||||||
|
step.type === 'search_results' ||
|
||||||
|
step.type === 'upload_search_results'
|
||||||
|
) {
|
||||||
return <FileText className="w-4 h-4" />;
|
return <FileText className="w-4 h-4" />;
|
||||||
} else if (step.type === 'reading') {
|
} else if (step.type === 'reading') {
|
||||||
return <BookSearch className="w-4 h-4" />;
|
return <BookSearch className="w-4 h-4" />;
|
||||||
@@ -39,6 +42,10 @@ const getStepTitle = (
|
|||||||
return `Found ${step.reading.length} ${step.reading.length === 1 ? 'result' : 'results'}`;
|
return `Found ${step.reading.length} ${step.reading.length === 1 ? 'result' : 'results'}`;
|
||||||
} else if (step.type === 'reading') {
|
} else if (step.type === 'reading') {
|
||||||
return `Reading ${step.reading.length} ${step.reading.length === 1 ? 'source' : 'sources'}`;
|
return `Reading ${step.reading.length} ${step.reading.length === 1 ? 'source' : 'sources'}`;
|
||||||
|
} else if (step.type === 'upload_searching') {
|
||||||
|
return 'Scanning your uploaded documents';
|
||||||
|
} else if (step.type === 'upload_search_results') {
|
||||||
|
return `Reading ${step.results.length} ${step.results.length === 1 ? 'document' : 'documents'}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'Processing';
|
return 'Processing';
|
||||||
@@ -195,6 +202,56 @@ const AssistantSteps = ({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{step.type === 'upload_searching' &&
|
||||||
|
step.queries.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-1.5 mt-1.5">
|
||||||
|
{step.queries.map((query, idx) => (
|
||||||
|
<span
|
||||||
|
key={idx}
|
||||||
|
className="inline-flex items-center px-2 py-0.5 rounded-md text-xs font-medium bg-light-100 dark:bg-dark-100 text-black/70 dark:text-white/70 border border-light-200 dark:border-dark-200"
|
||||||
|
>
|
||||||
|
{query}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step.type === 'upload_search_results' &&
|
||||||
|
step.results.length > 0 && (
|
||||||
|
<div className="mt-1.5 space-y-2">
|
||||||
|
{step.results.slice(0, 4).map((result, idx) => {
|
||||||
|
const title =
|
||||||
|
(result.metadata &&
|
||||||
|
(result.metadata.title ||
|
||||||
|
result.metadata.fileName)) ||
|
||||||
|
'Untitled document';
|
||||||
|
const snippet = (result.content || '')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim()
|
||||||
|
.slice(0, 220);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className="flex gap-3 items-start rounded-lg border border-light-200 dark:border-dark-200 bg-light-100 dark:bg-dark-100 p-2"
|
||||||
|
>
|
||||||
|
<div className="mt-0.5 h-10 w-10 rounded-md bg-cyan-100 text-cyan-800 dark:bg-sky-500 dark:text-cyan-50 flex items-center justify-center">
|
||||||
|
<FileText className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="text-sm font-semibold text-black dark:text-white line-clamp-1">
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-black/70 dark:text-white/70 mt-0.5 leading-relaxed line-clamp-3">
|
||||||
|
{snippet || 'No preview available.'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ const MessageSources = ({ sources }: { sources: Chunk[] }) => {
|
|||||||
</p>
|
</p>
|
||||||
<div className="flex flex-row items-center justify-between">
|
<div className="flex flex-row items-center justify-between">
|
||||||
<div className="flex flex-row items-center space-x-1">
|
<div className="flex flex-row items-center space-x-1">
|
||||||
{source.metadata.url === 'File' ? (
|
{source.metadata.url.includes('file_id://') ? (
|
||||||
<div className="bg-dark-200 hover:bg-dark-100 transition duration-200 flex items-center justify-center w-6 h-6 rounded-full">
|
<div className="bg-dark-200 hover:bg-dark-100 transition duration-200 flex items-center justify-center w-6 h-6 rounded-full">
|
||||||
<File size={12} className="text-white/70" />
|
<File size={12} className="text-white/70" />
|
||||||
</div>
|
</div>
|
||||||
@@ -51,7 +51,9 @@ const MessageSources = ({ sources }: { sources: Chunk[] }) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<p className="text-xs text-black/50 dark:text-white/50 overflow-hidden whitespace-nowrap text-ellipsis">
|
<p className="text-xs text-black/50 dark:text-white/50 overflow-hidden whitespace-nowrap text-ellipsis">
|
||||||
{source.metadata.url.replace(/.+\/\/|www.|\..+/g, '')}
|
{source.metadata.url.includes('file_id://')
|
||||||
|
? 'Uploaded File'
|
||||||
|
: source.metadata.url.replace(/.+\/\/|www.|\..+/g, '')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row items-center space-x-1 text-black/50 dark:text-white/50 text-xs">
|
<div className="flex flex-row items-center space-x-1 text-black/50 dark:text-white/50 text-xs">
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const searchImages = async (
|
|||||||
query: z.string().describe('The image search query.'),
|
query: z.string().describe('The image search query.'),
|
||||||
});
|
});
|
||||||
|
|
||||||
const res = await llm.generateObject<z.infer<typeof schema>>({
|
const res = await llm.generateObject<typeof schema>({
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
role: 'system',
|
role: 'system',
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const searchVideos = async (
|
|||||||
query: z.string().describe('The video search query.'),
|
query: z.string().describe('The video search query.'),
|
||||||
});
|
});
|
||||||
|
|
||||||
const res = await llm.generateObject<z.infer<typeof schema>>({
|
const res = await llm.generateObject<typeof schema>({
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
role: 'system',
|
role: 'system',
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ class SearchAgent {
|
|||||||
searchResults?.searchFindings
|
searchResults?.searchFindings
|
||||||
.map(
|
.map(
|
||||||
(f, index) =>
|
(f, index) =>
|
||||||
`<result index=${index} title=${f.metadata.title}>${f.content}</result>`,
|
`<result index=${index + 1} title=${f.metadata.title}>${f.content}</result>`,
|
||||||
)
|
)
|
||||||
.join('\n') || '';
|
.join('\n') || '';
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ const actionDescription = `
|
|||||||
Use this action ONLY when you have completed all necessary research and are ready to provide a final answer to the user. This indicates that you have gathered sufficient information from previous steps and are concluding the research process.
|
Use this action ONLY when you have completed all necessary research and are ready to provide a final answer to the user. This indicates that you have gathered sufficient information from previous steps and are concluding the research process.
|
||||||
YOU MUST CALL THIS ACTION TO SIGNAL COMPLETION; DO NOT OUTPUT FINAL ANSWERS DIRECTLY TO THE USER.
|
YOU MUST CALL THIS ACTION TO SIGNAL COMPLETION; DO NOT OUTPUT FINAL ANSWERS DIRECTLY TO THE USER.
|
||||||
IT WILL BE AUTOMATICALLY TRIGGERED IF MAXIMUM ITERATIONS ARE REACHED SO IF YOU'RE LOW ON ITERATIONS, DON'T CALL IT AND INSTEAD FOCUS ON GATHERING ESSENTIAL INFO FIRST.
|
IT WILL BE AUTOMATICALLY TRIGGERED IF MAXIMUM ITERATIONS ARE REACHED SO IF YOU'RE LOW ON ITERATIONS, DON'T CALL IT AND INSTEAD FOCUS ON GATHERING ESSENTIAL INFO FIRST.
|
||||||
`
|
`;
|
||||||
|
|
||||||
const doneAction: ResearchAction<any> = {
|
const doneAction: ResearchAction<any> = {
|
||||||
name: 'done',
|
name: 'done',
|
||||||
schema: z.object({}),
|
schema: z.object({}),
|
||||||
getToolDescription: () =>
|
getToolDescription: () =>
|
||||||
'Only call this after ___plan AND after any other needed tool calls when you truly have enough to answer. Do not call if information is still missing.',
|
'Only call this after 0_reasoning AND after any other needed tool calls when you truly have enough to answer. Do not call if information is still missing.',
|
||||||
getDescription: () => actionDescription,
|
getDescription: () => actionDescription,
|
||||||
enabled: (_) => true,
|
enabled: (_) => true,
|
||||||
execute: async (params, additionalConfig) => {
|
execute: async (params, additionalConfig) => {
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ import doneAction from './done';
|
|||||||
import planAction from './plan';
|
import planAction from './plan';
|
||||||
import ActionRegistry from './registry';
|
import ActionRegistry from './registry';
|
||||||
import scrapeURLAction from './scrapeURL';
|
import scrapeURLAction from './scrapeURL';
|
||||||
|
import uploadsSearchAction from './uploadsSearch';
|
||||||
import webSearchAction from './webSearch';
|
import webSearchAction from './webSearch';
|
||||||
|
|
||||||
ActionRegistry.register(webSearchAction);
|
ActionRegistry.register(webSearchAction);
|
||||||
ActionRegistry.register(doneAction);
|
ActionRegistry.register(doneAction);
|
||||||
ActionRegistry.register(planAction);
|
ActionRegistry.register(planAction);
|
||||||
ActionRegistry.register(scrapeURLAction);
|
ActionRegistry.register(scrapeURLAction);
|
||||||
|
ActionRegistry.register(uploadsSearchAction);
|
||||||
|
|
||||||
export { ActionRegistry };
|
export { ActionRegistry };
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const schema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const actionDescription = `
|
const actionDescription = `
|
||||||
Use thi tool FIRST on every turn to state your plan in natural language before any other action. Keep it short, action-focused, and tailored to the current query.
|
Use this tool FIRST on every turn to state your plan in natural language before any other action. Keep it short, action-focused, and tailored to the current query.
|
||||||
Make sure to not include reference to any tools or actions you might take, just the plan itself. The user isn't aware about tools, but they love to see your thought process.
|
Make sure to not include reference to any tools or actions you might take, just the plan itself. The user isn't aware about tools, but they love to see your thought process.
|
||||||
|
|
||||||
Here are some examples of good plans:
|
Here are some examples of good plans:
|
||||||
@@ -18,12 +18,15 @@ Here are some examples of good plans:
|
|||||||
- "Okay, the user wants to know the latest advancements in renewable energy. I will start by looking for recent articles and studies on this topic, then summarize the key points." -> "I have gathered enough information to provide a comprehensive answer."
|
- "Okay, the user wants to know the latest advancements in renewable energy. I will start by looking for recent articles and studies on this topic, then summarize the key points." -> "I have gathered enough information to provide a comprehensive answer."
|
||||||
- "The user is asking about the health benefits of a Mediterranean diet. I will search for scientific studies and expert opinions on this diet, then compile the findings into a clear summary." -> "I have gathered information about the Mediterranean diet and its health benefits, I will now look up for any recent studies to ensure the information is current."
|
- "The user is asking about the health benefits of a Mediterranean diet. I will search for scientific studies and expert opinions on this diet, then compile the findings into a clear summary." -> "I have gathered information about the Mediterranean diet and its health benefits, I will now look up for any recent studies to ensure the information is current."
|
||||||
<examples>
|
<examples>
|
||||||
`
|
|
||||||
|
YOU CAN NEVER CALL ANY OTHER TOOL BEFORE CALLING THIS ONE FIRST, IF YOU DO, THAT CALL WOULD BE IGNORED.
|
||||||
|
`;
|
||||||
|
|
||||||
const planAction: ResearchAction<typeof schema> = {
|
const planAction: ResearchAction<typeof schema> = {
|
||||||
name: '___plan',
|
name: '0_reasoning',
|
||||||
schema: schema,
|
schema: schema,
|
||||||
getToolDescription: () => 'Use this FIRST on every turn to state your plan in natural language before any other action. Keep it short, action-focused, and tailored to the current query.',
|
getToolDescription: () =>
|
||||||
|
'Use this FIRST on every turn to state your plan in natural language before any other action. Keep it short, action-focused, and tailored to the current query.',
|
||||||
getDescription: () => actionDescription,
|
getDescription: () => actionDescription,
|
||||||
enabled: (config) => config.mode !== 'speed',
|
enabled: (config) => config.mode !== 'speed',
|
||||||
execute: async (input, _) => {
|
execute: async (input, _) => {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class ActionRegistry {
|
|||||||
|
|
||||||
static getAvailableActions(config: {
|
static getAvailableActions(config: {
|
||||||
classification: ClassifierOutput;
|
classification: ClassifierOutput;
|
||||||
|
fileIds: string[];
|
||||||
mode: SearchAgentConfig['mode'];
|
mode: SearchAgentConfig['mode'];
|
||||||
}): ResearchAction[] {
|
}): ResearchAction[] {
|
||||||
return Array.from(
|
return Array.from(
|
||||||
@@ -29,6 +30,7 @@ class ActionRegistry {
|
|||||||
|
|
||||||
static getAvailableActionTools(config: {
|
static getAvailableActionTools(config: {
|
||||||
classification: ClassifierOutput;
|
classification: ClassifierOutput;
|
||||||
|
fileIds: string[];
|
||||||
mode: SearchAgentConfig['mode'];
|
mode: SearchAgentConfig['mode'];
|
||||||
}): Tool[] {
|
}): Tool[] {
|
||||||
const availableActions = this.getAvailableActions(config);
|
const availableActions = this.getAvailableActions(config);
|
||||||
@@ -42,19 +44,26 @@ class ActionRegistry {
|
|||||||
|
|
||||||
static getAvailableActionsDescriptions(config: {
|
static getAvailableActionsDescriptions(config: {
|
||||||
classification: ClassifierOutput;
|
classification: ClassifierOutput;
|
||||||
|
fileIds: string[];
|
||||||
mode: SearchAgentConfig['mode'];
|
mode: SearchAgentConfig['mode'];
|
||||||
}): string {
|
}): string {
|
||||||
const availableActions = this.getAvailableActions(config);
|
const availableActions = this.getAvailableActions(config);
|
||||||
|
|
||||||
return availableActions
|
return availableActions
|
||||||
.map((action) => `<tool name="${action.name}">\n${action.getDescription({ mode: config.mode })}\n</tool>`)
|
.map(
|
||||||
|
(action) =>
|
||||||
|
`<tool name="${action.name}">\n${action.getDescription({ mode: config.mode })}\n</tool>`,
|
||||||
|
)
|
||||||
.join('\n\n');
|
.join('\n\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
static async execute(
|
static async execute(
|
||||||
name: string,
|
name: string,
|
||||||
params: any,
|
params: any,
|
||||||
additionalConfig: AdditionalConfig & { researchBlockId: string },
|
additionalConfig: AdditionalConfig & {
|
||||||
|
researchBlockId: string;
|
||||||
|
fileIds: string[];
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
const action = this.actions.get(name);
|
const action = this.actions.get(name);
|
||||||
|
|
||||||
@@ -67,7 +76,10 @@ class ActionRegistry {
|
|||||||
|
|
||||||
static async executeAll(
|
static async executeAll(
|
||||||
actions: ToolCall[],
|
actions: ToolCall[],
|
||||||
additionalConfig: AdditionalConfig & { researchBlockId: string },
|
additionalConfig: AdditionalConfig & {
|
||||||
|
researchBlockId: string;
|
||||||
|
fileIds: string[];
|
||||||
|
},
|
||||||
): Promise<ActionOutput[]> {
|
): Promise<ActionOutput[]> {
|
||||||
const results: ActionOutput[] = [];
|
const results: ActionOutput[] = [];
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ Use this tool to scrape and extract content from the provided URLs. This is usef
|
|||||||
You should only call this tool when the user has specifically requested information from certain web pages, never call this yourself to get extra information without user instruction.
|
You should only call this tool when the user has specifically requested information from certain web pages, never call this yourself to get extra information without user instruction.
|
||||||
|
|
||||||
For example, if the user says "Please summarize the content of https://example.com/article", you can call this tool with that URL to get the content and then provide the summary or "What does X mean according to https://example.com/page", you can call this tool with that URL to get the content and provide the explanation.
|
For example, if the user says "Please summarize the content of https://example.com/article", you can call this tool with that URL to get the content and then provide the summary or "What does X mean according to https://example.com/page", you can call this tool with that URL to get the content and provide the explanation.
|
||||||
`
|
`;
|
||||||
|
|
||||||
const scrapeURLAction: ResearchAction<typeof schema> = {
|
const scrapeURLAction: ResearchAction<typeof schema> = {
|
||||||
name: 'scrape_url',
|
name: 'scrape_url',
|
||||||
|
|||||||
102
src/lib/agents/search/researcher/actions/uploadsSearch.ts
Normal file
102
src/lib/agents/search/researcher/actions/uploadsSearch.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import z from 'zod';
|
||||||
|
import { ResearchAction } from '../../types';
|
||||||
|
import UploadStore from '@/lib/uploads/store';
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
queries: z
|
||||||
|
.array(z.string())
|
||||||
|
.describe(
|
||||||
|
'A list of queries to search in user uploaded files. Can be a maximum of 3 queries.',
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
const uploadsSearchAction: ResearchAction<typeof schema> = {
|
||||||
|
name: 'uploads_search',
|
||||||
|
enabled: (config) =>
|
||||||
|
(config.classification.classification.personalSearch &&
|
||||||
|
config.fileIds.length > 0) ||
|
||||||
|
config.fileIds.length > 0,
|
||||||
|
schema,
|
||||||
|
getToolDescription: () =>
|
||||||
|
`Use this tool to perform searches over the user's uploaded files. This is useful when you need to gather information from the user's documents to answer their questions. You can provide up to 3 queries at a time. You will have to use this every single time if this is present and relevant.`,
|
||||||
|
getDescription: () => `
|
||||||
|
Use this tool to perform searches over the user's uploaded files. This is useful when you need to gather information from the user's documents to answer their questions. You can provide up to 3 queries at a time. You will have to use this every single time if this is present and relevant.
|
||||||
|
Always ensure that the queries you use are directly relevant to the user's request and pertain to the content of their uploaded files.
|
||||||
|
|
||||||
|
For example, if the user says "Please find information about X in my uploaded documents", you can call this tool with a query related to X to retrieve the relevant information from their files.
|
||||||
|
Never use this tool to search the web or for information that is not contained within the user's uploaded files.
|
||||||
|
`,
|
||||||
|
execute: async (input, additionalConfig) => {
|
||||||
|
input.queries = input.queries.slice(0, 3);
|
||||||
|
|
||||||
|
const researchBlock = additionalConfig.session.getBlock(
|
||||||
|
additionalConfig.researchBlockId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (researchBlock && researchBlock.type === 'research') {
|
||||||
|
researchBlock.data.subSteps.push({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
type: 'upload_searching',
|
||||||
|
queries: input.queries,
|
||||||
|
});
|
||||||
|
|
||||||
|
additionalConfig.session.updateBlock(additionalConfig.researchBlockId, [
|
||||||
|
{
|
||||||
|
op: 'replace',
|
||||||
|
path: '/data/subSteps',
|
||||||
|
value: researchBlock.data.subSteps,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadStore = new UploadStore({
|
||||||
|
embeddingModel: additionalConfig.embedding,
|
||||||
|
fileIds: additionalConfig.fileIds,
|
||||||
|
});
|
||||||
|
|
||||||
|
const results = await uploadStore.query(input.queries, 10);
|
||||||
|
|
||||||
|
const seenIds = new Map<string, number>();
|
||||||
|
|
||||||
|
const filteredSearchResults = results
|
||||||
|
.map((result, index) => {
|
||||||
|
if (result.metadata.url && !seenIds.has(result.metadata.url)) {
|
||||||
|
seenIds.set(result.metadata.url, index);
|
||||||
|
return result;
|
||||||
|
} else if (result.metadata.url && seenIds.has(result.metadata.url)) {
|
||||||
|
const existingIndex = seenIds.get(result.metadata.url)!;
|
||||||
|
const existingResult = results[existingIndex];
|
||||||
|
|
||||||
|
existingResult.content += `\n\n${result.content}`;
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
})
|
||||||
|
.filter((r) => r !== undefined);
|
||||||
|
|
||||||
|
if (researchBlock && researchBlock.type === 'research') {
|
||||||
|
researchBlock.data.subSteps.push({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
type: 'upload_search_results',
|
||||||
|
results: filteredSearchResults,
|
||||||
|
});
|
||||||
|
|
||||||
|
additionalConfig.session.updateBlock(additionalConfig.researchBlockId, [
|
||||||
|
{
|
||||||
|
op: 'replace',
|
||||||
|
path: '/data/subSteps',
|
||||||
|
value: researchBlock.data.subSteps,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'search_results',
|
||||||
|
results: filteredSearchResults,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default uploadsSearchAction;
|
||||||
@@ -21,7 +21,7 @@ For example, if the user is asking about the features of a new technology, you m
|
|||||||
|
|
||||||
You can search for 3 queries in one go, make sure to utilize all 3 queries to maximize the information you can gather. If a question is simple, then split your queries to cover different aspects or related topics to get a comprehensive understanding.
|
You can search for 3 queries in one go, make sure to utilize all 3 queries to maximize the information you can gather. If a question is simple, then split your queries to cover different aspects or related topics to get a comprehensive understanding.
|
||||||
If this tool is present and no other tools are more relevant, you MUST use this tool to get the needed information.
|
If this tool is present and no other tools are more relevant, you MUST use this tool to get the needed information.
|
||||||
`
|
`;
|
||||||
|
|
||||||
const balancedModePrompt = `
|
const balancedModePrompt = `
|
||||||
Use this tool to perform web searches based on the provided queries. This is useful when you need to gather information from the web to answer the user's questions. You can provide up to 3 queries at a time. You will have to use this every single time if this is present and relevant.
|
Use this tool to perform web searches based on the provided queries. This is useful when you need to gather information from the web to answer the user's questions. You can provide up to 3 queries at a time. You will have to use this every single time if this is present and relevant.
|
||||||
@@ -32,16 +32,16 @@ Start initially with broader queries to get an overview, then narrow down with m
|
|||||||
Your queries shouldn't be sentences but rather keywords that are SEO friendly and can be used to search the web for information.
|
Your queries shouldn't be sentences but rather keywords that are SEO friendly and can be used to search the web for information.
|
||||||
|
|
||||||
For example if the user is asking about Tesla, your actions should be like:
|
For example if the user is asking about Tesla, your actions should be like:
|
||||||
1. __plan "The user is asking about Tesla. I will start with broader queries to get an overview of Tesla, then narrow down with more specific queries based on the results I receive." then
|
1. 0_reasoning "The user is asking about Tesla. I will start with broader queries to get an overview of Tesla, then narrow down with more specific queries based on the results I receive." then
|
||||||
2. web_search ["Tesla", "Tesla latest news", "Tesla stock price"] then
|
2. web_search ["Tesla", "Tesla latest news", "Tesla stock price"] then
|
||||||
3. __plan "Based on the previous search results, I will now narrow down my queries to focus on Tesla's recent developments and stock performance." then
|
3. 0_reasoning "Based on the previous search results, I will now narrow down my queries to focus on Tesla's recent developments and stock performance." then
|
||||||
4. web_search ["Tesla Q2 2025 earnings", "Tesla new model 2025", "Tesla stock analysis"] then done.
|
4. web_search ["Tesla Q2 2025 earnings", "Tesla new model 2025", "Tesla stock analysis"] then done.
|
||||||
5. __plan "I have gathered enough information to provide a comprehensive answer."
|
5. 0_reasoning "I have gathered enough information to provide a comprehensive answer."
|
||||||
6. done.
|
6. done.
|
||||||
|
|
||||||
You can search for 3 queries in one go, make sure to utilize all 3 queries to maximize the information you can gather. If a question is simple, then split your queries to cover different aspects or related topics to get a comprehensive understanding.
|
You can search for 3 queries in one go, make sure to utilize all 3 queries to maximize the information you can gather. If a question is simple, then split your queries to cover different aspects or related topics to get a comprehensive understanding.
|
||||||
If this tool is present and no other tools are more relevant, you MUST use this tool to get the needed information. You can call this tools, multiple times as needed.
|
If this tool is present and no other tools are more relevant, you MUST use this tool to get the needed information. You can call this tools, multiple times as needed.
|
||||||
`
|
`;
|
||||||
|
|
||||||
const qualityModePrompt = `
|
const qualityModePrompt = `
|
||||||
Use this tool to perform web searches based on the provided queries. This is useful when you need to gather information from the web to answer the user's questions. You can provide up to 3 queries at a time. You will have to use this every single time if this is present and relevant.
|
Use this tool to perform web searches based on the provided queries. This is useful when you need to gather information from the web to answer the user's questions. You can provide up to 3 queries at a time. You will have to use this every single time if this is present and relevant.
|
||||||
@@ -54,31 +54,32 @@ Your queries shouldn't be sentences but rather keywords that are SEO friendly an
|
|||||||
|
|
||||||
You can search for 3 queries in one go, make sure to utilize all 3 queries to maximize the information you can gather. If a question is simple, then split your queries to cover different aspects or related topics to get a comprehensive understanding.
|
You can search for 3 queries in one go, make sure to utilize all 3 queries to maximize the information you can gather. If a question is simple, then split your queries to cover different aspects or related topics to get a comprehensive understanding.
|
||||||
If this tool is present and no other tools are more relevant, you MUST use this tool to get the needed information. You can call this tools, multiple times as needed.
|
If this tool is present and no other tools are more relevant, you MUST use this tool to get the needed information. You can call this tools, multiple times as needed.
|
||||||
`
|
`;
|
||||||
|
|
||||||
const webSearchAction: ResearchAction<typeof actionSchema> = {
|
const webSearchAction: ResearchAction<typeof actionSchema> = {
|
||||||
name: 'web_search',
|
name: 'web_search',
|
||||||
schema: actionSchema,
|
schema: actionSchema,
|
||||||
getToolDescription: () => 'Use this tool to perform web searches based on the provided queries. This is useful when you need to gather information from the web to answer the user\'s questions. You can provide up to 3 queries at a time. You will have to use this every single time if this is present and relevant.',
|
getToolDescription: () =>
|
||||||
|
"Use this tool to perform web searches based on the provided queries. This is useful when you need to gather information from the web to answer the user's questions. You can provide up to 3 queries at a time. You will have to use this every single time if this is present and relevant.",
|
||||||
getDescription: (config) => {
|
getDescription: (config) => {
|
||||||
let prompt = ''
|
let prompt = '';
|
||||||
|
|
||||||
switch (config.mode) {
|
switch (config.mode) {
|
||||||
case 'speed':
|
case 'speed':
|
||||||
prompt = speedModePrompt
|
prompt = speedModePrompt;
|
||||||
break;
|
break;
|
||||||
case 'balanced':
|
case 'balanced':
|
||||||
prompt = balancedModePrompt
|
prompt = balancedModePrompt;
|
||||||
break;
|
break;
|
||||||
case 'quality':
|
case 'quality':
|
||||||
prompt = qualityModePrompt
|
prompt = qualityModePrompt;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
prompt = speedModePrompt
|
prompt = speedModePrompt;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return prompt
|
return prompt;
|
||||||
},
|
},
|
||||||
enabled: (config) =>
|
enabled: (config) =>
|
||||||
config.classification.classification.skipSearch === false,
|
config.classification.classification.skipSearch === false,
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import SessionManager from '@/lib/session';
|
|||||||
import { Message, ReasoningResearchBlock } from '@/lib/types';
|
import { Message, ReasoningResearchBlock } from '@/lib/types';
|
||||||
import formatChatHistoryAsString from '@/lib/utils/formatHistory';
|
import formatChatHistoryAsString from '@/lib/utils/formatHistory';
|
||||||
import { ToolCall } from '@/lib/models/types';
|
import { ToolCall } from '@/lib/models/types';
|
||||||
import fs from 'fs';
|
|
||||||
|
|
||||||
class Researcher {
|
class Researcher {
|
||||||
async research(
|
async research(
|
||||||
@@ -22,13 +21,15 @@ class Researcher {
|
|||||||
|
|
||||||
const availableTools = ActionRegistry.getAvailableActionTools({
|
const availableTools = ActionRegistry.getAvailableActionTools({
|
||||||
classification: input.classification,
|
classification: input.classification,
|
||||||
|
fileIds: input.config.fileIds,
|
||||||
mode: input.config.mode,
|
mode: input.config.mode,
|
||||||
});
|
});
|
||||||
|
|
||||||
const availableActionsDescription =
|
const availableActionsDescription =
|
||||||
ActionRegistry.getAvailableActionsDescriptions({
|
ActionRegistry.getAvailableActionsDescriptions({
|
||||||
classification: input.classification,
|
classification: input.classification,
|
||||||
mode: input.config.mode
|
fileIds: input.config.fileIds,
|
||||||
|
mode: input.config.mode,
|
||||||
});
|
});
|
||||||
|
|
||||||
const researchBlockId = crypto.randomUUID();
|
const researchBlockId = crypto.randomUUID();
|
||||||
@@ -59,6 +60,7 @@ class Researcher {
|
|||||||
input.config.mode,
|
input.config.mode,
|
||||||
i,
|
i,
|
||||||
maxIteration,
|
maxIteration,
|
||||||
|
input.config.fileIds,
|
||||||
);
|
);
|
||||||
|
|
||||||
const actionStream = input.config.llm.streamText({
|
const actionStream = input.config.llm.streamText({
|
||||||
@@ -83,7 +85,7 @@ class Researcher {
|
|||||||
if (partialRes.toolCallChunk.length > 0) {
|
if (partialRes.toolCallChunk.length > 0) {
|
||||||
partialRes.toolCallChunk.forEach((tc) => {
|
partialRes.toolCallChunk.forEach((tc) => {
|
||||||
if (
|
if (
|
||||||
tc.name === '___plan' &&
|
tc.name === '0_reasoning' &&
|
||||||
tc.arguments['plan'] &&
|
tc.arguments['plan'] &&
|
||||||
!reasoningEmitted &&
|
!reasoningEmitted &&
|
||||||
block &&
|
block &&
|
||||||
@@ -105,7 +107,7 @@ class Researcher {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
} else if (
|
} else if (
|
||||||
tc.name === '___plan' &&
|
tc.name === '0_reasoning' &&
|
||||||
tc.arguments['plan'] &&
|
tc.arguments['plan'] &&
|
||||||
reasoningEmitted &&
|
reasoningEmitted &&
|
||||||
block &&
|
block &&
|
||||||
@@ -162,6 +164,7 @@ class Researcher {
|
|||||||
embedding: input.config.embedding,
|
embedding: input.config.embedding,
|
||||||
session: session,
|
session: session,
|
||||||
researchBlockId: researchBlockId,
|
researchBlockId: researchBlockId,
|
||||||
|
fileIds: input.config.fileIds,
|
||||||
});
|
});
|
||||||
|
|
||||||
actionOutput.push(...actionResults);
|
actionOutput.push(...actionResults);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export type SearchSources = 'web' | 'discussions' | 'academic';
|
|||||||
|
|
||||||
export type SearchAgentConfig = {
|
export type SearchAgentConfig = {
|
||||||
sources: SearchSources[];
|
sources: SearchSources[];
|
||||||
|
fileIds: string[];
|
||||||
llm: BaseLLM<any>;
|
llm: BaseLLM<any>;
|
||||||
embedding: BaseEmbedding<any>;
|
embedding: BaseEmbedding<any>;
|
||||||
mode: 'speed' | 'balanced' | 'quality';
|
mode: 'speed' | 'balanced' | 'quality';
|
||||||
@@ -102,11 +103,16 @@ export interface ResearchAction<
|
|||||||
schema: z.ZodObject<any>;
|
schema: z.ZodObject<any>;
|
||||||
getToolDescription: (config: { mode: SearchAgentConfig['mode'] }) => string;
|
getToolDescription: (config: { mode: SearchAgentConfig['mode'] }) => string;
|
||||||
getDescription: (config: { mode: SearchAgentConfig['mode'] }) => string;
|
getDescription: (config: { mode: SearchAgentConfig['mode'] }) => string;
|
||||||
enabled: (config: { classification: ClassifierOutput, mode: SearchAgentConfig['mode'] }) => boolean;
|
enabled: (config: {
|
||||||
|
classification: ClassifierOutput;
|
||||||
|
fileIds: string[];
|
||||||
|
mode: SearchAgentConfig['mode'];
|
||||||
|
}) => boolean;
|
||||||
execute: (
|
execute: (
|
||||||
params: z.infer<TSchema>,
|
params: z.infer<TSchema>,
|
||||||
additionalConfig: AdditionalConfig & {
|
additionalConfig: AdditionalConfig & {
|
||||||
researchBlockId: string;
|
researchBlockId: string;
|
||||||
|
fileIds: string[];
|
||||||
},
|
},
|
||||||
) => Promise<ActionOutput>;
|
) => Promise<ActionOutput>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const generateSuggestions = async (
|
|||||||
input: SuggestionGeneratorInput,
|
input: SuggestionGeneratorInput,
|
||||||
llm: BaseLLM<any>,
|
llm: BaseLLM<any>,
|
||||||
) => {
|
) => {
|
||||||
const res = await llm.generateObject<z.infer<typeof schema>>({
|
const res = await llm.generateObject<typeof schema>({
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
role: 'system',
|
role: 'system',
|
||||||
|
|||||||
@@ -161,8 +161,13 @@ class OllamaLLM extends BaseLLM<OllamaConfig> {
|
|||||||
yield {
|
yield {
|
||||||
contentChunk: chunk.message.content,
|
contentChunk: chunk.message.content,
|
||||||
toolCallChunk:
|
toolCallChunk:
|
||||||
chunk.message.tool_calls?.map((tc) => ({
|
chunk.message.tool_calls?.map((tc, i) => ({
|
||||||
id: crypto.randomUUID(),
|
id: crypto
|
||||||
|
.createHash('sha256')
|
||||||
|
.update(
|
||||||
|
`${i}-${tc.function.name}`,
|
||||||
|
) /* Ollama currently doesn't return a tool call ID so we're creating one based on the index and tool call name */
|
||||||
|
.digest('hex'),
|
||||||
name: tc.function.name,
|
name: tc.function.name,
|
||||||
arguments: tc.function.arguments,
|
arguments: tc.function.arguments,
|
||||||
})) || [],
|
})) || [],
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
const getSpeedPrompt = (actionDesc: string, i: number, maxIteration: number) => {
|
import BaseEmbedding from '@/lib/models/base/embedding';
|
||||||
|
import UploadStore from '@/lib/uploads/store';
|
||||||
|
|
||||||
|
const getSpeedPrompt = (
|
||||||
|
actionDesc: string,
|
||||||
|
i: number,
|
||||||
|
maxIteration: number,
|
||||||
|
fileDesc: string,
|
||||||
|
) => {
|
||||||
const today = new Date().toLocaleDateString('en-US', {
|
const today = new Date().toLocaleDateString('en-US', {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: 'long',
|
month: 'long',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
})
|
});
|
||||||
|
|
||||||
return `
|
return `
|
||||||
Assistant is an action orchestrator. Your job is to fulfill user requests by selecting and executing the available tools—no free-form replies.
|
Assistant is an action orchestrator. Your job is to fulfill user requests by selecting and executing the available tools—no free-form replies.
|
||||||
@@ -65,18 +73,33 @@ const getSpeedPrompt = (actionDesc: string, i: number, maxIteration: number) =>
|
|||||||
- Call done when you have gathered enough to answer or performed the required actions.
|
- Call done when you have gathered enough to answer or performed the required actions.
|
||||||
- Do not invent tools. Do not return JSON.
|
- Do not invent tools. Do not return JSON.
|
||||||
</response_protocol>
|
</response_protocol>
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|
||||||
const getBalancedPrompt = (actionDesc: string, i: number, maxIteration: number) => {
|
${
|
||||||
|
fileDesc.length > 0
|
||||||
|
? `<user_uploaded_files>
|
||||||
|
The user has uploaded the following files which may be relevant to their request:
|
||||||
|
${fileDesc}
|
||||||
|
You can use the uploaded files search tool to look for information within these documents if needed.
|
||||||
|
</user_uploaded_files>`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBalancedPrompt = (
|
||||||
|
actionDesc: string,
|
||||||
|
i: number,
|
||||||
|
maxIteration: number,
|
||||||
|
fileDesc: string,
|
||||||
|
) => {
|
||||||
const today = new Date().toLocaleDateString('en-US', {
|
const today = new Date().toLocaleDateString('en-US', {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: 'long',
|
month: 'long',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
})
|
});
|
||||||
|
|
||||||
return `
|
return `
|
||||||
Assistant is an action orchestrator. Your job is to fulfill user requests by planning briefly and executing the available tools—no free-form replies.
|
Assistant is an action orchestrator. Your job is to fulfill user requests by reasoning briefly and executing the available tools—no free-form replies.
|
||||||
You will be shared with the conversation history between user and an AI, along with the user's latest follow-up question. Based on this, you must use the available tools to fulfill the user's request.
|
You will be shared with the conversation history between user and an AI, along with the user's latest follow-up question. Based on this, you must use the available tools to fulfill the user's request.
|
||||||
|
|
||||||
Today's date: ${today}
|
Today's date: ${today}
|
||||||
@@ -85,44 +108,43 @@ const getBalancedPrompt = (actionDesc: string, i: number, maxIteration: number)
|
|||||||
When you are finished, you must call the \`done\` tool. Never output text directly.
|
When you are finished, you must call the \`done\` tool. Never output text directly.
|
||||||
|
|
||||||
<goal>
|
<goal>
|
||||||
Fulfill the user's request with concise planning plus focused actions.
|
Fulfill the user's request with concise reasoning plus focused actions.
|
||||||
You must call the ___plan tool first on every turn to state a short plan. Open with a brief intent phrase (e.g., "Okay, the user wants to...", "Searching for...", "Looking into...") and lay out the steps you will take. Keep it natural language, no tool names.
|
You must call the 0_reasoning tool before every tool call in this assistant turn. Alternate: 0_reasoning → tool → 0_reasoning → tool ... and finish with 0_reasoning → done. Open each 0_reasoning with a brief intent phrase (e.g., "Okay, the user wants to...", "Searching for...", "Looking into...") and lay out your reasoning for the next step. Keep it natural language, no tool names.
|
||||||
After planning, use the available tools as needed to gather or act, then finish with done.
|
|
||||||
</goal>
|
</goal>
|
||||||
|
|
||||||
<core_principle>
|
<core_principle>
|
||||||
Your knowledge is outdated; if you have web search, use it to ground answers even for seemingly basic facts.
|
Your knowledge is outdated; if you have web search, use it to ground answers even for seemingly basic facts.
|
||||||
You can call at most 6 tools total per turn: up to 2 reasoning (___plan counts as reasoning), 2-3 information-gathering calls, and 1 done. If you hit the cap, stop after done.
|
You can call at most 6 tools total per turn: up to 2 reasoning (0_reasoning counts as reasoning), 2-3 information-gathering calls, and 1 done. If you hit the cap, stop after done.
|
||||||
Aim for at least two information-gathering calls when the answer is not already obvious; only skip the second if the question is trivial or you already have sufficient context.
|
Aim for at least two information-gathering calls when the answer is not already obvious; only skip the second if the question is trivial or you already have sufficient context.
|
||||||
Do not spam searches—pick the most targeted queries.
|
Do not spam searches—pick the most targeted queries.
|
||||||
</core_principle>
|
</core_principle>
|
||||||
|
|
||||||
<done_usage>
|
<done_usage>
|
||||||
Call done only after the plan plus the necessary tool calls are completed and you have enough to answer. If you call done early, stop. If you reach the tool cap, call done to conclude.
|
Call done only after the reasoning plus the necessary tool calls are completed and you have enough to answer. If you call done early, stop. If you reach the tool cap, call done to conclude.
|
||||||
</done_usage>
|
</done_usage>
|
||||||
|
|
||||||
<examples>
|
<examples>
|
||||||
|
|
||||||
## Example 1: Unknown Subject
|
## Example 1: Unknown Subject
|
||||||
User: "What is Kimi K2?"
|
User: "What is Kimi K2?"
|
||||||
Plan: "Okay, the user wants to know about Kimi K2. I will start by looking for what Kimi K2 is and its key details, then summarize the findings."
|
Reason: "Okay, the user wants to know about Kimi K2. I will start by looking for what Kimi K2 is and its key details, then summarize the findings."
|
||||||
Action: web_search ["Kimi K2", "Kimi K2 AI"] then done.
|
Action: web_search ["Kimi K2", "Kimi K2 AI"] then reasoning then done.
|
||||||
|
|
||||||
## Example 2: Subject You're Uncertain About
|
## Example 2: Subject You're Uncertain About
|
||||||
User: "What are the features of GPT-5.1?"
|
User: "What are the features of GPT-5.1?"
|
||||||
Plan: "The user is asking about GPT-5.1 features. I will search for current feature and release information, then compile a summary."
|
Reason: "The user is asking about GPT-5.1 features. I will search for current feature and release information, then compile a summary."
|
||||||
Action: web_search ["GPT-5.1", "GPT-5.1 features", "GPT-5.1 release"] then done.
|
Action: web_search ["GPT-5.1", "GPT-5.1 features", "GPT-5.1 release"] then reasoning then done.
|
||||||
|
|
||||||
## Example 3: After Tool calls Return Results
|
## Example 3: After Tool calls Return Results
|
||||||
User: "What are the features of GPT-5.1?"
|
User: "What are the features of GPT-5.1?"
|
||||||
[Previous tool calls returned the needed info]
|
[Previous tool calls returned the needed info]
|
||||||
Plan: "I have gathered enough information about GPT-5.1 features; I will now wrap up."
|
Reason: "I have gathered enough information about GPT-5.1 features; I will now wrap up."
|
||||||
Action: done.
|
Action: done.
|
||||||
|
|
||||||
</examples>
|
</examples>
|
||||||
|
|
||||||
<available_tools>
|
<available_tools>
|
||||||
YOU MUST ALWAYS CALL THE ___plan TOOL FIRST ON EVERY TURN BEFORE ANY OTHER ACTION. IF YOU DO NOT CALL IT, THE TOOL CALL WILL BE IGNORED.
|
YOU MUST CALL 0_reasoning BEFORE EVERY TOOL CALL IN THIS ASSISTANT TURN. IF YOU DO NOT CALL IT, THE TOOL CALL WILL BE IGNORED.
|
||||||
${actionDesc}
|
${actionDesc}
|
||||||
</available_tools>
|
</available_tools>
|
||||||
|
|
||||||
@@ -138,29 +160,44 @@ const getBalancedPrompt = (actionDesc: string, i: number, maxIteration: number)
|
|||||||
|
|
||||||
5. **Overthinking**: Keep reasoning simple and tool calls focused
|
5. **Overthinking**: Keep reasoning simple and tool calls focused
|
||||||
|
|
||||||
6. **Skipping the plan**: Always call ___plan first to outline your approach before other actions
|
6. **Skipping the reasoning step**: Always call 0_reasoning first to outline your approach before other actions
|
||||||
|
|
||||||
</mistakes_to_avoid>
|
</mistakes_to_avoid>
|
||||||
|
|
||||||
<response_protocol>
|
<response_protocol>
|
||||||
- NEVER output normal text to the user. ONLY call tools.
|
- NEVER output normal text to the user. ONLY call tools.
|
||||||
- Start with ___plan: open with intent phrase ("Okay, the user wants to...", "Looking into...", etc.) and lay out steps. No tool names.
|
- Start with 0_reasoning and call 0_reasoning before every tool call (including done): open with intent phrase ("Okay, the user wants to...", "Looking into...", etc.) and lay out your reasoning for the next step. No tool names.
|
||||||
- Choose tools based on the action descriptions provided above.
|
- Choose tools based on the action descriptions provided above.
|
||||||
- Default to web_search when information is missing or stale; keep queries targeted (max 3 per call).
|
- Default to web_search when information is missing or stale; keep queries targeted (max 3 per call).
|
||||||
- Use at most 6 tool calls total (___plan + 2-3 info calls + optional extra reasoning if needed + done). If done is called early, stop.
|
- Use at most 6 tool calls total (0_reasoning + 2-3 info calls + 0_reasoning + done). If done is called early, stop.
|
||||||
- Do not stop after a single information-gathering call unless the task is trivial or prior results already cover the answer.
|
- Do not stop after a single information-gathering call unless the task is trivial or prior results already cover the answer.
|
||||||
- Call done only after you have the needed info or actions completed; do not call it early.
|
- Call done only after you have the needed info or actions completed; do not call it early.
|
||||||
- Do not invent tools. Do not return JSON.
|
- Do not invent tools. Do not return JSON.
|
||||||
</response_protocol>
|
</response_protocol>
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|
||||||
const getQualityPrompt = (actionDesc: string, i: number, maxIteration: number) => {
|
${
|
||||||
|
fileDesc.length > 0
|
||||||
|
? `<user_uploaded_files>
|
||||||
|
The user has uploaded the following files which may be relevant to their request:
|
||||||
|
${fileDesc}
|
||||||
|
You can use the uploaded files search tool to look for information within these documents if needed.
|
||||||
|
</user_uploaded_files>`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getQualityPrompt = (
|
||||||
|
actionDesc: string,
|
||||||
|
i: number,
|
||||||
|
maxIteration: number,
|
||||||
|
fileDesc: string,
|
||||||
|
) => {
|
||||||
const today = new Date().toLocaleDateString('en-US', {
|
const today = new Date().toLocaleDateString('en-US', {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: 'long',
|
month: 'long',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
})
|
});
|
||||||
|
|
||||||
return `
|
return `
|
||||||
Assistant is a deep-research orchestrator. Your job is to fulfill user requests with the most thorough, comprehensive research possible—no free-form replies.
|
Assistant is a deep-research orchestrator. Your job is to fulfill user requests with the most thorough, comprehensive research possible—no free-form replies.
|
||||||
@@ -173,16 +210,16 @@ const getQualityPrompt = (actionDesc: string, i: number, maxIteration: number) =
|
|||||||
|
|
||||||
<goal>
|
<goal>
|
||||||
Conduct the deepest, most thorough research possible. Leave no stone unturned.
|
Conduct the deepest, most thorough research possible. Leave no stone unturned.
|
||||||
Follow an iterative plan-act loop: call ___plan first to outline your next step, then call the appropriate tool(s) to gather info or take action, then call ___plan again to reflect on results and decide the next step. Repeat until you have exhaustive coverage.
|
Follow an iterative reason-act loop: call 0_reasoning before every tool call to outline the next step, then call the tool, then 0_reasoning again to reflect and decide the next step. Repeat until you have exhaustive coverage.
|
||||||
Open each plan with a brief intent phrase (e.g., "Okay, the user wants to know about...", "From the results, it looks like...", "Now I need to dig into...") and describe what you'll do next. Keep it natural language, no tool names.
|
Open each 0_reasoning with a brief intent phrase (e.g., "Okay, the user wants to know about...", "From the results, it looks like...", "Now I need to dig into...") and describe what you'll do next. Keep it natural language, no tool names.
|
||||||
Finish with done only when you have comprehensive, multi-angle information.
|
Finish with done only when you have comprehensive, multi-angle information.
|
||||||
</goal>
|
</goal>
|
||||||
|
|
||||||
<core_principle>
|
<core_principle>
|
||||||
Your knowledge is outdated; always use the available tools to ground answers.
|
Your knowledge is outdated; always use the available tools to ground answers.
|
||||||
This is DEEP RESEARCH mode—be exhaustive. Explore multiple angles: definitions, features, comparisons, recent news, expert opinions, use cases, limitations, and alternatives.
|
This is DEEP RESEARCH mode—be exhaustive. Explore multiple angles: definitions, features, comparisons, recent news, expert opinions, use cases, limitations, and alternatives.
|
||||||
You can call up to 10 tools total per turn. Use an iterative loop: ___plan → tool call(s) → ___plan → tool call(s) → ... → done.
|
You can call up to 10 tools total per turn. Use an iterative loop: 0_reasoning → tool call(s) → 0_reasoning → tool call(s) → ... → 0_reasoning → done.
|
||||||
Never settle for surface-level answers. If results hint at more depth, plan your next step and follow up. Cross-reference information from multiple queries.
|
Never settle for surface-level answers. If results hint at more depth, reason about your next step and follow up. Cross-reference information from multiple queries.
|
||||||
</core_principle>
|
</core_principle>
|
||||||
|
|
||||||
<done_usage>
|
<done_usage>
|
||||||
@@ -193,41 +230,41 @@ const getQualityPrompt = (actionDesc: string, i: number, maxIteration: number) =
|
|||||||
|
|
||||||
## Example 1: Unknown Subject - Deep Dive
|
## Example 1: Unknown Subject - Deep Dive
|
||||||
User: "What is Kimi K2?"
|
User: "What is Kimi K2?"
|
||||||
Plan: "Okay, the user wants to know about Kimi K2. I'll start by finding out what it is and its key capabilities."
|
Reason: "Okay, the user wants to know about Kimi K2. I'll start by finding out what it is and its key capabilities."
|
||||||
[calls info-gathering tool]
|
[calls info-gathering tool]
|
||||||
Plan: "From the results, Kimi K2 is an AI model by Moonshot. Now I need to dig into how it compares to competitors and any recent news."
|
Reason: "From the results, Kimi K2 is an AI model by Moonshot. Now I need to dig into how it compares to competitors and any recent news."
|
||||||
[calls info-gathering tool]
|
[calls info-gathering tool]
|
||||||
Plan: "Got comparison info. Let me also check for limitations or critiques to give a balanced view."
|
Reason: "Got comparison info. Let me also check for limitations or critiques to give a balanced view."
|
||||||
[calls info-gathering tool]
|
[calls info-gathering tool]
|
||||||
Plan: "I now have comprehensive coverage—definition, capabilities, comparisons, and critiques. Wrapping up."
|
Reason: "I now have comprehensive coverage—definition, capabilities, comparisons, and critiques. Wrapping up."
|
||||||
Action: done.
|
Action: done.
|
||||||
|
|
||||||
## Example 2: Feature Research - Comprehensive
|
## Example 2: Feature Research - Comprehensive
|
||||||
User: "What are the features of GPT-5.1?"
|
User: "What are the features of GPT-5.1?"
|
||||||
Plan: "The user wants comprehensive GPT-5.1 feature information. I'll start with core features and specs."
|
Reason: "The user wants comprehensive GPT-5.1 feature information. I'll start with core features and specs."
|
||||||
[calls info-gathering tool]
|
[calls info-gathering tool]
|
||||||
Plan: "Got the basics. Now I should look into how it compares to GPT-4 and benchmark performance."
|
Reason: "Got the basics. Now I should look into how it compares to GPT-4 and benchmark performance."
|
||||||
[calls info-gathering tool]
|
[calls info-gathering tool]
|
||||||
Plan: "Good comparison data. Let me also gather use cases and expert opinions for depth."
|
Reason: "Good comparison data. Let me also gather use cases and expert opinions for depth."
|
||||||
[calls info-gathering tool]
|
[calls info-gathering tool]
|
||||||
Plan: "I have exhaustive coverage across features, comparisons, benchmarks, and reviews. Done."
|
Reason: "I have exhaustive coverage across features, comparisons, benchmarks, and reviews. Done."
|
||||||
Action: done.
|
Action: done.
|
||||||
|
|
||||||
## Example 3: Iterative Refinement
|
## Example 3: Iterative Refinement
|
||||||
User: "Tell me about quantum computing applications in healthcare."
|
User: "Tell me about quantum computing applications in healthcare."
|
||||||
Plan: "Okay, the user wants to know about quantum computing in healthcare. I'll start with an overview of current applications."
|
Reason: "Okay, the user wants to know about quantum computing in healthcare. I'll start with an overview of current applications."
|
||||||
[calls info-gathering tool]
|
[calls info-gathering tool]
|
||||||
Plan: "Results mention drug discovery and diagnostics. Let me dive deeper into drug discovery use cases."
|
Reason: "Results mention drug discovery and diagnostics. Let me dive deeper into drug discovery use cases."
|
||||||
[calls info-gathering tool]
|
[calls info-gathering tool]
|
||||||
Plan: "Now I'll explore the diagnostics angle and any recent breakthroughs."
|
Reason: "Now I'll explore the diagnostics angle and any recent breakthroughs."
|
||||||
[calls info-gathering tool]
|
[calls info-gathering tool]
|
||||||
Plan: "Comprehensive coverage achieved. Wrapping up."
|
Reason: "Comprehensive coverage achieved. Wrapping up."
|
||||||
Action: done.
|
Action: done.
|
||||||
|
|
||||||
</examples>
|
</examples>
|
||||||
|
|
||||||
<available_tools>
|
<available_tools>
|
||||||
YOU MUST ALWAYS CALL THE ___plan TOOL FIRST ON EVERY TURN BEFORE ANY OTHER ACTION. IF YOU DO NOT CALL IT, THE TOOL CALL WILL BE IGNORED.
|
YOU MUST CALL 0_reasoning BEFORE EVERY TOOL CALL IN THIS ASSISTANT TURN. IF YOU DO NOT CALL IT, THE TOOL CALL WILL BE IGNORED.
|
||||||
${actionDesc}
|
${actionDesc}
|
||||||
</available_tools>
|
</available_tools>
|
||||||
|
|
||||||
@@ -254,44 +291,64 @@ const getQualityPrompt = (actionDesc: string, i: number, maxIteration: number) =
|
|||||||
|
|
||||||
5. **Premature done**: Don't call done until you've exhausted reasonable research avenues
|
5. **Premature done**: Don't call done until you've exhausted reasonable research avenues
|
||||||
|
|
||||||
6. **Skipping the plan**: Always call ___plan first to outline your research strategy
|
6. **Skipping the reasoning step**: Always call 0_reasoning first to outline your research strategy
|
||||||
|
|
||||||
</mistakes_to_avoid>
|
</mistakes_to_avoid>
|
||||||
|
|
||||||
<response_protocol>
|
<response_protocol>
|
||||||
- NEVER output normal text to the user. ONLY call tools.
|
- NEVER output normal text to the user. ONLY call tools.
|
||||||
- Follow an iterative loop: ___plan → tool call(s) → ___plan → tool call(s) → ... → done.
|
- Follow an iterative loop: 0_reasoning → tool call → 0_reasoning → tool call → ... → 0_reasoning → done.
|
||||||
- Each ___plan should reflect on previous results (if any) and state the next research step. No tool names in the plan.
|
- Each 0_reasoning should reflect on previous results (if any) and state the next research step. No tool names in the reasoning.
|
||||||
- Choose tools based on the action descriptions provided above—use whatever tools are available to accomplish the task.
|
- Choose tools based on the action descriptions provided above—use whatever tools are available to accomplish the task.
|
||||||
- Aim for 4-7 information-gathering calls covering different angles; cross-reference and follow up on interesting leads.
|
- Aim for 4-7 information-gathering calls covering different angles; cross-reference and follow up on interesting leads.
|
||||||
- Call done only after comprehensive, multi-angle research is complete.
|
- Call done only after comprehensive, multi-angle research is complete.
|
||||||
- Do not invent tools. Do not return JSON.
|
- Do not invent tools. Do not return JSON.
|
||||||
</response_protocol>
|
</response_protocol>
|
||||||
`
|
|
||||||
}
|
${
|
||||||
|
fileDesc.length > 0
|
||||||
|
? `<user_uploaded_files>
|
||||||
|
The user has uploaded the following files which may be relevant to their request:
|
||||||
|
${fileDesc}
|
||||||
|
You can use the uploaded files search tool to look for information within these documents if needed.
|
||||||
|
</user_uploaded_files>`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
export const getResearcherPrompt = (
|
export const getResearcherPrompt = (
|
||||||
actionDesc: string,
|
actionDesc: string,
|
||||||
mode: 'speed' | 'balanced' | 'quality',
|
mode: 'speed' | 'balanced' | 'quality',
|
||||||
i: number,
|
i: number,
|
||||||
maxIteration: number,
|
maxIteration: number,
|
||||||
|
fileIds: string[],
|
||||||
) => {
|
) => {
|
||||||
let prompt = ''
|
let prompt = '';
|
||||||
|
|
||||||
|
const filesData = UploadStore.getFileData(fileIds);
|
||||||
|
|
||||||
|
const fileDesc = filesData
|
||||||
|
.map(
|
||||||
|
(f) =>
|
||||||
|
`<file><name>${f.fileName}</name><initial_content>${f.initialContent}</initial_content></file>`,
|
||||||
|
)
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case 'speed':
|
case 'speed':
|
||||||
prompt = getSpeedPrompt(actionDesc, i, maxIteration)
|
prompt = getSpeedPrompt(actionDesc, i, maxIteration, fileDesc);
|
||||||
break
|
break;
|
||||||
case 'balanced':
|
case 'balanced':
|
||||||
prompt = getBalancedPrompt(actionDesc, i, maxIteration)
|
prompt = getBalancedPrompt(actionDesc, i, maxIteration, fileDesc);
|
||||||
break
|
break;
|
||||||
case 'quality':
|
case 'quality':
|
||||||
prompt = getQualityPrompt(actionDesc, i, maxIteration)
|
prompt = getQualityPrompt(actionDesc, i, maxIteration, fileDesc);
|
||||||
break
|
break;
|
||||||
default:
|
default:
|
||||||
prompt = getSpeedPrompt(actionDesc, i, maxIteration)
|
prompt = getSpeedPrompt(actionDesc, i, maxIteration, fileDesc);
|
||||||
break
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return prompt
|
return prompt;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,87 +1,46 @@
|
|||||||
export const getWriterPrompt = (context: string) => {
|
export const getWriterPrompt = (context: string) => {
|
||||||
return `
|
return `
|
||||||
You are Perplexica, an AI assistant that provides helpful, accurate, and engaging answers. You combine web search results with a warm, conversational tone to deliver responses that feel personal and genuinely useful.
|
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.
|
||||||
|
|
||||||
## Core Principles
|
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.
|
||||||
|
|
||||||
**Be warm and conversational**: Write like you're having a friendly conversation with someone curious about the topic. Show genuine interest in helping them understand. Avoid being robotic or overly formal.
|
### 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.
|
||||||
|
|
||||||
**Be informative and thorough**: Address the user's query comprehensively using the provided context. Explain concepts clearly and anticipate follow-up questions they might have.
|
### 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.
|
||||||
|
|
||||||
**Be honest and credible**: Cite your sources using [number] notation. If information is uncertain or unavailable, say so transparently.
|
### 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.
|
||||||
|
|
||||||
**No emojis**: Keep responses clean and professional. Never use emojis unless the user explicitly requests them.
|
|
||||||
|
|
||||||
## Formatting 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.
|
||||||
|
|
||||||
**Use Markdown effectively**:
|
<context>
|
||||||
- Use headings (## and ###) to organize longer responses into logical sections
|
${context}
|
||||||
- Use **bold** for key terms and *italics* for emphasis
|
</context>
|
||||||
- Use bullet points and numbered lists to break down complex information
|
|
||||||
- Use tables when comparing data, features, or options
|
|
||||||
- Use code blocks for technical content when appropriate
|
|
||||||
|
|
||||||
**Adapt length to the query**:
|
Current date & time in ISO format (UTC timezone) is: ${new Date().toISOString()}.
|
||||||
- Simple questions (weather, calculations, quick facts): Brief, direct answers
|
|
||||||
- Complex topics: Structured responses with sections, context, and depth
|
|
||||||
- Always start with the direct answer before expanding into details
|
|
||||||
|
|
||||||
**No main title**: Jump straight into your response without a title heading.
|
|
||||||
|
|
||||||
**No references section**: Never include a "Sources" or "References" section at the end. Citations are handled inline only.
|
|
||||||
|
|
||||||
## Citation Rules
|
|
||||||
|
|
||||||
**Cite all factual claims** using [number] notation corresponding to sources in the context:
|
|
||||||
- Place citations at the end of the relevant sentence or clause
|
|
||||||
- Example: "The Great Wall of China stretches over 13,000 miles[1]."
|
|
||||||
- Use multiple citations when information comes from several sources[1][2]
|
|
||||||
|
|
||||||
**Never cite widget data**: Weather, stock prices, calculations, and other widget data should be stated directly without any citation notation.
|
|
||||||
|
|
||||||
**Never list citation mappings**: Only use [number] in the text. Do not provide a list showing which number corresponds to which source.
|
|
||||||
|
|
||||||
**CRITICAL - No references section**: NEVER include a "Sources", "References", footnotes, or any numbered list at the end of your response that maps citations to their sources. This is strictly forbidden. The system handles source display separately. Your response must end with your final paragraph of content, not a list of sources.
|
|
||||||
|
|
||||||
## Widget Data
|
|
||||||
|
|
||||||
Widget data (weather, stocks, calculations) is displayed to the user in interactive cards above your response.
|
|
||||||
|
|
||||||
**IMPORTANT**: When widget data is present, keep your response VERY brief (2-3 sentences max). The user already sees the detailed data in the widget card. Do NOT repeat all the widget data in your text response.
|
|
||||||
|
|
||||||
For example, for a weather query, just say:
|
|
||||||
"It's currently -8.7°C in New York with overcast skies. You can see the full details including hourly and daily forecasts in the weather card above."
|
|
||||||
|
|
||||||
**Do NOT**:
|
|
||||||
- List out all the weather metrics (temperature, humidity, wind, pressure, etc.)
|
|
||||||
- Provide forecasts unless explicitly asked
|
|
||||||
- Add citations to widget data
|
|
||||||
- Repeat information that's already visible in the widget
|
|
||||||
|
|
||||||
## Response Style
|
|
||||||
|
|
||||||
**Opening**: Start with a direct, engaging answer to the question. Get to the point quickly.
|
|
||||||
|
|
||||||
**Body**: Expand with relevant details, context, or explanations. Use formatting to make information scannable and easy to digest.
|
|
||||||
|
|
||||||
**Closing**: For longer responses, summarize key takeaways or suggest related topics they might find interesting. Keep it natural, not formulaic.
|
|
||||||
|
|
||||||
## When Information is Limited
|
|
||||||
|
|
||||||
If you cannot find relevant information, respond honestly:
|
|
||||||
"I wasn't able to find specific information about this topic. You might want to try rephrasing your question, or I can help you explore related areas."
|
|
||||||
|
|
||||||
Suggest alternative angles or related topics that might be helpful.
|
|
||||||
|
|
||||||
<context>
|
|
||||||
${context}
|
|
||||||
</context>
|
|
||||||
|
|
||||||
Current date & time in ISO format (UTC timezone) is: ${new Date().toISOString()}.
|
|
||||||
|
|
||||||
FINAL REMINDERS:
|
|
||||||
1. DO NOT add a references/sources section at the end. Your response ends with content, not citations.
|
|
||||||
2. For widget queries (weather, stocks, calculations): Keep it to 2-3 sentences. The widget shows the details.
|
|
||||||
3. No emojis.
|
|
||||||
`;
|
`;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -87,11 +87,25 @@ export type ReadingResearchBlock = {
|
|||||||
reading: Chunk[];
|
reading: Chunk[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UploadSearchingResearchBlock = {
|
||||||
|
id: string;
|
||||||
|
type: 'upload_searching';
|
||||||
|
queries: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UploadSearchResultsResearchBlock = {
|
||||||
|
id: string;
|
||||||
|
type: 'upload_search_results';
|
||||||
|
results: Chunk[];
|
||||||
|
};
|
||||||
|
|
||||||
export type ResearchBlockSubStep =
|
export type ResearchBlockSubStep =
|
||||||
| ReasoningResearchBlock
|
| ReasoningResearchBlock
|
||||||
| SearchingResearchBlock
|
| SearchingResearchBlock
|
||||||
| SearchResultsResearchBlock
|
| SearchResultsResearchBlock
|
||||||
| ReadingResearchBlock;
|
| ReadingResearchBlock
|
||||||
|
| UploadSearchingResearchBlock
|
||||||
|
| UploadSearchResultsResearchBlock;
|
||||||
|
|
||||||
export type ResearchBlock = {
|
export type ResearchBlock = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
217
src/lib/uploads/manager.ts
Normal file
217
src/lib/uploads/manager.ts
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
import path from "path";
|
||||||
|
import BaseEmbedding from "../models/base/embedding"
|
||||||
|
import crypto from "crypto"
|
||||||
|
import fs from 'fs';
|
||||||
|
import { splitText } from "../utils/splitText";
|
||||||
|
import { PDFParse } from 'pdf-parse';
|
||||||
|
import officeParser from 'officeparser'
|
||||||
|
import { Chunk } from "../types";
|
||||||
|
|
||||||
|
const supportedMimeTypes = ['application/pdf', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'text/plain'] as const
|
||||||
|
|
||||||
|
type SupportedMimeType = typeof supportedMimeTypes[number];
|
||||||
|
|
||||||
|
type UploadManagerParams = {
|
||||||
|
embeddingModel: BaseEmbedding<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecordedFile = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
filePath: string;
|
||||||
|
contentPath: string;
|
||||||
|
uploadedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type FileRes = {
|
||||||
|
fileName: string;
|
||||||
|
fileExtension: string;
|
||||||
|
fileId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class UploadManager {
|
||||||
|
private embeddingModel: BaseEmbedding<any>;
|
||||||
|
static uploadsDir = path.join(process.cwd(), 'data', 'uploads');
|
||||||
|
static uploadedFilesRecordPath = path.join(this.uploadsDir, 'uploaded_files.json');
|
||||||
|
|
||||||
|
constructor(private params: UploadManagerParams) {
|
||||||
|
this.embeddingModel = params.embeddingModel;
|
||||||
|
|
||||||
|
if (!fs.existsSync(UploadManager.uploadsDir)) {
|
||||||
|
fs.mkdirSync(UploadManager.uploadsDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(UploadManager.uploadedFilesRecordPath)) {
|
||||||
|
const data = {
|
||||||
|
files: []
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(UploadManager.uploadedFilesRecordPath, JSON.stringify(data, null, 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static getRecordedFiles(): RecordedFile[] {
|
||||||
|
const data = fs.readFileSync(UploadManager.uploadedFilesRecordPath, 'utf-8');
|
||||||
|
return JSON.parse(data).files;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static addNewRecordedFile(fileRecord: RecordedFile) {
|
||||||
|
const currentData = this.getRecordedFiles()
|
||||||
|
|
||||||
|
currentData.push(fileRecord);
|
||||||
|
|
||||||
|
fs.writeFileSync(UploadManager.uploadedFilesRecordPath, JSON.stringify({ files: currentData }, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
static getFile(fileId: string): RecordedFile | null {
|
||||||
|
const recordedFiles = this.getRecordedFiles();
|
||||||
|
|
||||||
|
return recordedFiles.find(f => f.id === fileId) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getFileChunks(fileId: string): { content: string; embedding: number[] }[] {
|
||||||
|
try {
|
||||||
|
const recordedFile = this.getFile(fileId);
|
||||||
|
|
||||||
|
if (!recordedFile) {
|
||||||
|
throw new Error(`File with ID ${fileId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentData = JSON.parse(fs.readFileSync(recordedFile.contentPath, 'utf-8'))
|
||||||
|
|
||||||
|
return contentData.chunks;
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Error getting file chunks:', err);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async extractContentAndEmbed(filePath: string, fileType: SupportedMimeType): Promise<string> {
|
||||||
|
switch (fileType) {
|
||||||
|
case 'text/plain':
|
||||||
|
const content = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
|
||||||
|
const splittedText = splitText(content, 256, 64)
|
||||||
|
const embeddings = await this.embeddingModel.embedText(splittedText)
|
||||||
|
|
||||||
|
if (embeddings.length !== splittedText.length) {
|
||||||
|
throw new Error('Embeddings and text chunks length mismatch');
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentPath = filePath.split('.').slice(0, -1).join('.') + '.content.json';
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
chunks: splittedText.map((text, i) => {
|
||||||
|
return {
|
||||||
|
content: text,
|
||||||
|
embedding: embeddings[i],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(contentPath, JSON.stringify(data, null, 2));
|
||||||
|
|
||||||
|
return contentPath;
|
||||||
|
case 'application/pdf':
|
||||||
|
const pdfBuffer = fs.readFileSync(filePath);
|
||||||
|
|
||||||
|
const parser = new PDFParse({
|
||||||
|
data: pdfBuffer
|
||||||
|
})
|
||||||
|
|
||||||
|
const pdfText = await parser.getText().then(res => res.text)
|
||||||
|
|
||||||
|
const pdfSplittedText = splitText(pdfText, 256, 64)
|
||||||
|
const pdfEmbeddings = await this.embeddingModel.embedText(pdfSplittedText)
|
||||||
|
|
||||||
|
if (pdfEmbeddings.length !== pdfSplittedText.length) {
|
||||||
|
throw new Error('Embeddings and text chunks length mismatch');
|
||||||
|
}
|
||||||
|
|
||||||
|
const pdfContentPath = filePath.split('.').slice(0, -1).join('.') + '.content.json';
|
||||||
|
|
||||||
|
const pdfData = {
|
||||||
|
chunks: pdfSplittedText.map((text, i) => {
|
||||||
|
return {
|
||||||
|
content: text,
|
||||||
|
embedding: pdfEmbeddings[i],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(pdfContentPath, JSON.stringify(pdfData, null, 2));
|
||||||
|
|
||||||
|
return pdfContentPath;
|
||||||
|
case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
|
||||||
|
const docBuffer = fs.readFileSync(filePath);
|
||||||
|
|
||||||
|
const docText = await officeParser.parseOfficeAsync(docBuffer)
|
||||||
|
|
||||||
|
const docSplittedText = splitText(docText, 256, 64)
|
||||||
|
const docEmbeddings = await this.embeddingModel.embedText(docSplittedText)
|
||||||
|
|
||||||
|
if (docEmbeddings.length !== docSplittedText.length) {
|
||||||
|
throw new Error('Embeddings and text chunks length mismatch');
|
||||||
|
}
|
||||||
|
|
||||||
|
const docContentPath = filePath.split('.').slice(0, -1).join('.') + '.content.json';
|
||||||
|
|
||||||
|
const docData = {
|
||||||
|
chunks: docSplittedText.map((text, i) => {
|
||||||
|
return {
|
||||||
|
content: text,
|
||||||
|
embedding: docEmbeddings[i],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(docContentPath, JSON.stringify(docData, null, 2));
|
||||||
|
|
||||||
|
return docContentPath;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported file type: ${fileType}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async processFiles(files: File[]): Promise<FileRes[]> {
|
||||||
|
const processedFiles: FileRes[] = [];
|
||||||
|
|
||||||
|
await Promise.all(files.map(async (file) => {
|
||||||
|
if (!(supportedMimeTypes as unknown as string[]).includes(file.type)) {
|
||||||
|
throw new Error(`File type ${file.type} not supported`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileId = crypto.randomBytes(16).toString('hex');
|
||||||
|
|
||||||
|
const fileExtension = file.name.split('.').pop();
|
||||||
|
const fileName = `${crypto.randomBytes(16).toString('hex')}.${fileExtension}`;
|
||||||
|
const filePath = path.join(UploadManager.uploadsDir, fileName);
|
||||||
|
|
||||||
|
const buffer = Buffer.from(await file.arrayBuffer())
|
||||||
|
|
||||||
|
fs.writeFileSync(filePath, buffer);
|
||||||
|
|
||||||
|
const contentFilePath = await this.extractContentAndEmbed(filePath, file.type as SupportedMimeType);
|
||||||
|
|
||||||
|
const fileRecord: RecordedFile = {
|
||||||
|
id: fileId,
|
||||||
|
name: file.name,
|
||||||
|
filePath: filePath,
|
||||||
|
contentPath: contentFilePath,
|
||||||
|
uploadedAt: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
|
||||||
|
UploadManager.addNewRecordedFile(fileRecord);
|
||||||
|
|
||||||
|
processedFiles.push({
|
||||||
|
fileExtension: fileExtension || '',
|
||||||
|
fileId,
|
||||||
|
fileName: file.name
|
||||||
|
});
|
||||||
|
}))
|
||||||
|
|
||||||
|
return processedFiles;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UploadManager;
|
||||||
122
src/lib/uploads/store.ts
Normal file
122
src/lib/uploads/store.ts
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import BaseEmbedding from "../models/base/embedding";
|
||||||
|
import UploadManager from "./manager";
|
||||||
|
import computeSimilarity from "../utils/computeSimilarity";
|
||||||
|
import { Chunk } from "../types";
|
||||||
|
import { hashObj } from "../serverUtils";
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
type UploadStoreParams = {
|
||||||
|
embeddingModel: BaseEmbedding<any>;
|
||||||
|
fileIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type StoreRecord = {
|
||||||
|
embedding: number[];
|
||||||
|
content: string;
|
||||||
|
fileId: string;
|
||||||
|
metadata: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
class UploadStore {
|
||||||
|
embeddingModel: BaseEmbedding<any>;
|
||||||
|
fileIds: string[];
|
||||||
|
records: StoreRecord[] = [];
|
||||||
|
|
||||||
|
constructor(private params: UploadStoreParams) {
|
||||||
|
this.embeddingModel = params.embeddingModel;
|
||||||
|
this.fileIds = params.fileIds;
|
||||||
|
this.initializeStore()
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeStore() {
|
||||||
|
this.fileIds.forEach((fileId) => {
|
||||||
|
const file = UploadManager.getFile(fileId)
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
throw new Error(`File with ID ${fileId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const chunks = UploadManager.getFileChunks(fileId);
|
||||||
|
|
||||||
|
this.records.push(...chunks.map((chunk) => ({
|
||||||
|
embedding: chunk.embedding,
|
||||||
|
content: chunk.content,
|
||||||
|
fileId: fileId,
|
||||||
|
metadata: {
|
||||||
|
fileName: file.name,
|
||||||
|
title: file.name,
|
||||||
|
url: `file_id://${file.id}`,
|
||||||
|
}
|
||||||
|
})))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async query(queries: string[], topK: number): Promise<Chunk[]> {
|
||||||
|
const queryEmbeddings = await this.embeddingModel.embedText(queries)
|
||||||
|
|
||||||
|
const results: { chunk: Chunk; score: number; }[][] = [];
|
||||||
|
const hashResults: string[][] = []
|
||||||
|
|
||||||
|
await Promise.all(queryEmbeddings.map(async (query) => {
|
||||||
|
const similarities = this.records.map((record, idx) => {
|
||||||
|
return {
|
||||||
|
chunk: {
|
||||||
|
content: record.content,
|
||||||
|
metadata: {
|
||||||
|
...record.metadata,
|
||||||
|
fileId: record.fileId,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
score: computeSimilarity(query, record.embedding)
|
||||||
|
} as { chunk: Chunk; score: number; };
|
||||||
|
}).sort((a, b) => b.score - a.score)
|
||||||
|
|
||||||
|
results.push(similarities)
|
||||||
|
hashResults.push(similarities.map(s => hashObj(s)))
|
||||||
|
}))
|
||||||
|
|
||||||
|
const chunkMap: Map<string, Chunk> = new Map();
|
||||||
|
const scoreMap: Map<string, number> = new Map();
|
||||||
|
const k = 60;
|
||||||
|
|
||||||
|
for (let i = 0; i < results.length; i++) {
|
||||||
|
for (let j = 0; j < results[i].length; j++) {
|
||||||
|
const chunkHash = hashResults[i][j]
|
||||||
|
|
||||||
|
chunkMap.set(chunkHash, results[i][j].chunk);
|
||||||
|
scoreMap.set(chunkHash, (scoreMap.get(chunkHash) || 0) + results[i][j].score / (j + 1 + k));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalResults = Array.from(scoreMap.entries())
|
||||||
|
.sort((a, b) => b[1] - a[1])
|
||||||
|
.map(([chunkHash, _score]) => {
|
||||||
|
return chunkMap.get(chunkHash)!;
|
||||||
|
})
|
||||||
|
|
||||||
|
return finalResults.slice(0, topK);
|
||||||
|
}
|
||||||
|
|
||||||
|
static getFileData(fileIds: string[]): { fileName: string; initialContent: string }[] {
|
||||||
|
const filesData: { fileName: string; initialContent: string }[] = [];
|
||||||
|
|
||||||
|
fileIds.forEach((fileId) => {
|
||||||
|
const file = UploadManager.getFile(fileId)
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
throw new Error(`File with ID ${fileId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const chunks = UploadManager.getFileChunks(fileId);
|
||||||
|
|
||||||
|
filesData.push({
|
||||||
|
fileName: file.name,
|
||||||
|
initialContent: chunks.slice(0, 3).map(c => c.content).join('\n---\n'),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return filesData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UploadStore
|
||||||
74
src/lib/utils/splitText.ts
Normal file
74
src/lib/utils/splitText.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { getEncoding } from 'js-tiktoken';
|
||||||
|
|
||||||
|
const splitRegex = /(?<=\. |\n|! |\? |; |:\s|\d+\.\s|- |\* )/g;
|
||||||
|
|
||||||
|
const enc = getEncoding('cl100k_base');
|
||||||
|
|
||||||
|
const getTokenCount = (text: string): number => {
|
||||||
|
try {
|
||||||
|
return enc.encode(text).length;
|
||||||
|
} catch {
|
||||||
|
return Math.ceil(text.length / 4);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const splitText = (
|
||||||
|
text: string,
|
||||||
|
maxTokens = 512,
|
||||||
|
overlapTokens = 64,
|
||||||
|
): string[] => {
|
||||||
|
const segments = text.split(splitRegex).filter(Boolean);
|
||||||
|
|
||||||
|
if (segments.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const segmentTokenCounts = segments.map(getTokenCount);
|
||||||
|
|
||||||
|
const result: string[] = [];
|
||||||
|
|
||||||
|
let chunkStart = 0;
|
||||||
|
|
||||||
|
while (chunkStart < segments.length) {
|
||||||
|
let chunkEnd = chunkStart;
|
||||||
|
let currentTokenCount = 0;
|
||||||
|
|
||||||
|
while (chunkEnd < segments.length && currentTokenCount < maxTokens) {
|
||||||
|
if (currentTokenCount + segmentTokenCounts[chunkEnd] > maxTokens) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentTokenCount += segmentTokenCounts[chunkEnd];
|
||||||
|
chunkEnd++;
|
||||||
|
}
|
||||||
|
|
||||||
|
let overlapBeforeStart = Math.max(0, chunkStart - 1);
|
||||||
|
let overlapBeforeTokenCount = 0;
|
||||||
|
|
||||||
|
while (overlapBeforeStart >= 0 && overlapBeforeTokenCount < overlapTokens) {
|
||||||
|
if (
|
||||||
|
overlapBeforeTokenCount + segmentTokenCounts[overlapBeforeStart] >
|
||||||
|
overlapTokens
|
||||||
|
) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
overlapBeforeTokenCount += segmentTokenCounts[overlapBeforeStart];
|
||||||
|
overlapBeforeStart--;
|
||||||
|
}
|
||||||
|
|
||||||
|
const overlapStartIndex = Math.max(0, overlapBeforeStart + 1);
|
||||||
|
|
||||||
|
const overlapBeforeContent = segments
|
||||||
|
.slice(overlapStartIndex, chunkStart)
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
const chunkContent = segments.slice(chunkStart, chunkEnd).join('');
|
||||||
|
|
||||||
|
result.push(overlapBeforeContent + chunkContent);
|
||||||
|
|
||||||
|
chunkStart = chunkEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user