mirror of
https://github.com/jina-ai/node-DeepResearch.git
synced 2025-12-26 06:28:56 +08:00
723 lines
25 KiB
TypeScript
723 lines
25 KiB
TypeScript
import {GoogleGenerativeAI, SchemaType} from "@google/generative-ai";
|
|
import {readUrl} from "./tools/read";
|
|
import fs from 'fs/promises';
|
|
import {SafeSearchType, search as duckSearch} from "duck-duck-scrape";
|
|
import {braveSearch} from "./tools/brave-search";
|
|
import {rewriteQuery} from "./tools/query-rewriter";
|
|
import {dedupQueries} from "./tools/dedup";
|
|
import {evaluateAnswer} from "./tools/evaluator";
|
|
import {analyzeSteps} from "./tools/error-analyzer";
|
|
import {
|
|
GEMINI_API_KEY,
|
|
JINA_API_KEY,
|
|
SEARCH_PROVIDER,
|
|
STEP_SLEEP,
|
|
modelConfigs,
|
|
DEFAULT_BUDGET_SPLIT_RATIO
|
|
} from "./config";
|
|
import {TokenTracker} from "./utils/token-tracker";
|
|
import {ActionTracker} from "./utils/action-tracker";
|
|
import {StepAction, SchemaProperty, ResponseSchema, AnswerAction} from "./types";
|
|
import {TrackerContext} from "./types";
|
|
|
|
async function sleep(ms: number) {
|
|
const seconds = Math.ceil(ms / 1000);
|
|
console.log(`Waiting ${seconds}s...`);
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
}
|
|
|
|
function getSchema(allowReflect: boolean, allowRead: boolean, allowAnswer: boolean, allowSearch: boolean): ResponseSchema {
|
|
const actions: string[] = [];
|
|
const properties: Record<string, SchemaProperty> = {
|
|
action: {
|
|
type: SchemaType.STRING,
|
|
enum: actions,
|
|
description: "Must match exactly one action type"
|
|
},
|
|
thoughts: {
|
|
type: SchemaType.STRING,
|
|
description: "Explain why choose this action, what's the thought process behind choosing this action"
|
|
}
|
|
};
|
|
|
|
if (allowSearch) {
|
|
actions.push("search");
|
|
properties.searchQuery = {
|
|
type: SchemaType.STRING,
|
|
description: "Only required when choosing 'search' action, must be a short, keyword-based query that BM25, tf-idf based search engines can understand."
|
|
};
|
|
}
|
|
|
|
if (allowAnswer) {
|
|
actions.push("answer");
|
|
properties.answer = {
|
|
type: SchemaType.STRING,
|
|
description: "Only required when choosing 'answer' action, must be the final answer in natural language"
|
|
};
|
|
properties.references = {
|
|
type: SchemaType.ARRAY,
|
|
items: {
|
|
type: SchemaType.OBJECT,
|
|
properties: {
|
|
exactQuote: {
|
|
type: SchemaType.STRING,
|
|
description: "Exact relevant quote from the document"
|
|
},
|
|
url: {
|
|
type: SchemaType.STRING,
|
|
description: "URL of the document; must be directly from the context"
|
|
}
|
|
},
|
|
required: ["exactQuote", "url"]
|
|
},
|
|
description: "Must be an array of references that support the answer, each reference must contain an exact quote and the URL of the document"
|
|
};
|
|
}
|
|
|
|
if (allowReflect) {
|
|
actions.push("reflect");
|
|
properties.questionsToAnswer = {
|
|
type: SchemaType.ARRAY,
|
|
items: {
|
|
type: SchemaType.STRING,
|
|
description: "each question must be a single line, concise and clear. not composite or compound, less than 20 words."
|
|
},
|
|
description: "List of most important questions to fill the knowledge gaps of finding the answer to the original question",
|
|
maxItems: 2
|
|
};
|
|
}
|
|
|
|
if (allowRead) {
|
|
actions.push("visit");
|
|
properties.URLTargets = {
|
|
type: SchemaType.ARRAY,
|
|
items: {
|
|
type: SchemaType.STRING
|
|
},
|
|
maxItems: 2,
|
|
description: "Must be an array of URLs, choose up the most relevant 2 URLs to visit"
|
|
};
|
|
}
|
|
|
|
// Update the enum values after collecting all actions
|
|
properties.action.enum = actions;
|
|
|
|
return {
|
|
type: SchemaType.OBJECT,
|
|
properties,
|
|
required: ["action", "thoughts"]
|
|
};
|
|
}
|
|
|
|
function getPrompt(
|
|
question: string,
|
|
context?: string[],
|
|
allQuestions?: string[],
|
|
allowReflect: boolean = true,
|
|
allowAnswer: boolean = true,
|
|
allowRead: boolean = true,
|
|
allowSearch: boolean = true,
|
|
badContext?: { question: string, answer: string, evaluation: string, recap: string; blame: string; improvement: string; }[],
|
|
knowledge?: { question: string; answer: string; }[],
|
|
allURLs?: Record<string, string>,
|
|
beastMode?: boolean
|
|
): string {
|
|
const sections: string[] = [];
|
|
|
|
// Add header section
|
|
sections.push(`Current date: ${new Date().toUTCString()}
|
|
|
|
You are an advanced AI research analyst specializing in multi-step reasoning. Using your training data and prior lessons learned, answer the following question with absolute certainty:
|
|
|
|
## Question
|
|
${question}`);
|
|
|
|
// Add context section if exists
|
|
if (context?.length) {
|
|
sections.push(`## Context
|
|
You have conducted the following actions:
|
|
|
|
${context.join('\n')}`);
|
|
}
|
|
|
|
// Add knowledge section if exists
|
|
if (knowledge?.length) {
|
|
const knowledgeItems = knowledge
|
|
.map((k, i) => `### Knowledge ${i + 1}: ${k.question}\n${k.answer}`)
|
|
.join('\n\n');
|
|
|
|
sections.push(`## Knowledge
|
|
You have successfully gathered some knowledge which might be useful for answering the original question. Here is the knowledge you have gathered so far
|
|
|
|
${knowledgeItems}`);
|
|
}
|
|
|
|
// Add bad context section if exists
|
|
if (badContext?.length) {
|
|
const attempts = badContext
|
|
.map((c, i) => `### Attempt ${i + 1}
|
|
- Question: ${c.question}
|
|
- Answer: ${c.answer}
|
|
- Reject Reason: ${c.evaluation}
|
|
- Actions Recap: ${c.recap}
|
|
- Actions Blame: ${c.blame}`)
|
|
.join('\n\n');
|
|
|
|
const learnedStrategy = badContext.map(c => c.improvement).join('\n');
|
|
|
|
sections.push(`## Unsuccessful Attempts
|
|
Your have tried the following actions but failed to find the answer to the question.
|
|
|
|
${attempts}
|
|
|
|
## Learned Strategy
|
|
${learnedStrategy}
|
|
`);
|
|
}
|
|
|
|
// Build actions section
|
|
const actions: string[] = [];
|
|
|
|
if (allURLs && Object.keys(allURLs).length > 0 && allowRead) {
|
|
const urlList = Object.entries(allURLs)
|
|
.map(([url, desc]) => ` + "${url}": "${desc}"`)
|
|
.join('\n');
|
|
|
|
actions.push(`**visit**:
|
|
- Visit any URLs from below to gather external knowledge, choose the most relevant URLs that might contain the answer
|
|
${urlList}
|
|
- When you have enough search result in the context and want to deep dive into specific URLs
|
|
- It allows you to access the full content behind any URLs`);
|
|
}
|
|
|
|
if (allowSearch) {
|
|
actions.push(`**search**:
|
|
- Query external sources using a public search engine
|
|
- Focus on solving one specific aspect of the question
|
|
- Only give keywords search query, not full sentences`);
|
|
}
|
|
|
|
if (allowAnswer) {
|
|
actions.push(`**answer**:
|
|
- Provide final response only when 100% certain
|
|
- Responses must be definitive (no ambiguity, uncertainty, or disclaimers)${allowReflect ? '\n- If doubts remain, use "reflect" instead' : ''}`);
|
|
}
|
|
|
|
if (beastMode) {
|
|
actions.push(`**answer**:
|
|
- You have gathered enough information to answer the question; they may not be perfect, but this is your very last chance to answer the question.
|
|
- Try the best of the best reasoning ability, investigate every details in the context and provide the best answer you can think of.
|
|
- When uncertain, educated guess is allowed and encouraged, but make sure it is based on the context and knowledge you have gathered.
|
|
- Responses must be definitive (no ambiguity, uncertainty, or disclaimers`);
|
|
}
|
|
|
|
if (allowReflect) {
|
|
actions.push(`**reflect**:
|
|
- Perform critical analysis through hypothetical scenarios or systematic breakdowns
|
|
- Identify knowledge gaps and formulate essential clarifying questions
|
|
- Questions must be:
|
|
- Original (not variations of existing questions)
|
|
- Focused on single concepts
|
|
- Under 20 words
|
|
- Non-compound/non-complex`);
|
|
}
|
|
|
|
sections.push(`## Actions
|
|
|
|
Based on the current context, you must choose one of the following actions:
|
|
|
|
${actions.join('\n\n')}`);
|
|
|
|
// Add footer
|
|
sections.push(`Respond exclusively in valid JSON format matching exact JSON schema.
|
|
|
|
Critical Requirements:
|
|
- Include ONLY ONE action type
|
|
- Never add unsupported keys
|
|
- Exclude all non-JSON text, markdown, or explanations
|
|
- Maintain strict JSON syntax`);
|
|
|
|
return sections.join('\n\n');
|
|
}
|
|
|
|
const allContext: StepAction[] = []; // all steps in the current session, including those leads to wrong results
|
|
|
|
function updateContext(step: any) {
|
|
allContext.push(step)
|
|
}
|
|
|
|
function removeAllLineBreaks(text: string) {
|
|
return text.replace(/(\r\n|\n|\r)/gm, " ");
|
|
}
|
|
|
|
export async function getResponse(question: string, tokenBudget: number = 1_000_000,
|
|
maxBadAttempts: number = 3,
|
|
existingContext?: Partial<TrackerContext>,
|
|
parentKnowledge: Array<{ question: string, answer: string, type: string }> = [],
|
|
budgetSplitRatio: number = DEFAULT_BUDGET_SPLIT_RATIO,
|
|
recursionLevel: number = 0): Promise<{ result: StepAction; context: TrackerContext }> {
|
|
const context: TrackerContext = {
|
|
tokenTracker: existingContext?.tokenTracker || new TokenTracker(tokenBudget),
|
|
actionTracker: existingContext?.actionTracker || new ActionTracker()
|
|
};
|
|
context.actionTracker.trackAction({gaps: [question], totalStep: 0, badAttempts: 0});
|
|
let step = 0;
|
|
let totalStep = 0;
|
|
let badAttempts = 0;
|
|
const gaps: string[] = [question]; // All questions to be answered including the orginal question
|
|
const allQuestions = [question];
|
|
const allKeywords = [];
|
|
const allKnowledge = [...parentKnowledge]; // knowledge are intermedidate questions that are answered
|
|
const badContext = [];
|
|
let diaryContext = [];
|
|
let allowAnswer = true;
|
|
let allowSearch = true;
|
|
let allowRead = true;
|
|
let allowReflect = true;
|
|
let prompt = '';
|
|
let thisStep: StepAction = {action: 'answer', answer: '', references: [], thoughts: ''};
|
|
let isAnswered = false;
|
|
|
|
const allURLs: Record<string, string> = {};
|
|
const visitedURLs: string[] = [];
|
|
while (context.tokenTracker.getTotalUsage() < tokenBudget && badAttempts <= maxBadAttempts) {
|
|
// add 1s delay to avoid rate limiting
|
|
await sleep(STEP_SLEEP);
|
|
step++;
|
|
totalStep++;
|
|
context.actionTracker.trackAction({totalStep, thisStep, gaps, badAttempts});
|
|
const budgetPercentage = (context.tokenTracker.getTotalUsage() / tokenBudget * 100).toFixed(2);
|
|
console.log(`| Recursion ${recursionLevel} | Step ${totalStep} | Budget used ${budgetPercentage}% |`);
|
|
console.log('Gaps:', gaps);
|
|
allowReflect = allowReflect && (gaps.length <= 1);
|
|
const currentQuestion = gaps.length > 0 ? gaps.shift()! : question;
|
|
// update all urls with buildURLMap
|
|
allowRead = allowRead && (Object.keys(allURLs).length > 0);
|
|
allowSearch = allowSearch && (Object.keys(allURLs).length < 20); // disable search when too many urls already
|
|
|
|
// generate prompt for this step
|
|
prompt = getPrompt(
|
|
currentQuestion,
|
|
diaryContext,
|
|
allQuestions,
|
|
allowReflect,
|
|
allowAnswer,
|
|
allowRead,
|
|
allowSearch,
|
|
badContext,
|
|
allKnowledge,
|
|
allURLs,
|
|
false
|
|
);
|
|
|
|
const model = genAI.getGenerativeModel({
|
|
model: modelConfigs.agent.model,
|
|
generationConfig: {
|
|
temperature: modelConfigs.agent.temperature,
|
|
responseMimeType: "application/json",
|
|
responseSchema: getSchema(allowReflect, allowRead, allowAnswer, allowSearch)
|
|
}
|
|
});
|
|
|
|
// Check if we have enough budget for this operation (estimate 50 tokens for prompt + response)
|
|
const estimatedTokens = 50;
|
|
const currentUsage = context.tokenTracker.getTotalUsage();
|
|
if (currentUsage + estimatedTokens > tokenBudget) {
|
|
throw new Error(`Token budget would be exceeded: ${currentUsage + estimatedTokens} > ${tokenBudget}`);
|
|
}
|
|
|
|
const result = await model.generateContent(prompt);
|
|
const response = await result.response;
|
|
const usage = response.usageMetadata;
|
|
context.tokenTracker.trackUsage('agent', usage?.totalTokenCount || 0);
|
|
|
|
|
|
thisStep = JSON.parse(response.text());
|
|
// print allowed and chose action
|
|
const actionsStr = [allowSearch, allowRead, allowAnswer, allowReflect].map((a, i) => a ? ['search', 'read', 'answer', 'reflect'][i] : null).filter(a => a).join(', ');
|
|
console.log(`${thisStep.action} <- [${actionsStr}]`);
|
|
console.log(thisStep)
|
|
|
|
// reset allowAnswer to true
|
|
allowAnswer = true;
|
|
allowReflect = true;
|
|
allowRead = true;
|
|
allowSearch = true;
|
|
|
|
// execute the step and action
|
|
if (thisStep.action === 'answer') {
|
|
updateContext({
|
|
totalStep,
|
|
question: currentQuestion,
|
|
...thisStep,
|
|
});
|
|
|
|
const {response: evaluation} = await evaluateAnswer(currentQuestion, thisStep.answer, context.tokenTracker);
|
|
|
|
|
|
if (currentQuestion === question) {
|
|
if (badAttempts >= maxBadAttempts) {
|
|
// EXIT POINT OF THE PROGRAM!!!!
|
|
diaryContext.push(`
|
|
At step ${step} and ${badAttempts} attempts, you took **answer** action and found an answer, not a perfect one but good enough to answer the original question:
|
|
|
|
Original question:
|
|
${currentQuestion}
|
|
|
|
Your answer:
|
|
${thisStep.answer}
|
|
|
|
The evaluator thinks your answer is good because:
|
|
${evaluation.reasoning}
|
|
|
|
Your journey ends here.
|
|
`);
|
|
isAnswered = false;
|
|
break
|
|
}
|
|
if (evaluation.is_definitive) {
|
|
if (thisStep.references?.length > 0 || Object.keys(allURLs).length === 0) {
|
|
// EXIT POINT OF THE PROGRAM!!!!
|
|
diaryContext.push(`
|
|
At step ${step}, you took **answer** action and finally found the answer to the original question:
|
|
|
|
Original question:
|
|
${currentQuestion}
|
|
|
|
Your answer:
|
|
${thisStep.answer}
|
|
|
|
The evaluator thinks your answer is good because:
|
|
${evaluation.reasoning}
|
|
|
|
Your journey ends here. You have successfully answered the original question. Congratulations! 🎉
|
|
`);
|
|
isAnswered = true;
|
|
break
|
|
} else {
|
|
diaryContext.push(`
|
|
At step ${step}, you took **answer** action and finally found the answer to the original question:
|
|
|
|
Original question:
|
|
${currentQuestion}
|
|
|
|
Your answer:
|
|
${thisStep.answer}
|
|
|
|
Unfortunately, you did not provide any references to support your answer.
|
|
You need to find more URL references to support your answer.`);
|
|
}
|
|
|
|
isAnswered = true;
|
|
break
|
|
|
|
} else {
|
|
diaryContext.push(`
|
|
At step ${step}, you took **answer** action but evaluator thinks it is not a good answer:
|
|
|
|
Original question:
|
|
${currentQuestion}
|
|
|
|
Your answer:
|
|
${thisStep.answer}
|
|
|
|
The evaluator thinks your answer is bad because:
|
|
${evaluation.reasoning}
|
|
`);
|
|
// store the bad context and reset the diary context
|
|
const {response: errorAnalysis} = await analyzeSteps(diaryContext);
|
|
|
|
badContext.push({
|
|
question: currentQuestion,
|
|
answer: thisStep.answer,
|
|
evaluation: evaluation.reasoning,
|
|
...errorAnalysis
|
|
});
|
|
badAttempts++;
|
|
allowAnswer = false; // disable answer action in the immediate next step
|
|
diaryContext = [];
|
|
step = 0;
|
|
}
|
|
} else if (evaluation.is_definitive) {
|
|
diaryContext.push(`
|
|
At step ${step}, you took **answer** action. You found a good answer to the sub-question:
|
|
|
|
Sub-question:
|
|
${currentQuestion}
|
|
|
|
Your answer:
|
|
${thisStep.answer}
|
|
|
|
The evaluator thinks your answer is good because:
|
|
${evaluation.reasoning}
|
|
|
|
Although you solved a sub-question, you still need to find the answer to the original question. You need to keep going.
|
|
`);
|
|
allKnowledge.push({
|
|
question: currentQuestion,
|
|
answer: thisStep.answer,
|
|
type: 'qa'
|
|
});
|
|
}
|
|
} else if (thisStep.action === 'reflect' && thisStep.questionsToAnswer) {
|
|
let newGapQuestions = thisStep.questionsToAnswer
|
|
const oldQuestions = newGapQuestions;
|
|
if (allQuestions.length) {
|
|
newGapQuestions = (await dedupQueries(newGapQuestions, allQuestions)).unique_queries;
|
|
}
|
|
if (newGapQuestions.length > 0) {
|
|
for (const gapQuestion of newGapQuestions) {
|
|
// Create sub-context with remaining token budget
|
|
// Calculate sub-question budget as a fraction of current remaining budget
|
|
const currentRemaining = tokenBudget - context.tokenTracker.getTotalUsage();
|
|
const subQuestionBudget = Math.floor(currentRemaining * budgetSplitRatio);
|
|
// Log the budget allocation for monitoring
|
|
console.log(`Allocating ${subQuestionBudget} tokens for sub-question: "${gapQuestion}"`)
|
|
|
|
const subContext = {
|
|
tokenTracker: context.tokenTracker, // Share token tracker
|
|
actionTracker: context.actionTracker // Share action tracker
|
|
};
|
|
|
|
// Recursively resolve gap question
|
|
const {result: gapResult} = await getResponse(
|
|
gapQuestion,
|
|
subQuestionBudget,
|
|
maxBadAttempts,
|
|
subContext,
|
|
allKnowledge, // Pass current knowledge to sub-question
|
|
recursionLevel + 1
|
|
);
|
|
|
|
// If gap was successfully answered, add to knowledge
|
|
if (gapResult.action === 'answer') {
|
|
allKnowledge.push({
|
|
question: gapQuestion,
|
|
answer: gapResult.answer,
|
|
type: 'qa'
|
|
});
|
|
}
|
|
}
|
|
diaryContext.push(`
|
|
At step ${step}, you took **reflect** and resolved ${newGapQuestions.length} sub-questions:
|
|
${newGapQuestions.map(q => `- ${q}`).join('\n')}
|
|
The answers to these questions have been added to your knowledge base.
|
|
`);
|
|
} else {
|
|
diaryContext.push(`
|
|
At step ${step}, you took **reflect** and think about the knowledge gaps. You tried to break down the question "${currentQuestion}" into gap-questions like this: ${oldQuestions.join(', ')}
|
|
But then you realized you have asked them before. You decided to to think out of the box or cut from a completely different angle.
|
|
`);
|
|
updateContext({
|
|
totalStep,
|
|
...thisStep,
|
|
result: 'I have tried all possible questions and found no useful information. I must think out of the box or different angle!!!'
|
|
});
|
|
|
|
allowReflect = false;
|
|
}
|
|
} else if (thisStep.action === 'search' && thisStep.searchQuery) {
|
|
// rewrite queries
|
|
let {queries: keywordsQueries} = await rewriteQuery(thisStep);
|
|
|
|
const oldKeywords = keywordsQueries;
|
|
// avoid exisitng searched queries
|
|
if (allKeywords.length) {
|
|
const {unique_queries: dedupedQueries} = await dedupQueries(keywordsQueries, allKeywords);
|
|
keywordsQueries = dedupedQueries;
|
|
}
|
|
if (keywordsQueries.length > 0) {
|
|
const searchResults = [];
|
|
for (const query of keywordsQueries) {
|
|
console.log(`Search query: ${query}`);
|
|
let results;
|
|
if (SEARCH_PROVIDER === 'duck') {
|
|
results = await duckSearch(query, {
|
|
safeSearch: SafeSearchType.STRICT
|
|
});
|
|
} else {
|
|
const {response} = await braveSearch(query);
|
|
await sleep(STEP_SLEEP);
|
|
results = {
|
|
results: response.web.results.map(r => ({
|
|
title: r.title,
|
|
url: r.url,
|
|
description: r.description
|
|
}))
|
|
};
|
|
}
|
|
const minResults = results.results.map(r => ({
|
|
title: r.title,
|
|
url: r.url,
|
|
description: r.description,
|
|
}));
|
|
for (const r of minResults) {
|
|
allURLs[r.url] = r.title;
|
|
}
|
|
searchResults.push({query, results: minResults});
|
|
allKeywords.push(query);
|
|
}
|
|
diaryContext.push(`
|
|
At step ${step}, you took the **search** action and look for external information for the question: "${currentQuestion}".
|
|
In particular, you tried to search for the following keywords: "${keywordsQueries.join(', ')}".
|
|
You found quite some information and add them to your URL list and **visit** them later when needed.
|
|
`);
|
|
|
|
updateContext({
|
|
totalStep,
|
|
question: currentQuestion,
|
|
...thisStep,
|
|
result: searchResults
|
|
});
|
|
} else {
|
|
diaryContext.push(`
|
|
At step ${step}, you took the **search** action and look for external information for the question: "${currentQuestion}".
|
|
In particular, you tried to search for the following keywords: ${oldKeywords.join(', ')}.
|
|
But then you realized you have already searched for these keywords before.
|
|
You decided to think out of the box or cut from a completely different angle.
|
|
`);
|
|
|
|
|
|
updateContext({
|
|
totalStep,
|
|
...thisStep,
|
|
result: 'I have tried all possible queries and found no new information. I must think out of the box or different angle!!!'
|
|
});
|
|
|
|
allowSearch = false;
|
|
}
|
|
} else if (thisStep.action === 'visit' && thisStep.URLTargets?.length) {
|
|
|
|
let uniqueURLs = thisStep.URLTargets;
|
|
if (visitedURLs.length > 0) {
|
|
// check duplicate urls
|
|
uniqueURLs = uniqueURLs.filter((url: string) => !visitedURLs.includes(url));
|
|
}
|
|
|
|
if (uniqueURLs.length > 0) {
|
|
|
|
const urlResults = await Promise.all(
|
|
uniqueURLs.map(async (url: string) => {
|
|
const {response, tokens} = await readUrl(url, JINA_API_KEY, context.tokenTracker);
|
|
allKnowledge.push({
|
|
question: `What is in ${response.data?.url || 'the URL'}?`,
|
|
answer: removeAllLineBreaks(response.data?.content || 'No content available'),
|
|
type: 'url'
|
|
});
|
|
visitedURLs.push(url);
|
|
delete allURLs[url];
|
|
return {url, result: response, tokens};
|
|
})
|
|
);
|
|
diaryContext.push(`
|
|
At step ${step}, you took the **visit** action and deep dive into the following URLs:
|
|
${thisStep.URLTargets.join('\n')}
|
|
You found some useful information on the web and add them to your knowledge for future reference.
|
|
`);
|
|
updateContext({
|
|
totalStep,
|
|
question: currentQuestion,
|
|
...thisStep,
|
|
result: urlResults
|
|
});
|
|
} else {
|
|
|
|
diaryContext.push(`
|
|
At step ${step}, you took the **visit** action and try to visit the following URLs:
|
|
${thisStep.URLTargets.join('\n')}
|
|
But then you realized you have already visited these URLs and you already know very well about their contents.
|
|
|
|
You decided to think out of the box or cut from a completely different angle.`);
|
|
|
|
updateContext({
|
|
totalStep,
|
|
...thisStep,
|
|
result: 'I have visited all possible URLs and found no new information. I must think out of the box or different angle!!!'
|
|
});
|
|
|
|
allowRead = false;
|
|
}
|
|
}
|
|
|
|
await storeContext(prompt, [allContext, allKeywords, allQuestions, allKnowledge], totalStep, recursionLevel);
|
|
}
|
|
step++;
|
|
totalStep++;
|
|
await storeContext(prompt, [allContext, allKeywords, allQuestions, allKnowledge], totalStep, recursionLevel);
|
|
if (isAnswered) {
|
|
return {result: thisStep, context};
|
|
} else {
|
|
console.log('Enter Beast mode!!!')
|
|
const prompt = getPrompt(
|
|
question,
|
|
diaryContext,
|
|
allQuestions,
|
|
false,
|
|
false,
|
|
false,
|
|
false,
|
|
badContext,
|
|
allKnowledge,
|
|
allURLs,
|
|
true
|
|
);
|
|
|
|
const model = genAI.getGenerativeModel({
|
|
model: modelConfigs.agentBeastMode.model,
|
|
generationConfig: {
|
|
temperature: modelConfigs.agentBeastMode.temperature,
|
|
responseMimeType: "application/json",
|
|
responseSchema: getSchema(false, false, allowAnswer, false)
|
|
}
|
|
});
|
|
|
|
// Check if we have enough budget for this operation (estimate 50 tokens for prompt + response)
|
|
const estimatedTokens = 50;
|
|
const currentUsage = context.tokenTracker.getTotalUsage();
|
|
if (currentUsage + estimatedTokens > tokenBudget) {
|
|
throw new Error(`Token budget would be exceeded: ${currentUsage + estimatedTokens} > ${tokenBudget}`);
|
|
}
|
|
|
|
const result = await model.generateContent(prompt);
|
|
const response = await result.response;
|
|
const usage = response.usageMetadata;
|
|
context.tokenTracker.trackUsage('agent', usage?.totalTokenCount || 0);
|
|
|
|
await storeContext(prompt, [allContext, allKeywords, allQuestions, allKnowledge], totalStep, recursionLevel);
|
|
thisStep = JSON.parse(response.text());
|
|
console.log(thisStep)
|
|
return {result: thisStep, context};
|
|
}
|
|
}
|
|
|
|
async function storeContext(prompt: string, memory: any[][], step: number, recursionLevel: number) {
|
|
try {
|
|
await fs.writeFile(`prompt-${recursionLevel}-${step}.txt`, prompt);
|
|
const [context, keywords, questions, knowledge] = memory;
|
|
await fs.writeFile(`context-${recursionLevel}.json`, JSON.stringify(context, null, 2));
|
|
await fs.writeFile(`queries-${recursionLevel}.json`, JSON.stringify(keywords, null, 2));
|
|
await fs.writeFile(`questions-${recursionLevel}.json`, JSON.stringify(questions, null, 2));
|
|
await fs.writeFile(`knowledge-${recursionLevel}.json`, JSON.stringify(knowledge, null, 2));
|
|
} catch (error) {
|
|
console.error('Context storage failed:', error);
|
|
}
|
|
}
|
|
|
|
const genAI = new GoogleGenerativeAI(GEMINI_API_KEY);
|
|
|
|
|
|
export async function main() {
|
|
const question = process.argv[2] || "";
|
|
const {
|
|
result: finalStep,
|
|
context: tracker
|
|
} = await getResponse(question) as { result: AnswerAction; context: TrackerContext };
|
|
console.log('Final Answer:', finalStep.answer);
|
|
|
|
tracker.tokenTracker.printSummary();
|
|
}
|
|
|
|
if (require.main === module) {
|
|
main().catch(console.error);
|
|
}
|