feat(agent): Refactor search agents and implement SpeedSearchAgent
- Updated FileSearchAgent to improve code readability and formatting. - Refactored SynthesizerAgent for better prompt handling and document processing. - Enhanced TaskManagerAgent with clearer file context handling. - Modified AgentSearch to maintain consistent parameter formatting. - Introduced SpeedSearchAgent for optimized search functionality. - Updated metaSearchAgent to support new SpeedSearchAgent. - Improved file processing utilities for better document handling. - Added test attachments for sporting events queries.
This commit is contained in:
parent
de3d26fb15
commit
d66300e78e
19 changed files with 832 additions and 996 deletions
11
.github/copilot-instructions.md
vendored
11
.github/copilot-instructions.md
vendored
|
|
@ -15,6 +15,7 @@ The system works through these main steps:
|
||||||
## Architecture Details
|
## Architecture Details
|
||||||
|
|
||||||
### Technology Stack
|
### Technology Stack
|
||||||
|
|
||||||
- **Frontend**: React, Next.js, Tailwind CSS
|
- **Frontend**: React, Next.js, Tailwind CSS
|
||||||
- **Backend**: Node.js
|
- **Backend**: Node.js
|
||||||
- **Database**: SQLite with Drizzle ORM
|
- **Database**: SQLite with Drizzle ORM
|
||||||
|
|
@ -23,29 +24,31 @@ The system works through these main steps:
|
||||||
- **Content Processing**: Mozilla Readability, Cheerio, Playwright
|
- **Content Processing**: Mozilla Readability, Cheerio, Playwright
|
||||||
|
|
||||||
### Database (SQLite + Drizzle ORM)
|
### Database (SQLite + Drizzle ORM)
|
||||||
|
|
||||||
- Schema: `src/lib/db/schema.ts`
|
- Schema: `src/lib/db/schema.ts`
|
||||||
- Tables: `messages`, `chats`, `systemPrompts`
|
- Tables: `messages`, `chats`, `systemPrompts`
|
||||||
- Configuration: `drizzle.config.ts`
|
- Configuration: `drizzle.config.ts`
|
||||||
- Local file: `data/db.sqlite`
|
- Local file: `data/db.sqlite`
|
||||||
|
|
||||||
### AI/ML Stack
|
### AI/ML Stack
|
||||||
|
|
||||||
- **LLM Providers**: OpenAI, Anthropic, Groq, Ollama, Gemini, DeepSeek, LM Studio
|
- **LLM Providers**: OpenAI, Anthropic, Groq, Ollama, Gemini, DeepSeek, LM Studio
|
||||||
- **Embeddings**: Xenova Transformers, similarity search (cosine/dot product)
|
- **Embeddings**: Xenova Transformers, similarity search (cosine/dot product)
|
||||||
- **Agents**: `webSearchAgent`, `analyzerAgent`, `synthesizerAgent`, `taskManagerAgent`
|
- **Agents**: `webSearchAgent`, `analyzerAgent`, `synthesizerAgent`, `taskManagerAgent`
|
||||||
|
|
||||||
### External Services
|
### External Services
|
||||||
|
|
||||||
- **Search Engine**: SearXNG integration (`src/lib/searxng.ts`)
|
- **Search Engine**: SearXNG integration (`src/lib/searxng.ts`)
|
||||||
- **Configuration**: TOML-based config file
|
- **Configuration**: TOML-based config file
|
||||||
|
|
||||||
### Data Flow
|
### Data Flow
|
||||||
|
|
||||||
1. User query → Task Manager Agent
|
1. User query → Task Manager Agent
|
||||||
2. Web Search Agent → SearXNG → Content extraction
|
2. Web Search Agent → SearXNG → Content extraction
|
||||||
3. Analyzer Agent → Content processing + embedding
|
3. Analyzer Agent → Content processing + embedding
|
||||||
4. Synthesizer Agent → LLM response generation
|
4. Synthesizer Agent → LLM response generation
|
||||||
5. Response with cited sources
|
5. Response with cited sources
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
- `/src/app`: Next.js app directory with page components and API routes
|
- `/src/app`: Next.js app directory with page components and API routes
|
||||||
|
|
@ -117,22 +120,26 @@ When working on this codebase, you might need to:
|
||||||
## Code Style & Standards
|
## Code Style & Standards
|
||||||
|
|
||||||
### TypeScript Configuration
|
### TypeScript Configuration
|
||||||
|
|
||||||
- Strict mode enabled
|
- Strict mode enabled
|
||||||
- ES2017 target
|
- ES2017 target
|
||||||
- Path aliases: `@/*` → `src/*`
|
- Path aliases: `@/*` → `src/*`
|
||||||
- No test files (testing not implemented)
|
- No test files (testing not implemented)
|
||||||
|
|
||||||
### Formatting & Linting
|
### Formatting & Linting
|
||||||
|
|
||||||
- ESLint: Next.js core web vitals rules
|
- ESLint: Next.js core web vitals rules
|
||||||
- Prettier: Use `npm run format:write` before commits
|
- Prettier: Use `npm run format:write` before commits
|
||||||
- Import style: Use `@/` prefix for internal imports
|
- Import style: Use `@/` prefix for internal imports
|
||||||
|
|
||||||
### File Organization
|
### File Organization
|
||||||
|
|
||||||
- Components: React functional components with TypeScript
|
- Components: React functional components with TypeScript
|
||||||
- API routes: Next.js App Router (`src/app/api/`)
|
- API routes: Next.js App Router (`src/app/api/`)
|
||||||
- Utilities: Grouped by domain (`src/lib/`)
|
- Utilities: Grouped by domain (`src/lib/`)
|
||||||
- Naming: camelCase for functions/variables, PascalCase for components
|
- Naming: camelCase for functions/variables, PascalCase for components
|
||||||
|
|
||||||
### Error Handling
|
### Error Handling
|
||||||
|
|
||||||
- Use try/catch blocks for async operations
|
- Use try/catch blocks for async operations
|
||||||
- Return structured error responses from API routes
|
- Return structured error responses from API routes
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,6 @@ The API accepts a JSON object in the request body, where you define the focus mo
|
||||||
- **`optimizationMode`** (string, optional): Specifies the optimization mode to control the balance between performance and quality. Available modes:
|
- **`optimizationMode`** (string, optional): Specifies the optimization mode to control the balance between performance and quality. Available modes:
|
||||||
|
|
||||||
- `speed`: Prioritize speed and get the quickest possible answer. Minimum effort retrieving web content. - Only uses SearXNG result previews.
|
- `speed`: Prioritize speed and get the quickest possible answer. Minimum effort retrieving web content. - Only uses SearXNG result previews.
|
||||||
- `balanced`: Find the right balance between speed and accuracy. Medium effort retrieving web content. - Uses web scraping technologies to retrieve partial content from full web pages.
|
|
||||||
- `agent`: Use an agentic workflow to answer complex multi-part questions. This mode requires a model that is trained for tool use.
|
- `agent`: Use an agentic workflow to answer complex multi-part questions. This mode requires a model that is trained for tool use.
|
||||||
|
|
||||||
- **`query`** (string, required): The search query or question.
|
- **`query`** (string, required): The search query or question.
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,8 @@ Perplexica's architecture consists of the following key components:
|
||||||
- In Agent mode, the application uses an agentic workflow to answer complex multi-part questions
|
- In Agent mode, the application uses an agentic workflow to answer complex multi-part questions
|
||||||
- The agent can use reasoning steps to provide comprehensive answers to complex questions
|
- The agent can use reasoning steps to provide comprehensive answers to complex questions
|
||||||
- Agent mode is experimental and may consume lots of tokens and take a long time to produce responses
|
- Agent mode is experimental and may consume lots of tokens and take a long time to produce responses
|
||||||
- In Balanced mode, the application retrieves web content using Playwright and Mozilla Readability to extract relevant segments of web content
|
|
||||||
- Because it only uses segments of web content, it can be less accurate than Agent mode
|
|
||||||
- In Speed mode, the application only uses the preview content returned by SearXNG
|
- In Speed mode, the application only uses the preview content returned by SearXNG
|
||||||
- This content is provided by the search engines and contains minimal context from the actual web page
|
- This content is provided by the search engines and contains minimal context from the actual web page
|
||||||
- This mode is the least accurate and is often prone to hallucination
|
- This mode prioritizes quick responses over accuracy
|
||||||
|
|
||||||
For a more detailed explanation of how these components work together, see [WORKING.md](https://github.com/ItzCrazyKns/Perplexica/tree/master/docs/architecture/WORKING.md).
|
For a more detailed explanation of how these components work together, see [WORKING.md](https://github.com/ItzCrazyKns/Perplexica/tree/master/docs/architecture/WORKING.md).
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ type EmbeddingModel = {
|
||||||
|
|
||||||
type Body = {
|
type Body = {
|
||||||
message: Message;
|
message: Message;
|
||||||
optimizationMode: 'speed' | 'balanced' | 'agent';
|
optimizationMode: 'speed' | 'agent';
|
||||||
focusMode: string;
|
focusMode: string;
|
||||||
history: Array<[string, string]>;
|
history: Array<[string, string]>;
|
||||||
files: Array<string>;
|
files: Array<string>;
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ interface embeddingModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ChatRequestBody {
|
interface ChatRequestBody {
|
||||||
optimizationMode: 'speed' | 'balanced' | 'agent';
|
optimizationMode: 'speed' | 'agent';
|
||||||
focusMode: string;
|
focusMode: string;
|
||||||
chatModel?: chatModel;
|
chatModel?: chatModel;
|
||||||
embeddingModel?: embeddingModel;
|
embeddingModel?: embeddingModel;
|
||||||
|
|
@ -52,7 +52,7 @@ export const POST = async (req: Request) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
body.history = body.history || [];
|
body.history = body.history || [];
|
||||||
body.optimizationMode = body.optimizationMode || 'balanced';
|
body.optimizationMode = body.optimizationMode || 'speed';
|
||||||
body.stream = body.stream || false;
|
body.stream = body.stream || false;
|
||||||
|
|
||||||
const history: BaseMessage[] = body.history.map((msg) => {
|
const history: BaseMessage[] = body.history.map((msg) => {
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,7 @@ const MessageInput = ({
|
||||||
setFileIds={setFileIds}
|
setFileIds={setFileIds}
|
||||||
files={files}
|
files={files}
|
||||||
setFiles={setFiles}
|
setFiles={setFiles}
|
||||||
|
optimizationMode={optimizationMode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row items-center space-x-2">
|
<div className="flex flex-row items-center space-x-2">
|
||||||
|
|
|
||||||
|
|
@ -14,16 +14,23 @@ const Attach = ({
|
||||||
setFileIds,
|
setFileIds,
|
||||||
files,
|
files,
|
||||||
setFiles,
|
setFiles,
|
||||||
|
optimizationMode,
|
||||||
}: {
|
}: {
|
||||||
fileIds: string[];
|
fileIds: string[];
|
||||||
setFileIds: (fileIds: string[]) => void;
|
setFileIds: (fileIds: string[]) => void;
|
||||||
files: FileType[];
|
files: FileType[];
|
||||||
setFiles: (files: FileType[]) => void;
|
setFiles: (files: FileType[]) => void;
|
||||||
|
optimizationMode: string;
|
||||||
}) => {
|
}) => {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const fileInputRef = useRef<any>();
|
const fileInputRef = useRef<any>();
|
||||||
|
|
||||||
|
const isSpeedMode = optimizationMode === 'speed';
|
||||||
|
const isDisabled = isSpeedMode;
|
||||||
|
|
||||||
const handleChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (isDisabled) return;
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const data = new FormData();
|
const data = new FormData();
|
||||||
|
|
||||||
|
|
@ -37,7 +44,8 @@ const Attach = ({
|
||||||
const embeddingModel = localStorage.getItem('embeddingModel');
|
const embeddingModel = localStorage.getItem('embeddingModel');
|
||||||
const chatModelProvider = localStorage.getItem('chatModelProvider');
|
const chatModelProvider = localStorage.getItem('chatModelProvider');
|
||||||
const chatModel = localStorage.getItem('chatModel');
|
const chatModel = localStorage.getItem('chatModel');
|
||||||
const ollamaContextWindow = localStorage.getItem('ollamaContextWindow') || '2048';
|
const ollamaContextWindow =
|
||||||
|
localStorage.getItem('ollamaContextWindow') || '2048';
|
||||||
|
|
||||||
data.append('embedding_model_provider', embeddingModelProvider!);
|
data.append('embedding_model_provider', embeddingModelProvider!);
|
||||||
data.append('embedding_model', embeddingModel!);
|
data.append('embedding_model', embeddingModel!);
|
||||||
|
|
@ -67,18 +75,23 @@ const Attach = ({
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : files.length > 0 ? (
|
) : files.length > 0 ? (
|
||||||
|
<div className="relative group">
|
||||||
<Popover className="relative w-full max-w-[15rem] md:max-w-md lg:max-w-lg">
|
<Popover className="relative w-full max-w-[15rem] md:max-w-md lg:max-w-lg">
|
||||||
<PopoverButton
|
<PopoverButton
|
||||||
type="button"
|
type="button"
|
||||||
|
disabled={isDisabled}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex flex-row items-center justify-between space-x-1 p-2 text-black/50 dark:text-white/50 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary active:scale-95 transition duration-200 hover:text-black dark:hover:text-white',
|
'flex flex-row items-center justify-between space-x-1 p-2 rounded-xl transition duration-200',
|
||||||
files.length > 0 ? '-ml-2 lg:-ml-3' : '',
|
files.length > 0 ? '-ml-2 lg:-ml-3' : '',
|
||||||
|
isDisabled
|
||||||
|
? 'text-black/20 dark:text-white/20 cursor-not-allowed'
|
||||||
|
: 'text-black/50 dark:text-white/50 hover:bg-light-secondary dark:hover:bg-dark-secondary hover:text-black dark:hover:text-white',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{files.length > 1 && (
|
{files.length > 1 && (
|
||||||
<>
|
<>
|
||||||
<File size={19} className="text-sky-400" />
|
<File size={19} className={isDisabled ? 'text-sky-900' : 'text-sky-400'} />
|
||||||
<p className="text-sky-400 inline whitespace-nowrap text-xs font-medium">
|
<p className={cn("inline whitespace-nowrap text-xs font-medium", isDisabled ? 'text-sky-900' : 'text-sky-400')}>
|
||||||
{files.length} files
|
{files.length} files
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
|
|
@ -86,8 +99,8 @@ const Attach = ({
|
||||||
|
|
||||||
{files.length === 1 && (
|
{files.length === 1 && (
|
||||||
<>
|
<>
|
||||||
<File size={18} className="text-sky-400" />
|
<File size={18} className={isDisabled ? 'text-sky-900' : 'text-sky-400'} />
|
||||||
<p className="text-sky-400 text-xs font-medium">
|
<p className={cn("text-xs font-medium", isDisabled ? 'text-sky-900' : 'text-sky-400')}>
|
||||||
{files[0].fileName.length > 10
|
{files[0].fileName.length > 10
|
||||||
? files[0].fileName.replace(/\.\w+$/, '').substring(0, 3) +
|
? files[0].fileName.replace(/\.\w+$/, '').substring(0, 3) +
|
||||||
'...' +
|
'...' +
|
||||||
|
|
@ -115,8 +128,14 @@ const Attach = ({
|
||||||
<div className="flex flex-row items-center space-x-4">
|
<div className="flex flex-row items-center space-x-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => fileInputRef.current.click()}
|
onClick={() => !isDisabled && fileInputRef.current.click()}
|
||||||
className="flex flex-row items-center space-x-1 text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white transition duration-200"
|
disabled={isDisabled}
|
||||||
|
className={cn(
|
||||||
|
'flex flex-row items-center space-x-1 transition duration-200',
|
||||||
|
isDisabled
|
||||||
|
? 'text-black/20 dark:text-white/20 cursor-not-allowed'
|
||||||
|
: 'text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white',
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
|
|
@ -125,16 +144,25 @@ const Attach = ({
|
||||||
accept=".pdf,.docx,.txt"
|
accept=".pdf,.docx,.txt"
|
||||||
multiple
|
multiple
|
||||||
hidden
|
hidden
|
||||||
|
disabled={isDisabled}
|
||||||
/>
|
/>
|
||||||
<Plus size={18} />
|
<Plus size={18} />
|
||||||
<p className="text-xs">Add</p>
|
<p className="text-xs">Add</p>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
if (!isDisabled) {
|
||||||
setFiles([]);
|
setFiles([]);
|
||||||
setFileIds([]);
|
setFileIds([]);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
className="flex flex-row items-center space-x-1 text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white transition duration-200"
|
disabled={isDisabled}
|
||||||
|
className={cn(
|
||||||
|
'flex flex-row items-center space-x-1 transition duration-200',
|
||||||
|
isDisabled
|
||||||
|
? 'text-black/20 dark:text-white/20 cursor-not-allowed'
|
||||||
|
: 'text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white',
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<Trash size={14} />
|
<Trash size={14} />
|
||||||
<p className="text-xs">Clear</p>
|
<p className="text-xs">Clear</p>
|
||||||
|
|
@ -165,12 +193,26 @@ const Attach = ({
|
||||||
</PopoverPanel>
|
</PopoverPanel>
|
||||||
</Transition>
|
</Transition>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
{isSpeedMode && (
|
||||||
|
<div className="absolute bottom-full mb-2 left-1/2 transform -translate-x-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none">
|
||||||
|
<div className="bg-black dark:bg-white text-white dark:text-black text-xs px-2 py-1 rounded whitespace-nowrap">
|
||||||
|
File attachments are disabled in Speed mode
|
||||||
|
<div className="absolute top-full left-1/2 transform -translate-x-1/2 border-4 border-transparent border-t-black dark:border-t-white"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
<div className="relative group">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => fileInputRef.current.click()}
|
onClick={() => !isDisabled && fileInputRef.current.click()}
|
||||||
|
disabled={isDisabled}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex flex-row items-center space-x-1 text-black/50 dark:text-white/50 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary transition duration-200 hover:text-black dark:hover:text-white p-2',
|
'flex flex-row items-center space-x-1 rounded-xl transition duration-200 p-2',
|
||||||
|
isDisabled
|
||||||
|
? 'text-black/20 dark:text-white/20 cursor-not-allowed'
|
||||||
|
: 'text-black/50 dark:text-white/50 hover:bg-light-secondary dark:hover:bg-dark-secondary hover:text-black dark:hover:text-white',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
|
|
@ -180,9 +222,19 @@ const Attach = ({
|
||||||
accept=".pdf,.docx,.txt"
|
accept=".pdf,.docx,.txt"
|
||||||
multiple
|
multiple
|
||||||
hidden
|
hidden
|
||||||
|
disabled={isDisabled}
|
||||||
/>
|
/>
|
||||||
<Paperclip size="18" />
|
<Paperclip size="18" />
|
||||||
</button>
|
</button>
|
||||||
|
{isSpeedMode && (
|
||||||
|
<div className="absolute bottom-full mb-2 left-1/2 transform -translate-x-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none">
|
||||||
|
<div className="bg-black dark:bg-white text-white dark:text-black text-xs px-2 py-1 rounded whitespace-nowrap">
|
||||||
|
File attachments are disabled in Speed mode
|
||||||
|
<div className="absolute top-full left-1/2 transform -translate-x-1/2 border-4 border-transparent border-t-black dark:border-t-white"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,161 +0,0 @@
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
import {
|
|
||||||
Popover,
|
|
||||||
PopoverButton,
|
|
||||||
PopoverPanel,
|
|
||||||
Transition,
|
|
||||||
} from '@headlessui/react';
|
|
||||||
import { CopyPlus, File, LoaderCircle, Plus, Trash } from 'lucide-react';
|
|
||||||
import { Fragment, useRef, useState } from 'react';
|
|
||||||
import { File as FileType } from '../ChatWindow';
|
|
||||||
|
|
||||||
const AttachSmall = ({
|
|
||||||
fileIds,
|
|
||||||
setFileIds,
|
|
||||||
files,
|
|
||||||
setFiles,
|
|
||||||
}: {
|
|
||||||
fileIds: string[];
|
|
||||||
setFileIds: (fileIds: string[]) => void;
|
|
||||||
files: FileType[];
|
|
||||||
setFiles: (files: FileType[]) => void;
|
|
||||||
}) => {
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const fileInputRef = useRef<any>();
|
|
||||||
|
|
||||||
const handleChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setLoading(true);
|
|
||||||
const data = new FormData();
|
|
||||||
|
|
||||||
for (let i = 0; i < e.target.files!.length; i++) {
|
|
||||||
data.append('files', e.target.files![i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const embeddingModelProvider = localStorage.getItem(
|
|
||||||
'embeddingModelProvider',
|
|
||||||
);
|
|
||||||
const embeddingModel = localStorage.getItem('embeddingModel');
|
|
||||||
const chatModelProvider = localStorage.getItem('chatModelProvider');
|
|
||||||
const chatModel = localStorage.getItem('chatModel');
|
|
||||||
const ollamaContextWindow = localStorage.getItem('ollamaContextWindow') || '2048';
|
|
||||||
|
|
||||||
data.append('embedding_model_provider', embeddingModelProvider!);
|
|
||||||
data.append('embedding_model', embeddingModel!);
|
|
||||||
data.append('chat_model_provider', chatModelProvider!);
|
|
||||||
data.append('chat_model', chatModel!);
|
|
||||||
if (chatModelProvider === 'ollama') {
|
|
||||||
data.append('ollama_context_window', ollamaContextWindow);
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await fetch(`/api/uploads`, {
|
|
||||||
method: 'POST',
|
|
||||||
body: data,
|
|
||||||
});
|
|
||||||
|
|
||||||
const resData = await res.json();
|
|
||||||
|
|
||||||
setFiles([...files, ...resData.files]);
|
|
||||||
setFileIds([...fileIds, ...resData.files.map((file: any) => file.fileId)]);
|
|
||||||
setLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return loading ? (
|
|
||||||
<div className="flex flex-row items-center justify-between space-x-1 p-1">
|
|
||||||
<LoaderCircle size={20} className="text-sky-400 animate-spin" />
|
|
||||||
</div>
|
|
||||||
) : files.length > 0 ? (
|
|
||||||
<Popover className="max-w-[15rem] md:max-w-md lg:max-w-lg">
|
|
||||||
<PopoverButton
|
|
||||||
type="button"
|
|
||||||
className="flex flex-row items-center justify-between space-x-1 p-1 text-black/50 dark:text-white/50 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary active:scale-95 transition duration-200 hover:text-black dark:hover:text-white"
|
|
||||||
>
|
|
||||||
<File size={20} className="text-sky-400" />
|
|
||||||
</PopoverButton>
|
|
||||||
<Transition
|
|
||||||
as={Fragment}
|
|
||||||
enter="transition ease-out duration-150"
|
|
||||||
enterFrom="opacity-0 translate-y-1"
|
|
||||||
enterTo="opacity-100 translate-y-0"
|
|
||||||
leave="transition ease-in duration-150"
|
|
||||||
leaveFrom="opacity-100 translate-y-0"
|
|
||||||
leaveTo="opacity-0 translate-y-1"
|
|
||||||
>
|
|
||||||
<PopoverPanel className="absolute z-10 w-64 md:w-[350px] bottom-14 -ml-3">
|
|
||||||
<div className="bg-light-primary dark:bg-dark-primary border rounded-md border-light-200 dark:border-dark-200 w-full max-h-[200px] md:max-h-none overflow-y-auto flex flex-col">
|
|
||||||
<div className="flex flex-row items-center justify-between px-3 py-2">
|
|
||||||
<h4 className="text-black dark:text-white font-medium text-sm">
|
|
||||||
Attached files
|
|
||||||
</h4>
|
|
||||||
<div className="flex flex-row items-center space-x-4">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => fileInputRef.current.click()}
|
|
||||||
className="flex flex-row items-center space-x-1 text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white transition duration-200"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
onChange={handleChange}
|
|
||||||
ref={fileInputRef}
|
|
||||||
accept=".pdf,.docx,.txt"
|
|
||||||
multiple
|
|
||||||
hidden
|
|
||||||
/>
|
|
||||||
<Plus size={18} />
|
|
||||||
<p className="text-xs">Add</p>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setFiles([]);
|
|
||||||
setFileIds([]);
|
|
||||||
}}
|
|
||||||
className="flex flex-row items-center space-x-1 text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white transition duration-200"
|
|
||||||
>
|
|
||||||
<Trash size={14} />
|
|
||||||
<p className="text-xs">Clear</p>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="h-[0.5px] mx-2 bg-white/10" />
|
|
||||||
<div className="flex flex-col items-center">
|
|
||||||
{files.map((file, i) => (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className="flex flex-row items-center justify-start w-full space-x-3 p-3"
|
|
||||||
>
|
|
||||||
<div className="bg-dark-100 flex items-center justify-center w-10 h-10 rounded-md">
|
|
||||||
<File size={16} className="text-white/70" />
|
|
||||||
</div>
|
|
||||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
|
||||||
{file.fileName.length > 25
|
|
||||||
? file.fileName.replace(/\.\w+$/, '').substring(0, 25) +
|
|
||||||
'...' +
|
|
||||||
file.fileExtension
|
|
||||||
: file.fileName}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</PopoverPanel>
|
|
||||||
</Transition>
|
|
||||||
</Popover>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => fileInputRef.current.click()}
|
|
||||||
className="flex flex-row items-center space-x-1 text-black/50 dark:text-white/50 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary transition duration-200 hover:text-black dark:hover:text-white p-1"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
onChange={handleChange}
|
|
||||||
ref={fileInputRef}
|
|
||||||
accept=".pdf,.docx,.txt"
|
|
||||||
multiple
|
|
||||||
hidden
|
|
||||||
/>
|
|
||||||
<CopyPlus size={20} />
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AttachSmall;
|
|
||||||
|
|
@ -12,7 +12,7 @@ const OptimizationModes = [
|
||||||
key: 'speed',
|
key: 'speed',
|
||||||
title: 'Speed',
|
title: 'Speed',
|
||||||
description:
|
description:
|
||||||
'Prioritize speed and get the quickest possible answer. Minimum effort retrieving web content.',
|
'Prioritize speed and get the quickest possible answer. Uses only web search results - attached files will not be processed.',
|
||||||
icon: <Zap size={20} className="text-[#FF9800]" />,
|
icon: <Zap size={20} className="text-[#FF9800]" />,
|
||||||
},
|
},
|
||||||
// {
|
// {
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,7 @@ const RouterDecisionSchema = z.object({
|
||||||
decision: z
|
decision: z
|
||||||
.enum(['file_search', 'web_search', 'analyzer'])
|
.enum(['file_search', 'web_search', 'analyzer'])
|
||||||
.describe('The next step to take in the workflow'),
|
.describe('The next step to take in the workflow'),
|
||||||
reasoning: z
|
reasoning: z.string().describe('Explanation of why this decision was made'),
|
||||||
.string()
|
|
||||||
.describe('Explanation of why this decision was made'),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
type RouterDecision = z.infer<typeof RouterDecisionSchema>;
|
type RouterDecision = z.infer<typeof RouterDecisionSchema>;
|
||||||
|
|
@ -63,7 +61,9 @@ export class ContentRouterAgent {
|
||||||
const searchHistory = state.searchInstructionHistory.join(', ') || 'None';
|
const searchHistory = state.searchInstructionHistory.join(', ') || 'None';
|
||||||
|
|
||||||
// Extract file topics if files are available
|
// Extract file topics if files are available
|
||||||
const fileTopics = hasFiles ? await this.extractFileTopics(state.fileIds!) : 'None';
|
const fileTopics = hasFiles
|
||||||
|
? await this.extractFileTopics(state.fileIds!)
|
||||||
|
: 'None';
|
||||||
|
|
||||||
// Emit routing decision event
|
// Emit routing decision event
|
||||||
this.emitter.emit('agent_action', {
|
this.emitter.emit('agent_action', {
|
||||||
|
|
@ -97,9 +97,12 @@ export class ContentRouterAgent {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Use structured output for routing decision
|
// Use structured output for routing decision
|
||||||
const structuredLlm = this.llm.withStructuredOutput(RouterDecisionSchema, {
|
const structuredLlm = this.llm.withStructuredOutput(
|
||||||
|
RouterDecisionSchema,
|
||||||
|
{
|
||||||
name: 'route_content',
|
name: 'route_content',
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const routerDecision = await structuredLlm.invoke(
|
const routerDecision = await structuredLlm.invoke(
|
||||||
[...removeThinkingBlocksFromMessages(state.messages), prompt],
|
[...removeThinkingBlocksFromMessages(state.messages), prompt],
|
||||||
|
|
@ -112,7 +115,11 @@ export class ContentRouterAgent {
|
||||||
console.log(`Focus mode: ${focusMode}`);
|
console.log(`Focus mode: ${focusMode}`);
|
||||||
|
|
||||||
// Validate decision based on focus mode restrictions
|
// Validate decision based on focus mode restrictions
|
||||||
const validatedDecision = this.validateDecision(routerDecision, focusMode, hasFiles);
|
const validatedDecision = this.validateDecision(
|
||||||
|
routerDecision,
|
||||||
|
focusMode,
|
||||||
|
hasFiles,
|
||||||
|
);
|
||||||
|
|
||||||
// Emit routing result event
|
// Emit routing result event
|
||||||
this.emitter.emit('agent_action', {
|
this.emitter.emit('agent_action', {
|
||||||
|
|
@ -163,7 +170,7 @@ export class ContentRouterAgent {
|
||||||
*/
|
*/
|
||||||
private async extractFileTopics(fileIds: string[]): Promise<string> {
|
private async extractFileTopics(fileIds: string[]): Promise<string> {
|
||||||
try {
|
try {
|
||||||
const topics = fileIds.map(fileId => {
|
const topics = fileIds.map((fileId) => {
|
||||||
try {
|
try {
|
||||||
const filePath = path.join(process.cwd(), 'uploads', fileId);
|
const filePath = path.join(process.cwd(), 'uploads', fileId);
|
||||||
const contentPath = filePath + '-extracted.json';
|
const contentPath = filePath + '-extracted.json';
|
||||||
|
|
@ -199,14 +206,15 @@ export class ContentRouterAgent {
|
||||||
hasFiles: boolean,
|
hasFiles: boolean,
|
||||||
): RouterDecision {
|
): RouterDecision {
|
||||||
// Enforce focus mode restrictions for chat and localResearch modes
|
// Enforce focus mode restrictions for chat and localResearch modes
|
||||||
if ((focusMode === 'chat' || focusMode === 'localResearch') &&
|
if (
|
||||||
decision.decision === 'web_search') {
|
(focusMode === 'chat' || focusMode === 'localResearch') &&
|
||||||
|
decision.decision === 'web_search'
|
||||||
|
) {
|
||||||
// Override to file_search if files are available, otherwise analyzer
|
// Override to file_search if files are available, otherwise analyzer
|
||||||
const fallbackDecision = hasFiles ? 'file_search' : 'analyzer';
|
const fallbackDecision = hasFiles ? 'file_search' : 'analyzer';
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`Overriding web_search decision to ${fallbackDecision} due to focus mode restriction: ${focusMode}`
|
`Overriding web_search decision to ${fallbackDecision} due to focus mode restriction: ${focusMode}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,10 @@ import { EventEmitter } from 'events';
|
||||||
import { Document } from 'langchain/document';
|
import { Document } from 'langchain/document';
|
||||||
import { AgentState } from './agentState';
|
import { AgentState } from './agentState';
|
||||||
import { Embeddings } from '@langchain/core/embeddings';
|
import { Embeddings } from '@langchain/core/embeddings';
|
||||||
import { processFilesToDocuments, getRankedDocs } from '../utils/fileProcessing';
|
import {
|
||||||
|
processFilesToDocuments,
|
||||||
|
getRankedDocs,
|
||||||
|
} from '../utils/fileProcessing';
|
||||||
|
|
||||||
export class FileSearchAgent {
|
export class FileSearchAgent {
|
||||||
private llm: BaseChatModel;
|
private llm: BaseChatModel;
|
||||||
|
|
@ -79,12 +82,16 @@ export class FileSearchAgent {
|
||||||
return new Command({
|
return new Command({
|
||||||
goto: 'analyzer',
|
goto: 'analyzer',
|
||||||
update: {
|
update: {
|
||||||
messages: [new AIMessage('No searchable content found in attached files.')],
|
messages: [
|
||||||
|
new AIMessage('No searchable content found in attached files.'),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Processed ${fileDocuments.length} file documents for search`);
|
console.log(
|
||||||
|
`Processed ${fileDocuments.length} file documents for search`,
|
||||||
|
);
|
||||||
|
|
||||||
// Emit searching file content event
|
// Emit searching file content event
|
||||||
this.emitter.emit('agent_action', {
|
this.emitter.emit('agent_action', {
|
||||||
|
|
@ -139,7 +146,11 @@ export class FileSearchAgent {
|
||||||
return new Command({
|
return new Command({
|
||||||
goto: 'analyzer',
|
goto: 'analyzer',
|
||||||
update: {
|
update: {
|
||||||
messages: [new AIMessage('No relevant content found in attached files for the current task.')],
|
messages: [
|
||||||
|
new AIMessage(
|
||||||
|
'No relevant content found in attached files for the current task.',
|
||||||
|
),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -157,7 +168,8 @@ export class FileSearchAgent {
|
||||||
totalTasks: state.tasks?.length || 1,
|
totalTasks: state.tasks?.length || 1,
|
||||||
relevantSections: rankedDocuments.length,
|
relevantSections: rankedDocuments.length,
|
||||||
searchedDocuments: fileDocuments.length,
|
searchedDocuments: fileDocuments.length,
|
||||||
documentCount: state.relevantDocuments.length + rankedDocuments.length,
|
documentCount:
|
||||||
|
state.relevantDocuments.length + rankedDocuments.length,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -34,13 +34,13 @@ export class SynthesizerAgent {
|
||||||
// Format the prompt using the external template
|
// Format the prompt using the external template
|
||||||
const template = PromptTemplate.fromTemplate(synthesizerPrompt);
|
const template = PromptTemplate.fromTemplate(synthesizerPrompt);
|
||||||
|
|
||||||
const conversationHistory = removeThinkingBlocksFromMessages(state.messages)
|
const conversationHistory =
|
||||||
|
removeThinkingBlocksFromMessages(state.messages)
|
||||||
.map((msg) => `<${msg.getType()}>${msg.content}</${msg.getType()}>`)
|
.map((msg) => `<${msg.getType()}>${msg.content}</${msg.getType()}>`)
|
||||||
.join('\n') || 'No previous conversation context';
|
.join('\n') || 'No previous conversation context';
|
||||||
|
|
||||||
const relevantDocuments = state.relevantDocuments
|
const relevantDocuments = state.relevantDocuments
|
||||||
.map(
|
.map((doc, index) => {
|
||||||
(doc, index) => {
|
|
||||||
const isFile = doc.metadata?.url?.toLowerCase().includes('file');
|
const isFile = doc.metadata?.url?.toLowerCase().includes('file');
|
||||||
return `<${index + 1}>\n
|
return `<${index + 1}>\n
|
||||||
<title>${doc.metadata.title}</title>\n
|
<title>${doc.metadata.title}</title>\n
|
||||||
|
|
@ -48,8 +48,7 @@ export class SynthesizerAgent {
|
||||||
${isFile ? '' : '\n<url>' + doc.metadata.url + '</url>\n'}
|
${isFile ? '' : '\n<url>' + doc.metadata.url + '</url>\n'}
|
||||||
<content>\n${doc.pageContent}\n</content>\n
|
<content>\n${doc.pageContent}\n</content>\n
|
||||||
</${index + 1}>`;
|
</${index + 1}>`;
|
||||||
}
|
})
|
||||||
)
|
|
||||||
.join('\n');
|
.join('\n');
|
||||||
|
|
||||||
const formattedPrompt = await template.format({
|
const formattedPrompt = await template.format({
|
||||||
|
|
|
||||||
|
|
@ -129,7 +129,8 @@ export class TaskManagerAgent {
|
||||||
const template = PromptTemplate.fromTemplate(taskBreakdownPrompt);
|
const template = PromptTemplate.fromTemplate(taskBreakdownPrompt);
|
||||||
|
|
||||||
// Create file context information
|
// Create file context information
|
||||||
const fileContext = state.fileIds && state.fileIds.length > 0
|
const fileContext =
|
||||||
|
state.fileIds && state.fileIds.length > 0
|
||||||
? `Files attached: ${state.fileIds.length} file(s) are available for analysis. Consider creating tasks that can leverage these attached files when appropriate.`
|
? `Files attached: ${state.fileIds.length} file(s) are available for analysis. Consider creating tasks that can leverage these attached files when appropriate.`
|
||||||
: 'No files attached: Focus on tasks that can be answered through web research or general knowledge.';
|
: 'No files attached: Focus on tasks that can be answered through web research or general knowledge.';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -155,7 +155,7 @@ export class AgentSearch {
|
||||||
async searchAndAnswer(
|
async searchAndAnswer(
|
||||||
query: string,
|
query: string,
|
||||||
history: BaseMessage[] = [],
|
history: BaseMessage[] = [],
|
||||||
fileIds: string[] = []
|
fileIds: string[] = [],
|
||||||
) {
|
) {
|
||||||
const workflow = this.createWorkflow();
|
const workflow = this.createWorkflow();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
import MetaSearchAgent from '@/lib/search/metaSearchAgent';
|
import MetaSearchAgent from '@/lib/search/metaSearchAgent';
|
||||||
|
import SpeedSearchAgent from '@/lib/search/speedSearch';
|
||||||
import prompts from '../prompts';
|
import prompts from '../prompts';
|
||||||
|
|
||||||
|
export { default as SpeedSearchAgent } from './speedSearch';
|
||||||
|
|
||||||
export const searchHandlers: Record<string, MetaSearchAgent> = {
|
export const searchHandlers: Record<string, MetaSearchAgent> = {
|
||||||
webSearch: new MetaSearchAgent({
|
webSearch: new MetaSearchAgent({
|
||||||
activeEngines: [],
|
activeEngines: [],
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,9 @@
|
||||||
import type { Embeddings } from '@langchain/core/embeddings';
|
import type { Embeddings } from '@langchain/core/embeddings';
|
||||||
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||||
import { BaseMessage } from '@langchain/core/messages';
|
import { BaseMessage } from '@langchain/core/messages';
|
||||||
import { StringOutputParser } from '@langchain/core/output_parsers';
|
|
||||||
import {
|
|
||||||
ChatPromptTemplate,
|
|
||||||
MessagesPlaceholder,
|
|
||||||
PromptTemplate,
|
|
||||||
} from '@langchain/core/prompts';
|
|
||||||
import {
|
|
||||||
RunnableLambda,
|
|
||||||
RunnableMap,
|
|
||||||
RunnableSequence,
|
|
||||||
} from '@langchain/core/runnables';
|
|
||||||
import { StreamEvent } from '@langchain/core/tracers/log_stream';
|
|
||||||
import { ChatOpenAI } from '@langchain/openai';
|
|
||||||
import eventEmitter from 'events';
|
import eventEmitter from 'events';
|
||||||
import { Document } from 'langchain/document';
|
|
||||||
import fs from 'node:fs';
|
|
||||||
import path from 'node:path';
|
|
||||||
import LineOutputParser from '../outputParsers/lineOutputParser';
|
|
||||||
import LineListOutputParser from '../outputParsers/listLineOutputParser';
|
|
||||||
import { searchSearxng } from '../searxng';
|
|
||||||
import { formatDateForLLM } from '../utils';
|
|
||||||
import computeSimilarity from '../utils/computeSimilarity';
|
|
||||||
import { getDocumentsFromLinks, getWebContent } from '../utils/documents';
|
|
||||||
import formatChatHistoryAsString from '../utils/formatHistory';
|
|
||||||
import { getModelName } from '../utils/modelUtils';
|
|
||||||
import { AgentSearch } from './agentSearch';
|
import { AgentSearch } from './agentSearch';
|
||||||
|
import SpeedSearchAgent from './speedSearch';
|
||||||
|
|
||||||
export interface MetaSearchAgentType {
|
export interface MetaSearchAgentType {
|
||||||
searchAndAnswer: (
|
searchAndAnswer: (
|
||||||
|
|
@ -34,7 +11,7 @@ export interface MetaSearchAgentType {
|
||||||
history: BaseMessage[],
|
history: BaseMessage[],
|
||||||
llm: BaseChatModel,
|
llm: BaseChatModel,
|
||||||
embeddings: Embeddings,
|
embeddings: Embeddings,
|
||||||
optimizationMode: 'speed' | 'balanced' | 'agent',
|
optimizationMode: 'speed' | 'agent',
|
||||||
fileIds: string[],
|
fileIds: string[],
|
||||||
systemInstructions: string,
|
systemInstructions: string,
|
||||||
signal: AbortSignal,
|
signal: AbortSignal,
|
||||||
|
|
@ -54,623 +31,13 @@ interface Config {
|
||||||
additionalSearchCriteria?: string;
|
additionalSearchCriteria?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type BasicChainInput = {
|
|
||||||
chat_history: BaseMessage[];
|
|
||||||
query: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
class MetaSearchAgent implements MetaSearchAgentType {
|
class MetaSearchAgent implements MetaSearchAgentType {
|
||||||
private config: Config;
|
private config: Config;
|
||||||
private strParser = new StringOutputParser();
|
|
||||||
private searchQuery?: string;
|
|
||||||
private searxngUrl?: string;
|
|
||||||
|
|
||||||
constructor(config: Config) {
|
constructor(config: Config) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Emit a progress event with the given percentage and message
|
|
||||||
*/
|
|
||||||
private emitProgress(
|
|
||||||
emitter: eventEmitter,
|
|
||||||
percentage: number,
|
|
||||||
message: string,
|
|
||||||
subMessage?: string,
|
|
||||||
) {
|
|
||||||
const progressData: any = {
|
|
||||||
message,
|
|
||||||
current: percentage,
|
|
||||||
total: 100,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add subMessage if provided
|
|
||||||
if (subMessage) {
|
|
||||||
progressData.subMessage = subMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
emitter.emit(
|
|
||||||
'progress',
|
|
||||||
JSON.stringify({
|
|
||||||
type: 'progress',
|
|
||||||
data: progressData,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async createSearchRetrieverChain(
|
|
||||||
llm: BaseChatModel,
|
|
||||||
systemInstructions: string,
|
|
||||||
emitter: eventEmitter,
|
|
||||||
signal: AbortSignal,
|
|
||||||
) {
|
|
||||||
// TODO: Don't we want to set this back to default once search is done?
|
|
||||||
(llm as unknown as ChatOpenAI).temperature = 0;
|
|
||||||
|
|
||||||
this.emitProgress(emitter, 10, `Building search query`);
|
|
||||||
|
|
||||||
return RunnableSequence.from([
|
|
||||||
PromptTemplate.fromTemplate(this.config.queryGeneratorPrompt),
|
|
||||||
llm,
|
|
||||||
this.strParser,
|
|
||||||
RunnableLambda.from(async (input: string) => {
|
|
||||||
try {
|
|
||||||
//console.log(`LLM response for initial web search:"${input}"`);
|
|
||||||
const linksOutputParser = new LineListOutputParser({
|
|
||||||
key: 'links',
|
|
||||||
});
|
|
||||||
|
|
||||||
const questionOutputParser = new LineOutputParser({
|
|
||||||
key: 'answer',
|
|
||||||
});
|
|
||||||
|
|
||||||
const links = await linksOutputParser.parse(input);
|
|
||||||
let question = await questionOutputParser.parse(input);
|
|
||||||
|
|
||||||
//console.log('question', question);
|
|
||||||
|
|
||||||
if (question === 'not_needed') {
|
|
||||||
return { query: '', docs: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (links.length > 0) {
|
|
||||||
if (question.length === 0) {
|
|
||||||
question = 'summarize';
|
|
||||||
}
|
|
||||||
|
|
||||||
let docs: Document[] = [];
|
|
||||||
|
|
||||||
const linkDocs = await getDocumentsFromLinks({ links });
|
|
||||||
|
|
||||||
const docGroups: Document[] = [];
|
|
||||||
|
|
||||||
linkDocs.map((doc) => {
|
|
||||||
const URLDocExists = docGroups.find(
|
|
||||||
(d) =>
|
|
||||||
d.metadata.url === doc.metadata.url &&
|
|
||||||
d.metadata.totalDocs < 10,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!URLDocExists) {
|
|
||||||
docGroups.push({
|
|
||||||
...doc,
|
|
||||||
metadata: {
|
|
||||||
...doc.metadata,
|
|
||||||
totalDocs: 1,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const docIndex = docGroups.findIndex(
|
|
||||||
(d) =>
|
|
||||||
d.metadata.url === doc.metadata.url &&
|
|
||||||
d.metadata.totalDocs < 10,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (docIndex !== -1) {
|
|
||||||
docGroups[docIndex].pageContent =
|
|
||||||
docGroups[docIndex].pageContent + `\n\n` + doc.pageContent;
|
|
||||||
docGroups[docIndex].metadata.totalDocs += 1;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.emitProgress(emitter, 20, `Summarizing content`);
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
docGroups.map(async (doc) => {
|
|
||||||
const systemPrompt = systemInstructions
|
|
||||||
? `${systemInstructions}\n\n`
|
|
||||||
: '';
|
|
||||||
|
|
||||||
const res = await llm.invoke(
|
|
||||||
`${systemPrompt}You are a web search summarizer, tasked with summarizing a piece of text retrieved from a web search. Your job is to summarize the
|
|
||||||
text into a detailed, 2-4 paragraph explanation that captures the main ideas and provides a comprehensive answer to the query.
|
|
||||||
If the query is \"summarize\", you should provide a detailed summary of the text. If the query is a specific question, you should answer it in the summary.
|
|
||||||
|
|
||||||
- **Journalistic tone**: The summary should sound professional and journalistic, not too casual or vague.
|
|
||||||
- **Thorough and detailed**: Ensure that every key point from the text is captured and that the summary directly answers the query.
|
|
||||||
- **Not too lengthy, but detailed**: The summary should be informative but not excessively long. Focus on providing detailed information in a concise format.
|
|
||||||
|
|
||||||
The text will be shared inside the \`text\` XML tag, and the query inside the \`query\` XML tag.
|
|
||||||
|
|
||||||
<example>
|
|
||||||
1. \`<text>
|
|
||||||
Docker is a set of platform-as-a-service products that use OS-level virtualization to deliver software in packages called containers.
|
|
||||||
It was first released in 2013 and is developed by Docker, Inc. Docker is designed to make it easier to create, deploy, and run applications
|
|
||||||
by using containers.
|
|
||||||
</text>
|
|
||||||
|
|
||||||
<query>
|
|
||||||
What is Docker and how does it work?
|
|
||||||
</query>
|
|
||||||
|
|
||||||
Response:
|
|
||||||
Docker is a revolutionary platform-as-a-service product developed by Docker, Inc., that uses container technology to make application
|
|
||||||
deployment more efficient. It allows developers to package their software with all necessary dependencies, making it easier to run in
|
|
||||||
any environment. Released in 2013, Docker has transformed the way applications are built, deployed, and managed.
|
|
||||||
\`
|
|
||||||
2. \`<text>
|
|
||||||
The theory of relativity, or simply relativity, encompasses two interrelated theories of Albert Einstein: special relativity and general
|
|
||||||
relativity. However, the word "relativity" is sometimes used in reference to Galilean invariance. The term "theory of relativity" was based
|
|
||||||
on the expression "relative theory" used by Max Planck in 1906. The theory of relativity usually encompasses two interrelated theories by
|
|
||||||
Albert Einstein: special relativity and general relativity. Special relativity applies to all physical phenomena in the absence of gravity.
|
|
||||||
General relativity explains the law of gravitation and its relation to other forces of nature. It applies to the cosmological and astrophysical
|
|
||||||
realm, including astronomy.
|
|
||||||
</text>
|
|
||||||
|
|
||||||
<query>
|
|
||||||
summarize
|
|
||||||
</query>
|
|
||||||
|
|
||||||
Response:
|
|
||||||
The theory of relativity, developed by Albert Einstein, encompasses two main theories: special relativity and general relativity. Special
|
|
||||||
relativity applies to all physical phenomena in the absence of gravity, while general relativity explains the law of gravitation and its
|
|
||||||
relation to other forces of nature. The theory of relativity is based on the concept of "relative theory," as introduced by Max Planck in
|
|
||||||
1906. It is a fundamental theory in physics that has revolutionized our understanding of the universe.
|
|
||||||
\`
|
|
||||||
</example>
|
|
||||||
|
|
||||||
Everything below is the actual data you will be working with. Good luck!
|
|
||||||
|
|
||||||
<query>
|
|
||||||
${question}
|
|
||||||
</query>
|
|
||||||
|
|
||||||
<text>
|
|
||||||
${doc.pageContent}
|
|
||||||
</text>
|
|
||||||
|
|
||||||
Make sure to answer the query in the summary.
|
|
||||||
`,
|
|
||||||
{ signal },
|
|
||||||
);
|
|
||||||
|
|
||||||
const document = new Document({
|
|
||||||
pageContent: res.content as string,
|
|
||||||
metadata: {
|
|
||||||
title: doc.metadata.title,
|
|
||||||
url: doc.metadata.url,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
docs.push(document);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return { query: question, docs: docs };
|
|
||||||
} else {
|
|
||||||
if (this.config.additionalSearchCriteria) {
|
|
||||||
question = `${question} ${this.config.additionalSearchCriteria}`;
|
|
||||||
}
|
|
||||||
this.emitProgress(
|
|
||||||
emitter,
|
|
||||||
20,
|
|
||||||
`Searching the web`,
|
|
||||||
`Search Query: ${question}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const searxngResult = await searchSearxng(question, {
|
|
||||||
language: 'en',
|
|
||||||
engines: this.config.activeEngines,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Store the SearXNG URL for later use in emitting to the client
|
|
||||||
this.searxngUrl = searxngResult.searchUrl;
|
|
||||||
|
|
||||||
const documents = searxngResult.results.map(
|
|
||||||
(result) =>
|
|
||||||
new Document({
|
|
||||||
pageContent:
|
|
||||||
result.content ||
|
|
||||||
(this.config.activeEngines.includes('youtube')
|
|
||||||
? result.title
|
|
||||||
: '') /* Todo: Implement transcript grabbing using Youtubei (source: https://www.npmjs.com/package/youtubei) */,
|
|
||||||
metadata: {
|
|
||||||
title: result.title,
|
|
||||||
url: result.url,
|
|
||||||
...(result.img_src && { img_src: result.img_src }),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return { query: question, docs: documents, searchQuery: question };
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error in search retriever chain:', error);
|
|
||||||
emitter.emit('error', JSON.stringify({ data: error }));
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async createAnsweringChain(
|
|
||||||
llm: BaseChatModel,
|
|
||||||
fileIds: string[],
|
|
||||||
embeddings: Embeddings,
|
|
||||||
optimizationMode: 'speed' | 'balanced' | 'agent',
|
|
||||||
systemInstructions: string,
|
|
||||||
signal: AbortSignal,
|
|
||||||
emitter: eventEmitter,
|
|
||||||
personaInstructions?: string,
|
|
||||||
) {
|
|
||||||
return RunnableSequence.from([
|
|
||||||
RunnableMap.from({
|
|
||||||
systemInstructions: () => systemInstructions,
|
|
||||||
query: (input: BasicChainInput) => input.query,
|
|
||||||
chat_history: (input: BasicChainInput) => input.chat_history,
|
|
||||||
date: () => formatDateForLLM(),
|
|
||||||
personaInstructions: () => personaInstructions || '',
|
|
||||||
context: RunnableLambda.from(
|
|
||||||
async (
|
|
||||||
input: BasicChainInput,
|
|
||||||
options?: { signal?: AbortSignal },
|
|
||||||
) => {
|
|
||||||
// Check if the request was aborted
|
|
||||||
if (options?.signal?.aborted || signal?.aborted) {
|
|
||||||
console.log('Request cancelled by user');
|
|
||||||
throw new Error('Request cancelled by user');
|
|
||||||
}
|
|
||||||
|
|
||||||
const processedHistory = formatChatHistoryAsString(
|
|
||||||
input.chat_history,
|
|
||||||
);
|
|
||||||
|
|
||||||
let docs: Document[] | null = null;
|
|
||||||
let query = input.query;
|
|
||||||
|
|
||||||
if (this.config.searchWeb) {
|
|
||||||
const searchRetrieverChain =
|
|
||||||
await this.createSearchRetrieverChain(
|
|
||||||
llm,
|
|
||||||
systemInstructions,
|
|
||||||
emitter,
|
|
||||||
signal,
|
|
||||||
);
|
|
||||||
var date = formatDateForLLM();
|
|
||||||
|
|
||||||
const searchRetrieverResult = await searchRetrieverChain.invoke(
|
|
||||||
{
|
|
||||||
chat_history: processedHistory,
|
|
||||||
query,
|
|
||||||
date,
|
|
||||||
systemInstructions,
|
|
||||||
},
|
|
||||||
{ signal: options?.signal },
|
|
||||||
);
|
|
||||||
|
|
||||||
query = searchRetrieverResult.query;
|
|
||||||
docs = searchRetrieverResult.docs;
|
|
||||||
|
|
||||||
// Store the search query in the context for emitting to the client
|
|
||||||
if (searchRetrieverResult.searchQuery) {
|
|
||||||
this.searchQuery = searchRetrieverResult.searchQuery;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const sortedDocs = await this.rerankDocs(
|
|
||||||
query,
|
|
||||||
docs ?? [],
|
|
||||||
fileIds,
|
|
||||||
embeddings,
|
|
||||||
optimizationMode,
|
|
||||||
llm,
|
|
||||||
systemInstructions,
|
|
||||||
emitter,
|
|
||||||
signal,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (options?.signal?.aborted || signal?.aborted) {
|
|
||||||
console.log('Request cancelled by user');
|
|
||||||
throw new Error('Request cancelled by user');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.emitProgress(emitter, 100, `Done`);
|
|
||||||
return sortedDocs;
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.withConfig({
|
|
||||||
runName: 'FinalSourceRetriever',
|
|
||||||
})
|
|
||||||
.pipe(this.processDocs),
|
|
||||||
}),
|
|
||||||
ChatPromptTemplate.fromMessages([
|
|
||||||
['system', this.config.responsePrompt],
|
|
||||||
new MessagesPlaceholder('chat_history'),
|
|
||||||
['user', '{query}'],
|
|
||||||
]),
|
|
||||||
llm,
|
|
||||||
this.strParser,
|
|
||||||
]).withConfig({
|
|
||||||
runName: 'FinalResponseGenerator',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async rerankDocs(
|
|
||||||
query: string,
|
|
||||||
docs: Document[],
|
|
||||||
fileIds: string[],
|
|
||||||
embeddings: Embeddings,
|
|
||||||
optimizationMode: 'speed' | 'balanced' | 'agent',
|
|
||||||
llm: BaseChatModel,
|
|
||||||
systemInstructions: string,
|
|
||||||
emitter: eventEmitter,
|
|
||||||
signal: AbortSignal,
|
|
||||||
): Promise<Document[]> {
|
|
||||||
try {
|
|
||||||
if (docs.length === 0 && fileIds.length === 0) {
|
|
||||||
return docs;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query.toLocaleLowerCase() === 'summarize') {
|
|
||||||
return docs.slice(0, 15);
|
|
||||||
}
|
|
||||||
|
|
||||||
const filesData = fileIds
|
|
||||||
.map((file) => {
|
|
||||||
const filePath = path.join(process.cwd(), 'uploads', file);
|
|
||||||
|
|
||||||
const contentPath = filePath + '-extracted.json';
|
|
||||||
const embeddingsPath = filePath + '-embeddings.json';
|
|
||||||
|
|
||||||
const content = JSON.parse(fs.readFileSync(contentPath, 'utf8'));
|
|
||||||
const embeddings = JSON.parse(
|
|
||||||
fs.readFileSync(embeddingsPath, 'utf8'),
|
|
||||||
);
|
|
||||||
|
|
||||||
const fileSimilaritySearchObject = content.contents.map(
|
|
||||||
(c: string, i: number) => {
|
|
||||||
return {
|
|
||||||
fileName: content.title,
|
|
||||||
content: c,
|
|
||||||
embeddings: embeddings.embeddings[i],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return fileSimilaritySearchObject;
|
|
||||||
})
|
|
||||||
.flat();
|
|
||||||
|
|
||||||
let docsWithContent = docs.filter(
|
|
||||||
(doc) => doc.pageContent && doc.pageContent.length > 0,
|
|
||||||
);
|
|
||||||
|
|
||||||
const queryEmbedding = await embeddings.embedQuery(query);
|
|
||||||
|
|
||||||
const getRankedDocs = async (
|
|
||||||
queryEmbedding: number[],
|
|
||||||
includeFiles: boolean,
|
|
||||||
includeNonFileDocs: boolean,
|
|
||||||
maxDocs: number,
|
|
||||||
) => {
|
|
||||||
let docsToRank = includeNonFileDocs ? docsWithContent : [];
|
|
||||||
|
|
||||||
if (includeFiles) {
|
|
||||||
// Add file documents to the ranking
|
|
||||||
const fileDocs = filesData.map((fileData) => {
|
|
||||||
return new Document({
|
|
||||||
pageContent: fileData.content,
|
|
||||||
metadata: {
|
|
||||||
title: fileData.fileName,
|
|
||||||
url: `File`,
|
|
||||||
embeddings: fileData.embeddings,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
docsToRank.push(...fileDocs);
|
|
||||||
}
|
|
||||||
|
|
||||||
const similarity = await Promise.all(
|
|
||||||
docsToRank.map(async (doc, i) => {
|
|
||||||
const sim = computeSimilarity(
|
|
||||||
queryEmbedding,
|
|
||||||
doc.metadata?.embeddings
|
|
||||||
? doc.metadata?.embeddings
|
|
||||||
: (await embeddings.embedDocuments([doc.pageContent]))[0],
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
index: i,
|
|
||||||
similarity: sim,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
let rankedDocs = similarity
|
|
||||||
.filter(
|
|
||||||
(sim) => sim.similarity > (this.config.rerankThreshold ?? 0.3),
|
|
||||||
)
|
|
||||||
.sort((a, b) => b.similarity - a.similarity)
|
|
||||||
.map((sim) => docsToRank[sim.index]);
|
|
||||||
|
|
||||||
rankedDocs =
|
|
||||||
docsToRank.length > 0 ? rankedDocs.slice(0, maxDocs) : rankedDocs;
|
|
||||||
return rankedDocs;
|
|
||||||
};
|
|
||||||
if (optimizationMode === 'speed' || this.config.rerank === false) {
|
|
||||||
this.emitProgress(
|
|
||||||
emitter,
|
|
||||||
50,
|
|
||||||
`Ranking sources`,
|
|
||||||
this.searchQuery ? `Search Query: ${this.searchQuery}` : undefined,
|
|
||||||
);
|
|
||||||
if (filesData.length > 0) {
|
|
||||||
const sortedFiles = await getRankedDocs(
|
|
||||||
queryEmbedding,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
8,
|
|
||||||
);
|
|
||||||
|
|
||||||
return [
|
|
||||||
...sortedFiles,
|
|
||||||
...docsWithContent.slice(0, 15 - sortedFiles.length),
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
return docsWithContent.slice(0, 15);
|
|
||||||
}
|
|
||||||
} else if (optimizationMode === 'balanced') {
|
|
||||||
this.emitProgress(
|
|
||||||
emitter,
|
|
||||||
40,
|
|
||||||
`Ranking sources`,
|
|
||||||
this.searchQuery ? `Search Query: ${this.searchQuery}` : undefined,
|
|
||||||
);
|
|
||||||
// Get the top ranked attached files, if any
|
|
||||||
let sortedDocs = await getRankedDocs(queryEmbedding, true, false, 8);
|
|
||||||
|
|
||||||
sortedDocs = [
|
|
||||||
...sortedDocs,
|
|
||||||
...docsWithContent.slice(0, 15 - sortedDocs.length),
|
|
||||||
];
|
|
||||||
|
|
||||||
this.emitProgress(
|
|
||||||
emitter,
|
|
||||||
60,
|
|
||||||
`Enriching sources`,
|
|
||||||
this.searchQuery ? `Search Query: ${this.searchQuery}` : undefined,
|
|
||||||
);
|
|
||||||
sortedDocs = await Promise.all(
|
|
||||||
sortedDocs.map(async (doc) => {
|
|
||||||
const webContent = await getWebContent(doc.metadata.url);
|
|
||||||
const chunks =
|
|
||||||
webContent?.pageContent
|
|
||||||
.match(/.{1,500}/g)
|
|
||||||
?.map((chunk) => chunk.trim()) || [];
|
|
||||||
const chunkEmbeddings = await embeddings.embedDocuments(chunks);
|
|
||||||
const similarities = chunkEmbeddings.map((chunkEmbedding) => {
|
|
||||||
return computeSimilarity(queryEmbedding, chunkEmbedding);
|
|
||||||
});
|
|
||||||
|
|
||||||
const topChunks = similarities
|
|
||||||
.map((similarity, index) => ({ similarity, index }))
|
|
||||||
.sort((a, b) => b.similarity - a.similarity)
|
|
||||||
.slice(0, 5)
|
|
||||||
.map((chunk) => chunks[chunk.index]);
|
|
||||||
const excerpt = topChunks.join('\n\n');
|
|
||||||
|
|
||||||
let newDoc = {
|
|
||||||
...doc,
|
|
||||||
pageContent: excerpt
|
|
||||||
? `${excerpt}\n\n${doc.pageContent}`
|
|
||||||
: doc.pageContent,
|
|
||||||
};
|
|
||||||
return newDoc;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return sortedDocs;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error in rerankDocs:', error);
|
|
||||||
emitter.emit('error', JSON.stringify({ data: error }));
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
private processDocs(docs: Document[]) {
|
|
||||||
const fullDocs = docs
|
|
||||||
.map(
|
|
||||||
(_, index) =>
|
|
||||||
`<${index + 1}>\n
|
|
||||||
<title>${docs[index].metadata.title}</title>\n
|
|
||||||
${docs[index].metadata?.url.toLowerCase().includes('file') ? '' : '\n<url>' + docs[index].metadata.url + '</url>\n'}
|
|
||||||
<content>\n${docs[index].pageContent}\n</content>\n
|
|
||||||
</${index + 1}>\n`,
|
|
||||||
)
|
|
||||||
.join('\n');
|
|
||||||
console.log('Processed docs:', fullDocs);
|
|
||||||
return fullDocs;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async handleStream(
|
|
||||||
stream: AsyncGenerator<StreamEvent, any, any>,
|
|
||||||
emitter: eventEmitter,
|
|
||||||
llm: BaseChatModel,
|
|
||||||
signal: AbortSignal,
|
|
||||||
) {
|
|
||||||
if (signal.aborted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for await (const event of stream) {
|
|
||||||
if (signal.aborted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
event.event === 'on_chain_end' &&
|
|
||||||
event.name === 'FinalSourceRetriever'
|
|
||||||
) {
|
|
||||||
const sourcesData = event.data.output;
|
|
||||||
if (this.searchQuery) {
|
|
||||||
emitter.emit(
|
|
||||||
'data',
|
|
||||||
JSON.stringify({
|
|
||||||
type: 'sources',
|
|
||||||
data: sourcesData,
|
|
||||||
searchQuery: this.searchQuery,
|
|
||||||
searchUrl: this.searxngUrl,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
emitter.emit(
|
|
||||||
'data',
|
|
||||||
JSON.stringify({ type: 'sources', data: sourcesData }),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
event.event === 'on_chain_stream' &&
|
|
||||||
event.name === 'FinalResponseGenerator'
|
|
||||||
) {
|
|
||||||
emitter.emit(
|
|
||||||
'data',
|
|
||||||
JSON.stringify({ type: 'response', data: event.data.chunk }),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
event.event === 'on_chain_end' &&
|
|
||||||
event.name === 'FinalResponseGenerator'
|
|
||||||
) {
|
|
||||||
const modelName = getModelName(llm);
|
|
||||||
|
|
||||||
// Send model info before ending
|
|
||||||
emitter.emit(
|
|
||||||
'stats',
|
|
||||||
JSON.stringify({
|
|
||||||
type: 'modelStats',
|
|
||||||
data: {
|
|
||||||
modelName,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
emitter.emit('end');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute agent workflow asynchronously with proper streaming support
|
* Execute agent workflow asynchronously with proper streaming support
|
||||||
*/
|
*/
|
||||||
|
|
@ -719,7 +86,7 @@ ${docs[index].metadata?.url.toLowerCase().includes('file') ? '' : '\n<url>' + do
|
||||||
history: BaseMessage[],
|
history: BaseMessage[],
|
||||||
llm: BaseChatModel,
|
llm: BaseChatModel,
|
||||||
embeddings: Embeddings,
|
embeddings: Embeddings,
|
||||||
optimizationMode: 'speed' | 'balanced' | 'agent',
|
optimizationMode: 'speed' | 'agent',
|
||||||
fileIds: string[],
|
fileIds: string[],
|
||||||
systemInstructions: string,
|
systemInstructions: string,
|
||||||
signal: AbortSignal,
|
signal: AbortSignal,
|
||||||
|
|
@ -728,9 +95,22 @@ ${docs[index].metadata?.url.toLowerCase().includes('file') ? '' : '\n<url>' + do
|
||||||
) {
|
) {
|
||||||
const emitter = new eventEmitter();
|
const emitter = new eventEmitter();
|
||||||
|
|
||||||
// Branch to agent search if optimization mode is 'agent'
|
// Branch to speed search if optimization mode is 'speed'
|
||||||
if (optimizationMode === 'agent') {
|
if (optimizationMode === 'speed') {
|
||||||
// Execute agent workflow asynchronously to maintain streaming
|
const speedSearchAgent = new SpeedSearchAgent(this.config);
|
||||||
|
return speedSearchAgent.searchAndAnswer(
|
||||||
|
message,
|
||||||
|
history,
|
||||||
|
llm,
|
||||||
|
embeddings,
|
||||||
|
systemInstructions,
|
||||||
|
signal,
|
||||||
|
personaInstructions,
|
||||||
|
focusMode,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute agent workflow for 'agent' mode
|
||||||
this.executeAgentWorkflow(
|
this.executeAgentWorkflow(
|
||||||
llm,
|
llm,
|
||||||
embeddings,
|
embeddings,
|
||||||
|
|
@ -743,34 +123,6 @@ ${docs[index].metadata?.url.toLowerCase().includes('file') ? '' : '\n<url>' + do
|
||||||
signal,
|
signal,
|
||||||
focusMode || 'webSearch',
|
focusMode || 'webSearch',
|
||||||
);
|
);
|
||||||
return emitter;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Existing logic for other optimization modes
|
|
||||||
const answeringChain = await this.createAnsweringChain(
|
|
||||||
llm,
|
|
||||||
fileIds,
|
|
||||||
embeddings,
|
|
||||||
optimizationMode,
|
|
||||||
systemInstructions,
|
|
||||||
signal,
|
|
||||||
emitter,
|
|
||||||
personaInstructions,
|
|
||||||
);
|
|
||||||
|
|
||||||
const stream = answeringChain.streamEvents(
|
|
||||||
{
|
|
||||||
chat_history: history,
|
|
||||||
query: message,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
version: 'v1',
|
|
||||||
// Pass the abort signal to the LLM streaming chain
|
|
||||||
signal,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
this.handleStream(stream, emitter, llm, signal);
|
|
||||||
|
|
||||||
return emitter;
|
return emitter;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
560
src/lib/search/speedSearch.ts
Normal file
560
src/lib/search/speedSearch.ts
Normal file
|
|
@ -0,0 +1,560 @@
|
||||||
|
import type { Embeddings } from '@langchain/core/embeddings';
|
||||||
|
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||||
|
import { BaseMessage } from '@langchain/core/messages';
|
||||||
|
import { StringOutputParser } from '@langchain/core/output_parsers';
|
||||||
|
import {
|
||||||
|
ChatPromptTemplate,
|
||||||
|
MessagesPlaceholder,
|
||||||
|
PromptTemplate,
|
||||||
|
} from '@langchain/core/prompts';
|
||||||
|
import {
|
||||||
|
RunnableLambda,
|
||||||
|
RunnableMap,
|
||||||
|
RunnableSequence,
|
||||||
|
} from '@langchain/core/runnables';
|
||||||
|
import { StreamEvent } from '@langchain/core/tracers/log_stream';
|
||||||
|
import { ChatOpenAI } from '@langchain/openai';
|
||||||
|
import eventEmitter from 'events';
|
||||||
|
import { Document } from 'langchain/document';
|
||||||
|
import LineOutputParser from '../outputParsers/lineOutputParser';
|
||||||
|
import LineListOutputParser from '../outputParsers/listLineOutputParser';
|
||||||
|
import { searchSearxng } from '../searxng';
|
||||||
|
import { formatDateForLLM } from '../utils';
|
||||||
|
import { getDocumentsFromLinks } from '../utils/documents';
|
||||||
|
import formatChatHistoryAsString from '../utils/formatHistory';
|
||||||
|
import { getModelName } from '../utils/modelUtils';
|
||||||
|
|
||||||
|
export interface SpeedSearchAgentType {
|
||||||
|
searchAndAnswer: (
|
||||||
|
message: string,
|
||||||
|
history: BaseMessage[],
|
||||||
|
llm: BaseChatModel,
|
||||||
|
embeddings: Embeddings,
|
||||||
|
systemInstructions: string,
|
||||||
|
signal: AbortSignal,
|
||||||
|
personaInstructions?: string,
|
||||||
|
focusMode?: string,
|
||||||
|
) => Promise<eventEmitter>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Config {
|
||||||
|
searchWeb: boolean;
|
||||||
|
rerank: boolean;
|
||||||
|
summarizer: boolean;
|
||||||
|
rerankThreshold: number;
|
||||||
|
queryGeneratorPrompt: string;
|
||||||
|
responsePrompt: string;
|
||||||
|
activeEngines: string[];
|
||||||
|
additionalSearchCriteria?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type BasicChainInput = {
|
||||||
|
chat_history: BaseMessage[];
|
||||||
|
query: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
class SpeedSearchAgent implements SpeedSearchAgentType {
|
||||||
|
private config: Config;
|
||||||
|
private strParser = new StringOutputParser();
|
||||||
|
private searchQuery?: string;
|
||||||
|
private searxngUrl?: string;
|
||||||
|
|
||||||
|
constructor(config: Config) {
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit a progress event with the given percentage and message
|
||||||
|
*/
|
||||||
|
private emitProgress(
|
||||||
|
emitter: eventEmitter,
|
||||||
|
percentage: number,
|
||||||
|
message: string,
|
||||||
|
subMessage?: string,
|
||||||
|
) {
|
||||||
|
const progressData: any = {
|
||||||
|
message,
|
||||||
|
current: percentage,
|
||||||
|
total: 100,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add subMessage if provided
|
||||||
|
if (subMessage) {
|
||||||
|
progressData.subMessage = subMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
emitter.emit(
|
||||||
|
'progress',
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'progress',
|
||||||
|
data: progressData,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createSearchRetrieverChain(
|
||||||
|
llm: BaseChatModel,
|
||||||
|
systemInstructions: string,
|
||||||
|
emitter: eventEmitter,
|
||||||
|
signal: AbortSignal,
|
||||||
|
) {
|
||||||
|
// TODO: Don't we want to set this back to default once search is done?
|
||||||
|
(llm as unknown as ChatOpenAI).temperature = 0;
|
||||||
|
|
||||||
|
this.emitProgress(emitter, 10, `Building search query`);
|
||||||
|
|
||||||
|
return RunnableSequence.from([
|
||||||
|
PromptTemplate.fromTemplate(this.config.queryGeneratorPrompt),
|
||||||
|
llm,
|
||||||
|
this.strParser,
|
||||||
|
RunnableLambda.from(async (input: string) => {
|
||||||
|
try {
|
||||||
|
//console.log(`LLM response for initial web search:"${input}"`);
|
||||||
|
const linksOutputParser = new LineListOutputParser({
|
||||||
|
key: 'links',
|
||||||
|
});
|
||||||
|
|
||||||
|
const questionOutputParser = new LineOutputParser({
|
||||||
|
key: 'answer',
|
||||||
|
});
|
||||||
|
|
||||||
|
const links = await linksOutputParser.parse(input);
|
||||||
|
let question = await questionOutputParser.parse(input);
|
||||||
|
|
||||||
|
//console.log('question', question);
|
||||||
|
|
||||||
|
if (question === 'not_needed') {
|
||||||
|
return { query: '', docs: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (links.length > 0) {
|
||||||
|
if (question.length === 0) {
|
||||||
|
question = 'summarize';
|
||||||
|
}
|
||||||
|
|
||||||
|
let docs: Document[] = [];
|
||||||
|
|
||||||
|
const linkDocs = await getDocumentsFromLinks({ links });
|
||||||
|
|
||||||
|
const docGroups: Document[] = [];
|
||||||
|
|
||||||
|
linkDocs.map((doc) => {
|
||||||
|
const URLDocExists = docGroups.find(
|
||||||
|
(d) =>
|
||||||
|
d.metadata.url === doc.metadata.url &&
|
||||||
|
d.metadata.totalDocs < 10,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!URLDocExists) {
|
||||||
|
docGroups.push({
|
||||||
|
...doc,
|
||||||
|
metadata: {
|
||||||
|
...doc.metadata,
|
||||||
|
totalDocs: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const docIndex = docGroups.findIndex(
|
||||||
|
(d) =>
|
||||||
|
d.metadata.url === doc.metadata.url &&
|
||||||
|
d.metadata.totalDocs < 10,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (docIndex !== -1) {
|
||||||
|
docGroups[docIndex].pageContent =
|
||||||
|
docGroups[docIndex].pageContent + `\n\n` + doc.pageContent;
|
||||||
|
docGroups[docIndex].metadata.totalDocs += 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.emitProgress(emitter, 20, `Summarizing content`);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
docGroups.map(async (doc) => {
|
||||||
|
const systemPrompt = systemInstructions
|
||||||
|
? `${systemInstructions}\n\n`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const res = await llm.invoke(
|
||||||
|
`${systemPrompt}You are a web search summarizer, tasked with summarizing a piece of text retrieved from a web search. Your job is to summarize the
|
||||||
|
text into a detailed, 2-4 paragraph explanation that captures the main ideas and provides a comprehensive answer to the query.
|
||||||
|
If the query is \"summarize\", you should provide a detailed summary of the text. If the query is a specific question, you should answer it in the summary.
|
||||||
|
|
||||||
|
- **Journalistic tone**: The summary should sound professional and journalistic, not too casual or vague.
|
||||||
|
- **Thorough and detailed**: Ensure that every key point from the text is captured and that the summary directly answers the query.
|
||||||
|
- **Not too lengthy, but detailed**: The summary should be informative but not excessively long. Focus on providing detailed information in a concise format.
|
||||||
|
|
||||||
|
The text will be shared inside the \`text\` XML tag, and the query inside the \`query\` XML tag.
|
||||||
|
|
||||||
|
<example>
|
||||||
|
1. \`<text>
|
||||||
|
Docker is a set of platform-as-a-service products that use OS-level virtualization to deliver software in packages called containers.
|
||||||
|
It was first released in 2013 and is developed by Docker, Inc. Docker is designed to make it easier to create, deploy, and run applications
|
||||||
|
by using containers.
|
||||||
|
</text>
|
||||||
|
|
||||||
|
<query>
|
||||||
|
What is Docker and how does it work?
|
||||||
|
</query>
|
||||||
|
|
||||||
|
Response:
|
||||||
|
Docker is a revolutionary platform-as-a-service product developed by Docker, Inc., that uses container technology to make application
|
||||||
|
deployment more efficient. It allows developers to package their software with all necessary dependencies, making it easier to run in
|
||||||
|
any environment. Released in 2013, Docker has transformed the way applications are built, deployed, and managed.
|
||||||
|
\`
|
||||||
|
2. \`<text>
|
||||||
|
The theory of relativity, or simply relativity, encompasses two interrelated theories of Albert Einstein: special relativity and general
|
||||||
|
relativity. However, the word "relativity" is sometimes used in reference to Galilean invariance. The term "theory of relativity" was based
|
||||||
|
on the expression "relative theory" used by Max Planck in 1906. The theory of relativity usually encompasses two interrelated theories by
|
||||||
|
Albert Einstein: special relativity and general relativity. Special relativity applies to all physical phenomena in the absence of gravity.
|
||||||
|
General relativity explains the law of gravitation and its relation to other forces of nature. It applies to the cosmological and astrophysical
|
||||||
|
realm, including astronomy.
|
||||||
|
</text>
|
||||||
|
|
||||||
|
<query>
|
||||||
|
summarize
|
||||||
|
</query>
|
||||||
|
|
||||||
|
Response:
|
||||||
|
The theory of relativity, developed by Albert Einstein, encompasses two main theories: special relativity and general relativity. Special
|
||||||
|
relativity applies to all physical phenomena in the absence of gravity, while general relativity explains the law of gravitation and its
|
||||||
|
relation to other forces of nature. The theory of relativity is based on the concept of "relative theory," as introduced by Max Planck in
|
||||||
|
1906. It is a fundamental theory in physics that has revolutionized our understanding of the universe.
|
||||||
|
\`
|
||||||
|
</example>
|
||||||
|
|
||||||
|
Everything below is the actual data you will be working with. Good luck!
|
||||||
|
|
||||||
|
<query>
|
||||||
|
${question}
|
||||||
|
</query>
|
||||||
|
|
||||||
|
<text>
|
||||||
|
${doc.pageContent}
|
||||||
|
</text>
|
||||||
|
|
||||||
|
Make sure to answer the query in the summary.
|
||||||
|
`,
|
||||||
|
{ signal },
|
||||||
|
);
|
||||||
|
|
||||||
|
const document = new Document({
|
||||||
|
pageContent: res.content as string,
|
||||||
|
metadata: {
|
||||||
|
title: doc.metadata.title,
|
||||||
|
url: doc.metadata.url,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
docs.push(document);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return { query: question, docs: docs };
|
||||||
|
} else {
|
||||||
|
if (this.config.additionalSearchCriteria) {
|
||||||
|
question = `${question} ${this.config.additionalSearchCriteria}`;
|
||||||
|
}
|
||||||
|
this.emitProgress(
|
||||||
|
emitter,
|
||||||
|
20,
|
||||||
|
`Searching the web`,
|
||||||
|
`Search Query: ${question}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const searxngResult = await searchSearxng(question, {
|
||||||
|
language: 'en',
|
||||||
|
engines: this.config.activeEngines,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Store the SearXNG URL for later use in emitting to the client
|
||||||
|
this.searxngUrl = searxngResult.searchUrl;
|
||||||
|
|
||||||
|
const documents = searxngResult.results.map(
|
||||||
|
(result) =>
|
||||||
|
new Document({
|
||||||
|
pageContent:
|
||||||
|
result.content ||
|
||||||
|
(this.config.activeEngines.includes('youtube')
|
||||||
|
? result.title
|
||||||
|
: '') /* Todo: Implement transcript grabbing using Youtubei (source: https://www.npmjs.com/package/youtubei) */,
|
||||||
|
metadata: {
|
||||||
|
title: result.title,
|
||||||
|
url: result.url,
|
||||||
|
...(result.img_src && { img_src: result.img_src }),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return { query: question, docs: documents, searchQuery: question };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in search retriever chain:', error);
|
||||||
|
emitter.emit('error', JSON.stringify({ data: error }));
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createAnsweringChain(
|
||||||
|
llm: BaseChatModel,
|
||||||
|
embeddings: Embeddings,
|
||||||
|
systemInstructions: string,
|
||||||
|
signal: AbortSignal,
|
||||||
|
emitter: eventEmitter,
|
||||||
|
personaInstructions?: string,
|
||||||
|
) {
|
||||||
|
return RunnableSequence.from([
|
||||||
|
RunnableMap.from({
|
||||||
|
systemInstructions: () => systemInstructions,
|
||||||
|
query: (input: BasicChainInput) => input.query,
|
||||||
|
chat_history: (input: BasicChainInput) => input.chat_history,
|
||||||
|
date: () => formatDateForLLM(),
|
||||||
|
personaInstructions: () => personaInstructions || '',
|
||||||
|
context: RunnableLambda.from(
|
||||||
|
async (
|
||||||
|
input: BasicChainInput,
|
||||||
|
options?: { signal?: AbortSignal },
|
||||||
|
) => {
|
||||||
|
// Check if the request was aborted
|
||||||
|
if (options?.signal?.aborted || signal?.aborted) {
|
||||||
|
console.log('Request cancelled by user');
|
||||||
|
throw new Error('Request cancelled by user');
|
||||||
|
}
|
||||||
|
|
||||||
|
const processedHistory = formatChatHistoryAsString(
|
||||||
|
input.chat_history,
|
||||||
|
);
|
||||||
|
|
||||||
|
let docs: Document[] | null = null;
|
||||||
|
let query = input.query;
|
||||||
|
|
||||||
|
if (this.config.searchWeb) {
|
||||||
|
const searchRetrieverChain =
|
||||||
|
await this.createSearchRetrieverChain(
|
||||||
|
llm,
|
||||||
|
systemInstructions,
|
||||||
|
emitter,
|
||||||
|
signal,
|
||||||
|
);
|
||||||
|
var date = formatDateForLLM();
|
||||||
|
|
||||||
|
const searchRetrieverResult = await searchRetrieverChain.invoke(
|
||||||
|
{
|
||||||
|
chat_history: processedHistory,
|
||||||
|
query,
|
||||||
|
date,
|
||||||
|
systemInstructions,
|
||||||
|
},
|
||||||
|
{ signal: options?.signal },
|
||||||
|
);
|
||||||
|
|
||||||
|
query = searchRetrieverResult.query;
|
||||||
|
docs = searchRetrieverResult.docs;
|
||||||
|
|
||||||
|
// Store the search query in the context for emitting to the client
|
||||||
|
if (searchRetrieverResult.searchQuery) {
|
||||||
|
this.searchQuery = searchRetrieverResult.searchQuery;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedDocs = await this.rerankDocsForSpeed(
|
||||||
|
query,
|
||||||
|
docs ?? [],
|
||||||
|
embeddings,
|
||||||
|
emitter,
|
||||||
|
signal,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (options?.signal?.aborted || signal?.aborted) {
|
||||||
|
console.log('Request cancelled by user');
|
||||||
|
throw new Error('Request cancelled by user');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.emitProgress(emitter, 100, `Done`);
|
||||||
|
return sortedDocs;
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.withConfig({
|
||||||
|
runName: 'FinalSourceRetriever',
|
||||||
|
})
|
||||||
|
.pipe(this.processDocs),
|
||||||
|
}),
|
||||||
|
ChatPromptTemplate.fromMessages([
|
||||||
|
['system', this.config.responsePrompt],
|
||||||
|
new MessagesPlaceholder('chat_history'),
|
||||||
|
['user', '{query}'],
|
||||||
|
]),
|
||||||
|
llm,
|
||||||
|
this.strParser,
|
||||||
|
]).withConfig({
|
||||||
|
runName: 'FinalResponseGenerator',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Speed-optimized document reranking with simplified logic for web results only
|
||||||
|
*/
|
||||||
|
private async rerankDocsForSpeed(
|
||||||
|
query: string,
|
||||||
|
docs: Document[],
|
||||||
|
embeddings: Embeddings,
|
||||||
|
emitter: eventEmitter,
|
||||||
|
signal: AbortSignal,
|
||||||
|
): Promise<Document[]> {
|
||||||
|
try {
|
||||||
|
if (docs.length === 0) {
|
||||||
|
return docs;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.toLocaleLowerCase() === 'summarize') {
|
||||||
|
return docs.slice(0, 15);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out documents with no content
|
||||||
|
let docsWithContent = docs.filter(
|
||||||
|
(doc) => doc.pageContent && doc.pageContent.length > 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Speed mode logic - simply return first 15 documents with content
|
||||||
|
// No similarity ranking to prioritize speed
|
||||||
|
this.emitProgress(
|
||||||
|
emitter,
|
||||||
|
50,
|
||||||
|
`Ranking sources`,
|
||||||
|
this.searchQuery ? `Search Query: ${this.searchQuery}` : undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
return docsWithContent.slice(0, 15);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in rerankDocsForSpeed:', error);
|
||||||
|
emitter.emit('error', JSON.stringify({ data: error }));
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private processDocs(docs: Document[]) {
|
||||||
|
const fullDocs = docs
|
||||||
|
.map(
|
||||||
|
(_, index) =>
|
||||||
|
`<${index + 1}>\n
|
||||||
|
<title>${docs[index].metadata.title}</title>\n
|
||||||
|
${docs[index].metadata?.url.toLowerCase().includes('file') ? '' : '\n<url>' + docs[index].metadata.url + '</url>\n'}
|
||||||
|
<content>\n${docs[index].pageContent}\n</content>\n
|
||||||
|
</${index + 1}>\n`,
|
||||||
|
)
|
||||||
|
.join('\n');
|
||||||
|
console.log('Processed docs:', fullDocs);
|
||||||
|
return fullDocs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleStream(
|
||||||
|
stream: AsyncGenerator<StreamEvent, any, any>,
|
||||||
|
emitter: eventEmitter,
|
||||||
|
llm: BaseChatModel,
|
||||||
|
signal: AbortSignal,
|
||||||
|
) {
|
||||||
|
if (signal.aborted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for await (const event of stream) {
|
||||||
|
if (signal.aborted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
event.event === 'on_chain_end' &&
|
||||||
|
event.name === 'FinalSourceRetriever'
|
||||||
|
) {
|
||||||
|
const sourcesData = event.data.output;
|
||||||
|
if (this.searchQuery) {
|
||||||
|
emitter.emit(
|
||||||
|
'data',
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'sources',
|
||||||
|
data: sourcesData,
|
||||||
|
searchQuery: this.searchQuery,
|
||||||
|
searchUrl: this.searxngUrl,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
emitter.emit(
|
||||||
|
'data',
|
||||||
|
JSON.stringify({ type: 'sources', data: sourcesData }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
event.event === 'on_chain_stream' &&
|
||||||
|
event.name === 'FinalResponseGenerator'
|
||||||
|
) {
|
||||||
|
emitter.emit(
|
||||||
|
'data',
|
||||||
|
JSON.stringify({ type: 'response', data: event.data.chunk }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
event.event === 'on_chain_end' &&
|
||||||
|
event.name === 'FinalResponseGenerator'
|
||||||
|
) {
|
||||||
|
const modelName = getModelName(llm);
|
||||||
|
|
||||||
|
// Send model info before ending
|
||||||
|
emitter.emit(
|
||||||
|
'stats',
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'modelStats',
|
||||||
|
data: {
|
||||||
|
modelName,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
emitter.emit('end');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async searchAndAnswer(
|
||||||
|
message: string,
|
||||||
|
history: BaseMessage[],
|
||||||
|
llm: BaseChatModel,
|
||||||
|
embeddings: Embeddings,
|
||||||
|
systemInstructions: string,
|
||||||
|
signal: AbortSignal,
|
||||||
|
personaInstructions?: string,
|
||||||
|
focusMode?: string,
|
||||||
|
) {
|
||||||
|
const emitter = new eventEmitter();
|
||||||
|
|
||||||
|
const answeringChain = await this.createAnsweringChain(
|
||||||
|
llm,
|
||||||
|
embeddings,
|
||||||
|
systemInstructions,
|
||||||
|
signal,
|
||||||
|
emitter,
|
||||||
|
personaInstructions,
|
||||||
|
);
|
||||||
|
|
||||||
|
const stream = answeringChain.streamEvents(
|
||||||
|
{
|
||||||
|
chat_history: history,
|
||||||
|
query: message,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: 'v1',
|
||||||
|
// Pass the abort signal to the LLM streaming chain
|
||||||
|
signal,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
this.handleStream(stream, emitter, llm, signal);
|
||||||
|
|
||||||
|
return emitter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SpeedSearchAgent;
|
||||||
|
|
@ -17,7 +17,9 @@ export interface FileData {
|
||||||
* @param fileIds Array of file IDs to process
|
* @param fileIds Array of file IDs to process
|
||||||
* @returns Array of Document objects with content and embeddings
|
* @returns Array of Document objects with content and embeddings
|
||||||
*/
|
*/
|
||||||
export async function processFilesToDocuments(fileIds: string[]): Promise<Document[]> {
|
export async function processFilesToDocuments(
|
||||||
|
fileIds: string[],
|
||||||
|
): Promise<Document[]> {
|
||||||
if (fileIds.length === 0) {
|
if (fileIds.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
3
testAttachments/sporting-events.txt
Normal file
3
testAttachments/sporting-events.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
Who won the 2025 Super Bowl?
|
||||||
|
Who won the 2023 Formula One Driver's Championship?
|
||||||
|
Who won the 2022 World Cup?
|
||||||
Loading…
Add table
Add a link
Reference in a new issue