mirror of
https://github.com/ItzCrazyKns/Perplexica.git
synced 2025-12-14 15:48:15 +00:00
234 lines
6.3 KiB
TypeScript
234 lines
6.3 KiB
TypeScript
import { ActionOutput, ResearcherInput, ResearcherOutput } from '../types';
|
|
import { ActionRegistry } from './actions';
|
|
import { getResearcherPrompt } from '@/lib/prompts/search/researcher';
|
|
import SessionManager from '@/lib/session';
|
|
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 actionOutput: ActionOutput[] = [];
|
|
let maxIteration =
|
|
input.config.mode === 'speed'
|
|
? 2
|
|
: input.config.mode === 'balanced'
|
|
? 6
|
|
: 25;
|
|
|
|
const availableTools = ActionRegistry.getAvailableActionTools({
|
|
classification: input.classification,
|
|
});
|
|
|
|
const availableActionsDescription =
|
|
ActionRegistry.getAvailableActionsDescriptions({
|
|
classification: input.classification,
|
|
});
|
|
|
|
const researchBlockId = crypto.randomUUID();
|
|
|
|
session.emitBlock({
|
|
id: researchBlockId,
|
|
type: 'research',
|
|
data: {
|
|
subSteps: [],
|
|
},
|
|
});
|
|
|
|
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,
|
|
input.config.mode,
|
|
i,
|
|
maxIteration,
|
|
);
|
|
|
|
const actionStream = input.config.llm.streamText({
|
|
messages: [
|
|
{
|
|
role: 'system',
|
|
content: researcherPrompt,
|
|
},
|
|
...agentMessageHistory,
|
|
],
|
|
tools: availableTools,
|
|
});
|
|
|
|
const block = session.getBlock(researchBlockId);
|
|
|
|
let reasoningEmitted = false;
|
|
let reasoningId = crypto.randomUUID();
|
|
|
|
let finalToolCalls: ToolCall[] = [];
|
|
|
|
for await (const partialRes of actionStream) {
|
|
if (partialRes.toolCallChunk.length > 0) {
|
|
partialRes.toolCallChunk.forEach((tc) => {
|
|
if (
|
|
tc.name === '___plan' &&
|
|
tc.arguments['plan'] &&
|
|
!reasoningEmitted &&
|
|
block &&
|
|
block.type === 'research'
|
|
) {
|
|
reasoningEmitted = true;
|
|
|
|
block.data.subSteps.push({
|
|
id: reasoningId,
|
|
type: 'reasoning',
|
|
reasoning: tc.arguments['plan'],
|
|
});
|
|
|
|
session.updateBlock(researchBlockId, [
|
|
{
|
|
op: 'replace',
|
|
path: '/data/subSteps',
|
|
value: block.data.subSteps,
|
|
},
|
|
]);
|
|
} else if (
|
|
tc.name === '___plan' &&
|
|
tc.arguments['plan'] &&
|
|
reasoningEmitted &&
|
|
block &&
|
|
block.type === 'research'
|
|
) {
|
|
const subStepIndex = block.data.subSteps.findIndex(
|
|
(step: any) => step.id === reasoningId,
|
|
);
|
|
|
|
if (subStepIndex !== -1) {
|
|
const subStep = block.data.subSteps[
|
|
subStepIndex
|
|
] as ReasoningResearchBlock;
|
|
subStep.reasoning = tc.arguments['plan'];
|
|
session.updateBlock(researchBlockId, [
|
|
{
|
|
op: 'replace',
|
|
path: '/data/subSteps',
|
|
value: block.data.subSteps,
|
|
},
|
|
]);
|
|
}
|
|
}
|
|
|
|
const existingIndex = finalToolCalls.findIndex(
|
|
(ftc) => ftc.id === tc.id,
|
|
);
|
|
|
|
if (existingIndex !== -1) {
|
|
finalToolCalls[existingIndex].arguments = tc.arguments;
|
|
} else {
|
|
finalToolCalls.push(tc);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
if (finalToolCalls.length === 0) {
|
|
break;
|
|
}
|
|
|
|
if (finalToolCalls[finalToolCalls.length - 1].name === 'done') {
|
|
break;
|
|
}
|
|
|
|
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: searchCalls.map((sc) => sc.arguments.queries).flat(),
|
|
});
|
|
|
|
session.updateBlock(researchBlockId, [
|
|
{
|
|
op: 'replace',
|
|
path: '/data/subSteps',
|
|
value: block.data.subSteps,
|
|
},
|
|
]);
|
|
}
|
|
|
|
const actionResults = await ActionRegistry.executeAll(finalToolCalls, {
|
|
llm: input.config.llm,
|
|
embedding: input.config.embedding,
|
|
session: session,
|
|
});
|
|
|
|
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(
|
|
(a) => a.type === 'search_results',
|
|
);
|
|
|
|
session.emit('data', {
|
|
type: 'sources',
|
|
data: searchResults.flatMap((a) => a.results),
|
|
});
|
|
|
|
return {
|
|
findings: actionOutput,
|
|
};
|
|
}
|
|
}
|
|
|
|
export default Researcher;
|