mirror of
https://github.com/ItzCrazyKns/Perplexica.git
synced 2026-01-12 08:55:48 +00:00
Compare commits
12 Commits
4fc810d976
...
a14f3e9464
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a14f3e9464 | ||
|
|
9afea48d31 | ||
|
|
2d82cd65d9 | ||
|
|
97838fd693 | ||
|
|
8ab675b119 | ||
|
|
5e3001756b | ||
|
|
4c4c1d1930 | ||
|
|
3c524b0f98 | ||
|
|
e99c8bdd50 | ||
|
|
574b3d55e2 | ||
|
|
f2f2af9451 | ||
|
|
65ef299d72 |
@@ -1,3 +1,5 @@
|
||||
import pkg from './package.json' with { type: 'json' };
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: 'standalone',
|
||||
@@ -9,6 +11,9 @@ const nextConfig = {
|
||||
],
|
||||
},
|
||||
serverExternalPackages: ['pdf-parse'],
|
||||
env: {
|
||||
NEXT_PUBLIC_VERSION: pkg.version,
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
@@ -59,7 +59,7 @@ const Chat = () => {
|
||||
}, [messages]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col space-y-6 pt-8 pb-44 lg:pb-32 sm:mx-4 md:mx-8">
|
||||
<div className="flex flex-col space-y-6 pt-8 pb-28 sm:mx-4 md:mx-8">
|
||||
{sections.map((section, i) => {
|
||||
const isLast = i === sections.length - 1;
|
||||
|
||||
@@ -80,10 +80,21 @@ const Chat = () => {
|
||||
{loading && !messageAppeared && <MessageBoxLoading />}
|
||||
<div ref={messageEnd} className="h-0" />
|
||||
{dividerWidth > 0 && (
|
||||
<div className="bottom-6 fixed z-40" style={{ width: dividerWidth }}>
|
||||
<div
|
||||
className="bottom-24 lg:bottom-10 fixed z-40"
|
||||
style={{ width: dividerWidth }}
|
||||
>
|
||||
className="pointer-events-none absolute -bottom-6 left-0 right-0 h-[calc(100%+24px+24px)] dark:hidden"
|
||||
style={{
|
||||
background:
|
||||
'linear-gradient(to top, #ffffff 0%, #ffffff 35%, rgba(255,255,255,0.95) 45%, rgba(255,255,255,0.85) 55%, rgba(255,255,255,0.7) 65%, rgba(255,255,255,0.5) 75%, rgba(255,255,255,0.3) 85%, rgba(255,255,255,0.1) 92%, transparent 100%)',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="pointer-events-none absolute -bottom-6 left-0 right-0 h-[calc(100%+24px+24px)] hidden dark:block"
|
||||
style={{
|
||||
background:
|
||||
'linear-gradient(to top, #0d1117 0%, #0d1117 35%, rgba(13,17,23,0.95) 45%, rgba(13,17,23,0.85) 55%, rgba(13,17,23,0.7) 65%, rgba(13,17,23,0.5) 75%, rgba(13,17,23,0.3) 85%, rgba(13,17,23,0.1) 92%, transparent 100%)',
|
||||
}}
|
||||
/>
|
||||
<MessageInput />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -2,9 +2,7 @@ import { cn } from '@/lib/utils';
|
||||
import { ArrowUp } from 'lucide-react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import TextareaAutosize from 'react-textarea-autosize';
|
||||
import Attach from './MessageInputActions/Attach';
|
||||
import CopilotToggle from './MessageInputActions/Copilot';
|
||||
import { File } from './ChatWindow';
|
||||
import AttachSmall from './MessageInputActions/AttachSmall';
|
||||
import { useChat } from '@/lib/hooks/useChat';
|
||||
|
||||
@@ -64,7 +62,7 @@ const MessageInput = () => {
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
'bg-light-secondary dark:bg-dark-secondary p-4 flex items-center overflow-hidden border border-light-200 dark:border-dark-200 shadow-sm shadow-light-200/10 dark:shadow-black/20 transition-all duration-200 focus-within:border-light-300 dark:focus-within:border-dark-300',
|
||||
'relative bg-light-secondary dark:bg-dark-secondary p-4 flex items-center overflow-hidden border border-light-200 dark:border-dark-200 shadow-sm shadow-light-200/10 dark:shadow-black/20 transition-all duration-200 focus-within:border-light-300 dark:focus-within:border-dark-300',
|
||||
mode === 'multi' ? 'flex-col rounded-2xl' : 'flex-row rounded-full',
|
||||
)}
|
||||
>
|
||||
@@ -103,7 +101,7 @@ const MessageInput = () => {
|
||||
/>
|
||||
<button
|
||||
disabled={message.trim().length === 0 || loading}
|
||||
className="bg-[#24A0ED] text-white text-black/50 dark:disabled:text-white/50 hover:bg-opacity-85 transition duration-100 disabled:bg-[#e0e0dc79] dark:disabled:bg-[#ececec21] rounded-full p-2"
|
||||
className="bg-[#24A0ED] text-white disabled:text-black/50 dark:disabled:text-white/50 hover:bg-opacity-85 transition duration-100 disabled:bg-[#e0e0dc79] dark:disabled:bg-[#ececec21] rounded-full p-2"
|
||||
>
|
||||
<ArrowUp className="bg-background" size={17} />
|
||||
</button>
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
ArrowLeft,
|
||||
BrainCog,
|
||||
ChevronLeft,
|
||||
ExternalLink,
|
||||
Search,
|
||||
Sliders,
|
||||
ToggleRight,
|
||||
@@ -115,7 +116,8 @@ const SettingsDialogue = ({
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-1 inset-0 h-full overflow-hidden">
|
||||
<div className="hidden lg:flex flex-col w-[240px] border-r border-white-200 dark:border-dark-200 h-full px-3 pt-3 overflow-y-auto">
|
||||
<div className="hidden lg:flex flex-col justify-between w-[240px] border-r border-white-200 dark:border-dark-200 h-full px-3 pt-3 overflow-y-auto">
|
||||
<div className="flex flex-col">
|
||||
<button
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="group flex flex-row items-center hover:bg-light-200 hover:dark:bg-dark-200 p-2 rounded-lg"
|
||||
@@ -128,6 +130,7 @@ const SettingsDialogue = ({
|
||||
Back
|
||||
</p>
|
||||
</button>
|
||||
|
||||
<div className="flex flex-col items-start space-y-1 mt-8">
|
||||
{sections.map((section) => (
|
||||
<button
|
||||
@@ -146,6 +149,21 @@ const SettingsDialogue = ({
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-1 py-[18px] px-2">
|
||||
<p className="text-xs text-black/70 dark:text-white/70">
|
||||
Version: {process.env.NEXT_PUBLIC_VERSION}
|
||||
</p>
|
||||
<a
|
||||
href="https://github.com/itzcrazykns/perplexica"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs text-black/70 dark:text-white/70 flex flex-row space-x-1 items-center transition duration-200 hover:text-black/90 hover:dark:text-white/90"
|
||||
>
|
||||
<span>GitHub</span>
|
||||
<ExternalLink size={12} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full flex flex-col overflow-hidden">
|
||||
<div className="flex flex-row lg:hidden w-full justify-between px-[20px] my-4 flex-shrink-0">
|
||||
<button
|
||||
|
||||
@@ -4,11 +4,9 @@ import { ResearchAction } from '../../types';
|
||||
const doneAction: ResearchAction<any> = {
|
||||
name: 'done',
|
||||
description:
|
||||
"Indicates that the research process is complete and no further actions are needed. Use this action when you have gathered sufficient information to answer the user's query.",
|
||||
'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.',
|
||||
enabled: (_) => true,
|
||||
schema: z.object({
|
||||
type: z.literal('done'),
|
||||
}),
|
||||
schema: z.object({}),
|
||||
execute: async (params, additionalConfig) => {
|
||||
return {
|
||||
type: 'done',
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import doneAction from './done';
|
||||
import planAction from './plan';
|
||||
import ActionRegistry from './registry';
|
||||
import webSearchAction from './webSearch';
|
||||
|
||||
ActionRegistry.register(webSearchAction);
|
||||
ActionRegistry.register(doneAction);
|
||||
ActionRegistry.register(planAction);
|
||||
|
||||
export { ActionRegistry };
|
||||
|
||||
26
src/lib/agents/search/researcher/actions/plan.ts
Normal file
26
src/lib/agents/search/researcher/actions/plan.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import z from 'zod';
|
||||
import { ResearchAction } from '../../types';
|
||||
|
||||
const schema = z.object({
|
||||
plan: z
|
||||
.string()
|
||||
.describe(
|
||||
'A concise natural-language plan in one short paragraph. Open with a short intent phrase (e.g., "Okay, the user wants to...", "Searching for...", "Looking into...") and lay out the steps you will take.',
|
||||
),
|
||||
});
|
||||
|
||||
const planAction: ResearchAction<typeof schema> = {
|
||||
name: '___plan',
|
||||
description:
|
||||
'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.',
|
||||
schema: schema,
|
||||
enabled: (_) => true,
|
||||
execute: async (input, _) => {
|
||||
return {
|
||||
type: 'reasoning',
|
||||
reasoning: input.plan,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default planAction;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Tool, ToolCall } from '@/lib/models/types';
|
||||
import {
|
||||
ActionConfig,
|
||||
ActionOutput,
|
||||
AdditionalConfig,
|
||||
ClassifierOutput,
|
||||
@@ -25,6 +25,18 @@ class ActionRegistry {
|
||||
);
|
||||
}
|
||||
|
||||
static getAvailableActionTools(config: {
|
||||
classification: ClassifierOutput;
|
||||
}): Tool[] {
|
||||
const availableActions = this.getAvailableActions(config);
|
||||
|
||||
return availableActions.map((action) => ({
|
||||
name: action.name,
|
||||
description: action.description,
|
||||
schema: action.schema,
|
||||
}));
|
||||
}
|
||||
|
||||
static getAvailableActionsDescriptions(config: {
|
||||
classification: ClassifierOutput;
|
||||
}): string {
|
||||
@@ -50,7 +62,7 @@ class ActionRegistry {
|
||||
}
|
||||
|
||||
static async executeAll(
|
||||
actions: ActionConfig[],
|
||||
actions: ToolCall[],
|
||||
additionalConfig: AdditionalConfig,
|
||||
): Promise<ActionOutput[]> {
|
||||
const results: ActionOutput[] = [];
|
||||
@@ -58,8 +70,8 @@ class ActionRegistry {
|
||||
await Promise.all(
|
||||
actions.map(async (actionConfig) => {
|
||||
const output = await this.execute(
|
||||
actionConfig.type,
|
||||
actionConfig.params,
|
||||
actionConfig.name,
|
||||
actionConfig.arguments,
|
||||
additionalConfig,
|
||||
);
|
||||
results.push(output);
|
||||
|
||||
@@ -11,22 +11,20 @@ const actionSchema = z.object({
|
||||
});
|
||||
|
||||
const actionDescription = `
|
||||
You have to use this action aggressively to find relevant information from the web to answer user queries. You can combine this action with other actions to gather comprehensive data. Always ensure that you provide accurate and up-to-date information by leveraging web search results.
|
||||
When this action is present, you must use it to obtain current information from the web.
|
||||
Use immediately after the ___plan call when you need information. Default to using this unless you already have everything needed to finish. Provide 1-3 short, SEO-friendly queries (keywords, not sentences) that cover the user ask. Always prefer current/contextual queries (e.g., include year for news).
|
||||
|
||||
### How to use:
|
||||
1. For speed search mode, you can use this action once. Make sure to cover all aspects of the user's query in that single search.
|
||||
2. If you're on quality mode, you'll get to use this action up to two times. Use the first search to gather general information, and the second search to fill in any gaps or get more specific details based on the initial findings.
|
||||
3. If you're set on quality mode, then you will get to use this action multiple times to gather more information. Use your judgment to decide when additional searches are necessary to provide a thorough and accurate response.
|
||||
For fast mode, you can only use this tool once so make sure to get all needed information in one go.
|
||||
For balanced and quality modes, you can use this tool multiple times as needed.
|
||||
|
||||
Input: An array of search queries. Make sure the queries are relevant to the user's request and cover different aspects if necessary. You can include a maximum of 3 queries. Make sure the queries are SEO friendly and not sentences rather keywords which can be used to search a search engine like Google, Bing, etc.
|
||||
In quality and balanced mode, first try to gather upper level information with broad queries, then use more specific queries based on what you find to find all information needed.
|
||||
`;
|
||||
|
||||
const webSearchAction: ResearchAction<typeof actionSchema> = {
|
||||
name: 'web_search',
|
||||
description: actionDescription,
|
||||
schema: actionSchema,
|
||||
enabled: (config) => true,
|
||||
enabled: (config) =>
|
||||
config.classification.classification.skipSearch === false,
|
||||
execute: async (input, _) => {
|
||||
let results: Chunk[] = [];
|
||||
|
||||
|
||||
@@ -1,43 +1,28 @@
|
||||
import z from 'zod';
|
||||
import {
|
||||
ActionConfig,
|
||||
ActionOutput,
|
||||
ResearcherInput,
|
||||
ResearcherOutput,
|
||||
} from '../types';
|
||||
import { ActionOutput, ResearcherInput, ResearcherOutput } from '../types';
|
||||
import { ActionRegistry } from './actions';
|
||||
import { getResearcherPrompt } from '@/lib/prompts/search/researcher';
|
||||
import SessionManager from '@/lib/session';
|
||||
import { ReasoningResearchBlock } from '@/lib/types';
|
||||
import { Message, ReasoningResearchBlock } from '@/lib/types';
|
||||
import formatChatHistoryAsString from '@/lib/utils/formatHistory';
|
||||
import { ToolCall } from '@/lib/models/types';
|
||||
|
||||
class Researcher {
|
||||
async research(
|
||||
session: SessionManager,
|
||||
input: ResearcherInput,
|
||||
): Promise<ResearcherOutput> {
|
||||
let findings: string = '';
|
||||
let actionOutput: ActionOutput[] = [];
|
||||
let maxIteration =
|
||||
input.config.mode === 'speed'
|
||||
? 1
|
||||
? 2
|
||||
: input.config.mode === 'balanced'
|
||||
? 3
|
||||
? 6
|
||||
: 25;
|
||||
|
||||
const availableActions = ActionRegistry.getAvailableActions({
|
||||
const availableTools = ActionRegistry.getAvailableActionTools({
|
||||
classification: input.classification,
|
||||
});
|
||||
|
||||
const schema = z.object({
|
||||
reasoning: z
|
||||
.string()
|
||||
.describe('The reasoning behind choosing the next action.'),
|
||||
action: z
|
||||
.union(availableActions.map((a) => a.schema))
|
||||
.describe('The action to be performed next.'),
|
||||
});
|
||||
|
||||
const availableActionsDescription =
|
||||
ActionRegistry.getAvailableActionsDescriptions({
|
||||
classification: input.classification,
|
||||
@@ -53,6 +38,18 @@ class Researcher {
|
||||
},
|
||||
});
|
||||
|
||||
const agentMessageHistory: Message[] = [
|
||||
{
|
||||
role: 'user',
|
||||
content: `
|
||||
<conversation>
|
||||
${formatChatHistoryAsString(input.chatHistory.slice(-10))}
|
||||
User: ${input.followUp} (Standalone question: ${input.classification.standaloneFollowUp})
|
||||
</conversation>
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
for (let i = 0; i < maxIteration; i++) {
|
||||
const researcherPrompt = getResearcherPrompt(
|
||||
availableActionsDescription,
|
||||
@@ -61,27 +58,15 @@ class Researcher {
|
||||
maxIteration,
|
||||
);
|
||||
|
||||
const actionStream = input.config.llm.streamObject<typeof schema>({
|
||||
const actionStream = input.config.llm.streamText({
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: researcherPrompt,
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: `
|
||||
<conversation>
|
||||
${formatChatHistoryAsString(input.chatHistory.slice(-10))}
|
||||
User: ${input.followUp} (Standalone question: ${input.classification.standaloneFollowUp})
|
||||
</conversation>
|
||||
|
||||
<previous_actions>
|
||||
${findings}
|
||||
</previous_actions>
|
||||
`,
|
||||
},
|
||||
...agentMessageHistory,
|
||||
],
|
||||
schema,
|
||||
tools: availableTools,
|
||||
});
|
||||
|
||||
const block = session.getBlock(researchBlockId);
|
||||
@@ -89,22 +74,26 @@ class Researcher {
|
||||
let reasoningEmitted = false;
|
||||
let reasoningId = crypto.randomUUID();
|
||||
|
||||
let finalActionRes: any;
|
||||
let finalToolCalls: ToolCall[] = [];
|
||||
|
||||
for await (const partialRes of actionStream) {
|
||||
try {
|
||||
if (partialRes.toolCallChunk.length > 0) {
|
||||
partialRes.toolCallChunk.forEach((tc) => {
|
||||
if (
|
||||
partialRes.reasoning &&
|
||||
tc.name === '___plan' &&
|
||||
tc.arguments['plan'] &&
|
||||
!reasoningEmitted &&
|
||||
block &&
|
||||
block.type === 'research'
|
||||
) {
|
||||
reasoningEmitted = true;
|
||||
|
||||
block.data.subSteps.push({
|
||||
id: reasoningId,
|
||||
type: 'reasoning',
|
||||
reasoning: partialRes.reasoning,
|
||||
reasoning: tc.arguments['plan'],
|
||||
});
|
||||
|
||||
session.updateBlock(researchBlockId, [
|
||||
{
|
||||
op: 'replace',
|
||||
@@ -113,7 +102,8 @@ class Researcher {
|
||||
},
|
||||
]);
|
||||
} else if (
|
||||
partialRes.reasoning &&
|
||||
tc.name === '___plan' &&
|
||||
tc.arguments['plan'] &&
|
||||
reasoningEmitted &&
|
||||
block &&
|
||||
block.type === 'research'
|
||||
@@ -121,11 +111,12 @@ class Researcher {
|
||||
const subStepIndex = block.data.subSteps.findIndex(
|
||||
(step: any) => step.id === reasoningId,
|
||||
);
|
||||
|
||||
if (subStepIndex !== -1) {
|
||||
const subStep = block.data.subSteps[
|
||||
subStepIndex
|
||||
] as ReasoningResearchBlock;
|
||||
subStep.reasoning = partialRes.reasoning;
|
||||
subStep.reasoning = tc.arguments['plan'];
|
||||
session.updateBlock(researchBlockId, [
|
||||
{
|
||||
op: 'replace',
|
||||
@@ -136,56 +127,47 @@ class Researcher {
|
||||
}
|
||||
}
|
||||
|
||||
finalActionRes = partialRes;
|
||||
} catch (e) {
|
||||
// nothing
|
||||
const existingIndex = finalToolCalls.findIndex(
|
||||
(ftc) => ftc.id === tc.id,
|
||||
);
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
finalToolCalls[existingIndex].arguments = tc.arguments;
|
||||
} else {
|
||||
finalToolCalls.push(tc);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (finalActionRes.action.type === 'done') {
|
||||
if (finalToolCalls.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
const actionConfig: ActionConfig = {
|
||||
type: finalActionRes.action.type as string,
|
||||
params: finalActionRes.action,
|
||||
};
|
||||
if (finalToolCalls[finalToolCalls.length - 1].name === 'done') {
|
||||
break;
|
||||
}
|
||||
|
||||
const queries = actionConfig.params.queries || [];
|
||||
if (block && block.type === 'research') {
|
||||
agentMessageHistory.push({
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
tool_calls: finalToolCalls,
|
||||
});
|
||||
|
||||
const searchCalls = finalToolCalls.filter(
|
||||
(tc) =>
|
||||
tc.name === 'web_search' ||
|
||||
tc.name === 'academic_search' ||
|
||||
tc.name === 'discussion_search',
|
||||
);
|
||||
|
||||
if (searchCalls.length > 0 && block && block.type === 'research') {
|
||||
block.data.subSteps.push({
|
||||
id: crypto.randomUUID(),
|
||||
type: 'searching',
|
||||
searching: queries,
|
||||
searching: searchCalls.map((sc) => sc.arguments.queries).flat(),
|
||||
});
|
||||
session.updateBlock(researchBlockId, [
|
||||
{ op: 'replace', path: '/data/subSteps', value: block.data.subSteps },
|
||||
]);
|
||||
}
|
||||
|
||||
findings += `\n---\nIteration ${i + 1}:\n`;
|
||||
findings += 'Reasoning: ' + finalActionRes.reasoning + '\n';
|
||||
findings += `Executing Action: ${actionConfig.type} with params ${JSON.stringify(actionConfig.params)}\n`;
|
||||
|
||||
const actionResult = await ActionRegistry.execute(
|
||||
actionConfig.type,
|
||||
actionConfig.params,
|
||||
{
|
||||
llm: input.config.llm,
|
||||
embedding: input.config.embedding,
|
||||
session: session,
|
||||
},
|
||||
);
|
||||
|
||||
actionOutput.push(actionResult);
|
||||
|
||||
if (actionResult.type === 'search_results') {
|
||||
if (block && block.type === 'research') {
|
||||
block.data.subSteps.push({
|
||||
id: crypto.randomUUID(),
|
||||
type: 'reading',
|
||||
reading: actionResult.results,
|
||||
});
|
||||
session.updateBlock(researchBlockId, [
|
||||
{
|
||||
op: 'replace',
|
||||
@@ -195,15 +177,42 @@ class Researcher {
|
||||
]);
|
||||
}
|
||||
|
||||
findings += actionResult.results
|
||||
.map(
|
||||
(r) =>
|
||||
`Title: ${r.metadata.title}\nURL: ${r.metadata.url}\nContent: ${r.content}\n`,
|
||||
)
|
||||
.join('\n');
|
||||
}
|
||||
const actionResults = await ActionRegistry.executeAll(finalToolCalls, {
|
||||
llm: input.config.llm,
|
||||
embedding: input.config.embedding,
|
||||
session: session,
|
||||
});
|
||||
|
||||
findings += '\n---------\n';
|
||||
actionOutput.push(...actionResults);
|
||||
|
||||
actionResults.forEach((action, i) => {
|
||||
agentMessageHistory.push({
|
||||
role: 'tool',
|
||||
id: finalToolCalls[i].id,
|
||||
name: finalToolCalls[i].name,
|
||||
content: JSON.stringify(action),
|
||||
});
|
||||
});
|
||||
|
||||
const searchResults = actionResults.filter(
|
||||
(a) => a.type === 'search_results',
|
||||
);
|
||||
|
||||
if (searchResults.length > 0 && block && block.type === 'research') {
|
||||
block.data.subSteps.push({
|
||||
id: crypto.randomUUID(),
|
||||
type: 'reading',
|
||||
reading: searchResults.flatMap((a) => a.results),
|
||||
});
|
||||
|
||||
session.updateBlock(researchBlockId, [
|
||||
{
|
||||
op: 'replace',
|
||||
path: '/data/subSteps',
|
||||
value: block.data.subSteps,
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
const searchResults = actionOutput.filter(
|
||||
@@ -212,12 +221,7 @@ class Researcher {
|
||||
|
||||
session.emit('data', {
|
||||
type: 'sources',
|
||||
data: searchResults
|
||||
.flatMap((a) => a.results)
|
||||
.map((r) => ({
|
||||
content: r.content,
|
||||
metadata: r.metadata,
|
||||
})),
|
||||
data: searchResults.flatMap((a) => a.results),
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -84,7 +84,15 @@ export type DoneActionOutput = {
|
||||
type: 'done';
|
||||
};
|
||||
|
||||
export type ActionOutput = SearchActionOutput | DoneActionOutput;
|
||||
export type ReasoningResearchAction = {
|
||||
type: 'reasoning';
|
||||
reasoning: string;
|
||||
};
|
||||
|
||||
export type ActionOutput =
|
||||
| SearchActionOutput
|
||||
| DoneActionOutput
|
||||
| ReasoningResearchAction;
|
||||
|
||||
export interface ResearchAction<
|
||||
TSchema extends z.ZodObject<any> = z.ZodObject<any>,
|
||||
@@ -98,8 +106,3 @@ export interface ResearchAction<
|
||||
additionalConfig: AdditionalConfig,
|
||||
) => Promise<ActionOutput>;
|
||||
}
|
||||
|
||||
export type ActionConfig = {
|
||||
type: string;
|
||||
params: Record<string, any>;
|
||||
};
|
||||
|
||||
@@ -7,8 +7,10 @@ import {
|
||||
GenerateTextOutput,
|
||||
StreamTextOutput,
|
||||
} from '../../types';
|
||||
import { Ollama, Tool as OllamaTool } from 'ollama';
|
||||
import { Ollama, Tool as OllamaTool, Message as OllamaMessage } from 'ollama';
|
||||
import { parse } from 'partial-json';
|
||||
import crypto from 'crypto';
|
||||
import { Message } from '@/lib/types';
|
||||
|
||||
type OllamaConfig = {
|
||||
baseURL: string;
|
||||
@@ -35,6 +37,33 @@ class OllamaLLM extends BaseLLM<OllamaConfig> {
|
||||
});
|
||||
}
|
||||
|
||||
convertToOllamaMessages(messages: Message[]): OllamaMessage[] {
|
||||
return messages.map((msg) => {
|
||||
if (msg.role === 'tool') {
|
||||
return {
|
||||
role: 'tool',
|
||||
tool_name: msg.name,
|
||||
content: msg.content,
|
||||
} as OllamaMessage;
|
||||
} else if (msg.role === 'assistant') {
|
||||
return {
|
||||
role: 'assistant',
|
||||
content: msg.content,
|
||||
tool_calls:
|
||||
msg.tool_calls?.map((tc, i) => ({
|
||||
function: {
|
||||
index: i,
|
||||
name: tc.name,
|
||||
arguments: tc.arguments,
|
||||
},
|
||||
})) || [],
|
||||
};
|
||||
}
|
||||
|
||||
return msg;
|
||||
});
|
||||
}
|
||||
|
||||
async generateText(input: GenerateTextInput): Promise<GenerateTextOutput> {
|
||||
const ollamaTools: OllamaTool[] = [];
|
||||
|
||||
@@ -51,8 +80,11 @@ class OllamaLLM extends BaseLLM<OllamaConfig> {
|
||||
|
||||
const res = await this.ollamaClient.chat({
|
||||
model: this.config.model,
|
||||
messages: input.messages,
|
||||
messages: this.convertToOllamaMessages(input.messages),
|
||||
tools: ollamaTools.length > 0 ? ollamaTools : undefined,
|
||||
...(reasoningModels.find((m) => this.config.model.includes(m))
|
||||
? { think: false }
|
||||
: {}),
|
||||
options: {
|
||||
top_p: input.options?.topP ?? this.config.options?.topP,
|
||||
temperature:
|
||||
@@ -74,6 +106,7 @@ class OllamaLLM extends BaseLLM<OllamaConfig> {
|
||||
content: res.message.content,
|
||||
toolCalls:
|
||||
res.message.tool_calls?.map((tc) => ({
|
||||
id: crypto.randomUUID(),
|
||||
name: tc.function.name,
|
||||
arguments: tc.function.arguments,
|
||||
})) || [],
|
||||
@@ -101,8 +134,11 @@ class OllamaLLM extends BaseLLM<OllamaConfig> {
|
||||
|
||||
const stream = await this.ollamaClient.chat({
|
||||
model: this.config.model,
|
||||
messages: input.messages,
|
||||
messages: this.convertToOllamaMessages(input.messages),
|
||||
stream: true,
|
||||
...(reasoningModels.find((m) => this.config.model.includes(m))
|
||||
? { think: false }
|
||||
: {}),
|
||||
tools: ollamaTools.length > 0 ? ollamaTools : undefined,
|
||||
options: {
|
||||
top_p: input.options?.topP ?? this.config.options?.topP,
|
||||
@@ -126,6 +162,7 @@ class OllamaLLM extends BaseLLM<OllamaConfig> {
|
||||
contentChunk: chunk.message.content,
|
||||
toolCallChunk:
|
||||
chunk.message.tool_calls?.map((tc) => ({
|
||||
id: crypto.randomUUID(),
|
||||
name: tc.function.name,
|
||||
arguments: tc.function.arguments,
|
||||
})) || [],
|
||||
@@ -140,7 +177,7 @@ class OllamaLLM extends BaseLLM<OllamaConfig> {
|
||||
async generateObject<T>(input: GenerateObjectInput): Promise<T> {
|
||||
const response = await this.ollamaClient.chat({
|
||||
model: this.config.model,
|
||||
messages: input.messages,
|
||||
messages: this.convertToOllamaMessages(input.messages),
|
||||
format: z.toJSONSchema(input.schema),
|
||||
...(reasoningModels.find((m) => this.config.model.includes(m))
|
||||
? { think: false }
|
||||
@@ -173,7 +210,7 @@ class OllamaLLM extends BaseLLM<OllamaConfig> {
|
||||
|
||||
const stream = await this.ollamaClient.chat({
|
||||
model: this.config.model,
|
||||
messages: input.messages,
|
||||
messages: this.convertToOllamaMessages(input.messages),
|
||||
format: z.toJSONSchema(input.schema),
|
||||
stream: true,
|
||||
...(reasoningModels.find((m) => this.config.model.includes(m))
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import { parse } from 'partial-json';
|
||||
import z from 'zod';
|
||||
import {
|
||||
ChatCompletionAssistantMessageParam,
|
||||
ChatCompletionMessageParam,
|
||||
ChatCompletionTool,
|
||||
ChatCompletionToolMessageParam,
|
||||
@@ -45,6 +46,22 @@ class OpenAILLM extends BaseLLM<OpenAIConfig> {
|
||||
tool_call_id: msg.id,
|
||||
content: msg.content,
|
||||
} as ChatCompletionToolMessageParam;
|
||||
} else if (msg.role === 'assistant') {
|
||||
return {
|
||||
role: 'assistant',
|
||||
content: msg.content,
|
||||
...(msg.tool_calls &&
|
||||
msg.tool_calls.length > 0 && {
|
||||
tool_calls: msg.tool_calls?.map((tc) => ({
|
||||
id: tc.id,
|
||||
type: 'function',
|
||||
function: {
|
||||
name: tc.name,
|
||||
arguments: JSON.stringify(tc.arguments),
|
||||
},
|
||||
})),
|
||||
}),
|
||||
} as ChatCompletionAssistantMessageParam;
|
||||
}
|
||||
|
||||
return msg;
|
||||
@@ -178,7 +195,7 @@ class OpenAILLM extends BaseLLM<OpenAIConfig> {
|
||||
|
||||
async generateObject<T>(input: GenerateObjectInput): Promise<T> {
|
||||
const response = await this.openAIClient.chat.completions.parse({
|
||||
messages: input.messages,
|
||||
messages: this.convertToOpenAIMessages(input.messages),
|
||||
model: this.config.model,
|
||||
temperature:
|
||||
input.options?.temperature ?? this.config.options?.temperature ?? 1.0,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import z from 'zod';
|
||||
import { ChatTurnMessage } from '../types';
|
||||
import { Message } from '../types';
|
||||
|
||||
type Model = {
|
||||
name: string;
|
||||
@@ -44,13 +44,13 @@ type Tool = {
|
||||
};
|
||||
|
||||
type ToolCall = {
|
||||
id?: string;
|
||||
id: string;
|
||||
name: string;
|
||||
arguments: Record<string, any>;
|
||||
};
|
||||
|
||||
type GenerateTextInput = {
|
||||
messages: ChatTurnMessage[];
|
||||
messages: Message[];
|
||||
tools?: Tool[];
|
||||
options?: GenerateOptions;
|
||||
};
|
||||
@@ -63,14 +63,14 @@ type GenerateTextOutput = {
|
||||
|
||||
type StreamTextOutput = {
|
||||
contentChunk: string;
|
||||
toolCallChunk: Partial<ToolCall>[];
|
||||
toolCallChunk: ToolCall[];
|
||||
additionalInfo?: Record<string, any>;
|
||||
done?: boolean;
|
||||
};
|
||||
|
||||
type GenerateObjectInput = {
|
||||
schema: z.ZodTypeAny;
|
||||
messages: ChatTurnMessage[];
|
||||
messages: Message[];
|
||||
options?: GenerateOptions;
|
||||
};
|
||||
|
||||
|
||||
@@ -37,14 +37,8 @@ NEVER ASSUME - your knowledge may be outdated. When a user asks about something
|
||||
</core_principle>
|
||||
|
||||
<reasoning_approach>
|
||||
|
||||
Think like a human would. Your reasoning should be natural and show:
|
||||
- What the user is asking for
|
||||
- What you need to find out or do
|
||||
- Your plan to accomplish it
|
||||
|
||||
Keep it to 2-3 natural sentences.
|
||||
|
||||
You never speak your reasoning to the user. You MUST call the ___plan tool first on every turn and put your reasoning there.
|
||||
The plan must be 2-4 concise sentences, starting with "Okay, the user wants to..." and outlining the steps you will take next.
|
||||
</reasoning_approach>
|
||||
|
||||
<examples>
|
||||
@@ -239,17 +233,13 @@ Actions: web_search ["best Italian restaurant near me", "top rated Italian resta
|
||||
|
||||
</mistakes_to_avoid>
|
||||
|
||||
<output_format>
|
||||
Reasoning should be 2-3 natural sentences showing your thought process and plan. Then select and configure the appropriate action(s).
|
||||
|
||||
Always respond in the following JSON format and never deviate from it or output any extra text:
|
||||
{
|
||||
"reasoning": "<your reasoning here>",
|
||||
"actions": [
|
||||
{"type": "<action_type>", "param1": "value1", "...": "..."},
|
||||
...
|
||||
]
|
||||
}
|
||||
</output_format>
|
||||
<response_protocol>
|
||||
- NEVER output normal text to the user. ONLY call tools.
|
||||
- Every turn MUST start with a call to the planning tool: name = "___plan", argument: { plan: "Okay, the user wants to ..." + concise 2-4 sentence plan }.
|
||||
- Immediately after ___plan, if any information is missing, call \`web_search\` with up to 3 targeted queries. Default to searching unless you are certain you have enough.
|
||||
- Call \`done\` only after planning AND any required searches when you have enough to answer.
|
||||
- Do not invent tools. Do not return JSON. Do not echo the plan outside of the tool call.
|
||||
- If nothing else is needed after planning, call \`done\` immediately after the plan.
|
||||
</response_protocol>
|
||||
`;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
export type ChatTurnMessage = {
|
||||
role: 'user' | 'assistant' | 'system';
|
||||
import { ToolCall } from './models/types';
|
||||
|
||||
export type SystemMessage = {
|
||||
role: 'system';
|
||||
content: string;
|
||||
};
|
||||
|
||||
export type AssistantMessage = {
|
||||
role: 'assistant';
|
||||
content: string;
|
||||
tool_calls?: ToolCall[];
|
||||
};
|
||||
|
||||
export type UserMessage = {
|
||||
role: 'user';
|
||||
content: string;
|
||||
};
|
||||
|
||||
@@ -10,7 +23,13 @@ export type ToolMessage = {
|
||||
content: string;
|
||||
};
|
||||
|
||||
export type Message = ChatTurnMessage | ToolMessage;
|
||||
export type ChatTurnMessage = UserMessage | AssistantMessage;
|
||||
|
||||
export type Message =
|
||||
| UserMessage
|
||||
| AssistantMessage
|
||||
| SystemMessage
|
||||
| ToolMessage;
|
||||
|
||||
export type Chunk = {
|
||||
content: string;
|
||||
|
||||
Reference in New Issue
Block a user