mirror of
https://github.com/jina-ai/node-DeepResearch.git
synced 2025-12-26 06:28:56 +08:00
feat: optimize prompt, coding, reflect
This commit is contained in:
parent
5c91d5b4ad
commit
c02588a92c
@ -12,7 +12,7 @@
|
||||
"defaults": {
|
||||
"search_provider": "jina",
|
||||
"llm_provider": "gemini",
|
||||
"step_sleep": 0
|
||||
"step_sleep": 100
|
||||
},
|
||||
"providers": {
|
||||
"gemini": {
|
||||
@ -30,13 +30,13 @@
|
||||
"default": {
|
||||
"model": "gemini-2.0-flash",
|
||||
"temperature": 0,
|
||||
"maxTokens": 1000
|
||||
"maxTokens": 2000
|
||||
},
|
||||
"tools": {
|
||||
"coder": { "temperature": 0.7 },
|
||||
"searchGrounding": { "temperature": 0 },
|
||||
"dedup": { "temperature": 0.1 },
|
||||
"evaluator": {},
|
||||
"evaluator": {"temperature": 0.6, "maxTokens": 200},
|
||||
"errorAnalyzer": {},
|
||||
"queryRewriter": { "temperature": 0.1 },
|
||||
"agent": { "temperature": 0.7 },
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
"defaults": {
|
||||
"search_provider": "serper",
|
||||
"llm_provider": "vertex",
|
||||
"step_sleep": 0
|
||||
"step_sleep": 100
|
||||
},
|
||||
"providers": {
|
||||
"vertex": {
|
||||
@ -39,15 +39,15 @@
|
||||
"maxTokens": 8000
|
||||
},
|
||||
"tools": {
|
||||
"coder": { "temperature": 0.7, "maxTokens": 1000 },
|
||||
"coder": { "temperature": 0.6, "maxTokens": 1000 },
|
||||
"searchGrounding": { "temperature": 0 },
|
||||
"dedup": { "temperature": 0.1 },
|
||||
"evaluator": {"maxTokens": 500},
|
||||
"evaluator": {"temperature": 0.6, "maxTokens": 300},
|
||||
"errorAnalyzer": {"maxTokens": 500},
|
||||
"queryRewriter": { "temperature": 0.1, "maxTokens": 500 },
|
||||
"agent": { "temperature": 0.7 },
|
||||
"agentBeastMode": { "temperature": 0.7 },
|
||||
"fallback": { "temperature": 0, "maxTokens": 1000}
|
||||
"agent": { "temperature": 0.6 },
|
||||
"agentBeastMode": { "temperature": 0.6 },
|
||||
"fallback": { "temperature": 0, "maxTokens": 4000}
|
||||
}
|
||||
},
|
||||
"openai": {
|
||||
|
||||
217
src/agent.ts
217
src/agent.ts
@ -11,7 +11,7 @@ import {evaluateAnswer, evaluateQuestion} from "./tools/evaluator";
|
||||
import {analyzeSteps} from "./tools/error-analyzer";
|
||||
import {TokenTracker} from "./utils/token-tracker";
|
||||
import {ActionTracker} from "./utils/action-tracker";
|
||||
import {StepAction, AnswerAction, KnowledgeItem, EvaluationCriteria} from "./types";
|
||||
import {StepAction, AnswerAction, KnowledgeItem, EvaluationCriteria, SearchResult} from "./types";
|
||||
import {TrackerContext} from "./types";
|
||||
import {search} from "./tools/jina-search";
|
||||
// import {grounding} from "./tools/grounding";
|
||||
@ -19,6 +19,7 @@ import {zodToJsonSchema} from "zod-to-json-schema";
|
||||
import {ObjectGeneratorSafe} from "./utils/safe-generator";
|
||||
import {CodeSandbox} from "./tools/code-sandbox";
|
||||
import {serperSearch} from './tools/serper-search';
|
||||
import {normalizeUrl} from "./utils/url-tools";
|
||||
|
||||
async function sleep(ms: number) {
|
||||
const seconds = Math.ceil(ms / 1000);
|
||||
@ -26,6 +27,10 @@ async function sleep(ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
const MAX_URLS_PER_STEP = 2
|
||||
const MAX_QUERIES_PER_STEP = 5
|
||||
const MAX_REFLECT_PER_STEP = 3
|
||||
|
||||
function getSchema(allowReflect: boolean, allowRead: boolean, allowAnswer: boolean, allowSearch: boolean, allowCoding: boolean, languageStyle: string = 'same language as the question') {
|
||||
const actions: string[] = [];
|
||||
const properties: Record<string, z.ZodTypeAny> = {
|
||||
@ -61,15 +66,15 @@ function getSchema(allowReflect: boolean, allowRead: boolean, allowAnswer: boole
|
||||
actions.push("reflect");
|
||||
properties.questionsToAnswer = z.array(
|
||||
z.string().describe("each question must be a single line, Questions must be: Original (not variations of existing questions); Focused on single concepts; Under 20 words; Non-compound/non-complex")
|
||||
).max(2)
|
||||
.describe("Required when action='reflect'. List of most important questions to fill the knowledge gaps of finding the answer to the original question").optional();
|
||||
).max(MAX_REFLECT_PER_STEP)
|
||||
.describe(`Required when action='reflect'. List of most important questions to fill the knowledge gaps of finding the answer to the original question. Maximum provide ${MAX_REFLECT_PER_STEP} reflect questions.`).optional();
|
||||
}
|
||||
|
||||
if (allowRead) {
|
||||
actions.push("visit");
|
||||
properties.URLTargets = z.array(z.string())
|
||||
.max(2)
|
||||
.describe("Required when action='visit'. Must be an array of URLs, choose up the most relevant 2 URLs to visit").optional();
|
||||
.max(MAX_URLS_PER_STEP)
|
||||
.describe(`Required when action='visit'. Must be an array of URLs, choose up the most relevant ${MAX_URLS_PER_STEP} URLs to visit`).optional();
|
||||
}
|
||||
|
||||
// Update the enum values after collecting all actions
|
||||
@ -80,6 +85,11 @@ function getSchema(allowReflect: boolean, allowRead: boolean, allowAnswer: boole
|
||||
|
||||
}
|
||||
|
||||
function getUnvisitedURLs(allURLs: Record<string, SearchResult>, visitedURLs: string[]): SearchResult[] {
|
||||
return Object.entries(allURLs)
|
||||
.filter(([url]) => !visitedURLs.includes(url))
|
||||
.map(([, result]) => result);
|
||||
}
|
||||
|
||||
function getPrompt(
|
||||
context?: string[],
|
||||
@ -92,7 +102,7 @@ function getPrompt(
|
||||
allowCoding: boolean = true,
|
||||
badContext?: { question: string, answer: string, evaluation: string, recap: string; blame: string; improvement: string; }[],
|
||||
knowledge?: KnowledgeItem[],
|
||||
allURLs?: Record<string, string>,
|
||||
allURLs?: SearchResult[],
|
||||
beastMode?: boolean,
|
||||
): string {
|
||||
const sections: string[] = [];
|
||||
@ -181,9 +191,10 @@ ${learnedStrategy}
|
||||
|
||||
if (allowRead) {
|
||||
let urlList = '';
|
||||
if (allURLs && Object.keys(allURLs).length > 0) {
|
||||
urlList = Object.entries(allURLs)
|
||||
.map(([url, desc]) => ` + "${url}": "${desc}"`)
|
||||
if (allURLs && allURLs.length > 0) {
|
||||
urlList = allURLs
|
||||
.filter(r => 'url' in r)
|
||||
.map(r => ` + "${r.url}": "${r.title}"`)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
@ -232,6 +243,7 @@ ${allKeywords.join('\n')}
|
||||
actionSections.push(`
|
||||
<action-answer>
|
||||
- For greetings, casual conversation, or general knowledge questions, answer directly without references.
|
||||
- If the question is clearly within your knowledge cutoff (i.e. Aug. 2024), provide a confident answer directly.
|
||||
- For all other questions, provide a verified answer with references. Each reference must include exactQuote and url.
|
||||
- If uncertain, use <action-reflect>
|
||||
</action-answer>
|
||||
@ -257,8 +269,12 @@ FAILURE IS NOT AN OPTION. EXECUTE WITH EXTREME PREJUDICE! ⚡️
|
||||
if (allowReflect) {
|
||||
actionSections.push(`
|
||||
<action-reflect>
|
||||
- Analyze through scenarios and systematic breakdowns
|
||||
- Identify gaps and ask key clarifying questions that related to the original question and lead to the answer
|
||||
- Critically examine <question>, <context>, <knowledge>, <bad-attempts>, and <learned-strategy> to identify gaps and the problems.
|
||||
- Identify gaps and ask key clarifying questions that deeply related to the original question and lead to the answer
|
||||
- Ensure each reflection:
|
||||
- Cuts to core emotional truths while staying anchored to original <question>
|
||||
- Transforms surface-level problems into deeper psychological insights
|
||||
- Makes the unconscious conscious
|
||||
</action-reflect>
|
||||
`);
|
||||
}
|
||||
@ -286,6 +302,10 @@ function updateContext(step: any) {
|
||||
allContext.push(step)
|
||||
}
|
||||
|
||||
function chooseK(a: string[], k: number) {
|
||||
// randomly sample k from `a` without repitition
|
||||
return a.sort(() => 0.5 - Math.random()).slice(0, k);
|
||||
}
|
||||
|
||||
function removeHTMLtags(text: string) {
|
||||
return text.replace(/<[^>]*>?/gm, '');
|
||||
@ -297,12 +317,11 @@ export async function getResponse(question?: string,
|
||||
maxBadAttempts: number = 3,
|
||||
existingContext?: Partial<TrackerContext>,
|
||||
messages?: Array<CoreAssistantMessage | CoreUserMessage>
|
||||
): Promise<{ result: StepAction; context: TrackerContext; visitedURLs: string[] }> {
|
||||
): Promise<{ result: StepAction; context: TrackerContext; visitedURLs: string[], readURLs: string[] }> {
|
||||
const context: TrackerContext = {
|
||||
tokenTracker: existingContext?.tokenTracker || new TokenTracker(tokenBudget),
|
||||
actionTracker: existingContext?.actionTracker || new ActionTracker()
|
||||
};
|
||||
const maxGapQuestions = 3;
|
||||
let step = 0;
|
||||
let totalStep = 0;
|
||||
let badAttempts = 0;
|
||||
@ -328,12 +347,11 @@ export async function getResponse(question?: string,
|
||||
let system = '';
|
||||
let thisStep: StepAction = {action: 'answer', answer: '', references: [], think: '', isFinal: false};
|
||||
|
||||
const allURLs: Record<string, string> = {};
|
||||
const allURLs: Record<string, SearchResult> = {};
|
||||
const visitedURLs: string[] = [];
|
||||
const evaluationMetrics: Record<string, EvaluationCriteria> = {};
|
||||
while (context.tokenTracker.getTotalUsage().totalTokens < tokenBudget && badAttempts <= maxBadAttempts) {
|
||||
// add 1s delay to avoid rate limiting
|
||||
await sleep(STEP_SLEEP);
|
||||
step++;
|
||||
totalStep++;
|
||||
const budgetPercentage = (context.tokenTracker.getTotalUsage().totalTokens / tokenBudget * 100).toFixed(2);
|
||||
@ -347,7 +365,7 @@ export async function getResponse(question?: string,
|
||||
|
||||
// update all urls with buildURLMap
|
||||
// allowRead = allowRead && (Object.keys(allURLs).length > 0);
|
||||
allowSearch = allowSearch && (Object.keys(allURLs).length < 50); // disable search when too many urls already
|
||||
allowSearch = allowSearch && (getUnvisitedURLs(allURLs, visitedURLs).length < 50); // disable search when too many urls already
|
||||
|
||||
// generate prompt for this step
|
||||
system = getPrompt(
|
||||
@ -361,7 +379,7 @@ export async function getResponse(question?: string,
|
||||
allowCoding,
|
||||
badContext,
|
||||
allKnowledge,
|
||||
allURLs,
|
||||
getUnvisitedURLs(allURLs, visitedURLs),
|
||||
false,
|
||||
);
|
||||
schema = getSchema(allowReflect, allowRead, allowAnswer, allowSearch, allowCoding,
|
||||
@ -402,6 +420,15 @@ export async function getResponse(question?: string,
|
||||
...thisStep,
|
||||
});
|
||||
|
||||
// normalize all references urls, add title to it
|
||||
thisStep.references = thisStep.references?.map(ref => {
|
||||
return {
|
||||
exactQuote: ref.exactQuote,
|
||||
title: allURLs[ref.url]?.title,
|
||||
url: normalizeUrl(ref.url)
|
||||
}
|
||||
});
|
||||
|
||||
context.actionTracker.trackThink(`But wait, let me evaluate the answer first.`)
|
||||
|
||||
const {response: evaluation} = await evaluateAnswer(currentQuestion, thisStep,
|
||||
@ -465,8 +492,9 @@ ${evaluation.think}
|
||||
|
||||
if (errorAnalysis.questionsToAnswer) {
|
||||
// reranker? maybe
|
||||
gaps.push(...errorAnalysis.questionsToAnswer.slice(0, maxGapQuestions));
|
||||
allQuestions.push(...errorAnalysis.questionsToAnswer.slice(0,maxGapQuestions));
|
||||
errorAnalysis.questionsToAnswer = chooseK(errorAnalysis.questionsToAnswer, MAX_REFLECT_PER_STEP);
|
||||
gaps.push(...errorAnalysis.questionsToAnswer);
|
||||
allQuestions.push(...errorAnalysis.questionsToAnswer);
|
||||
gaps.push(question); // always keep the original question in the gaps
|
||||
}
|
||||
|
||||
@ -500,9 +528,8 @@ Although you solved a sub-question, you still need to find the answer to the ori
|
||||
});
|
||||
}
|
||||
} else if (thisStep.action === 'reflect' && thisStep.questionsToAnswer) {
|
||||
let newGapQuestions = thisStep.questionsToAnswer
|
||||
const oldQuestions = newGapQuestions;
|
||||
newGapQuestions = (await dedupQueries(newGapQuestions, allQuestions, context.tokenTracker)).unique_queries;
|
||||
thisStep.questionsToAnswer = chooseK((await dedupQueries(thisStep.questionsToAnswer, allQuestions, context.tokenTracker)).unique_queries, MAX_REFLECT_PER_STEP);
|
||||
const newGapQuestions = thisStep.questionsToAnswer
|
||||
if (newGapQuestions.length > 0) {
|
||||
// found new gap questions
|
||||
diaryContext.push(`
|
||||
@ -512,8 +539,8 @@ ${newGapQuestions.map((q: string) => `- ${q}`).join('\n')}
|
||||
|
||||
You will now figure out the answers to these sub-questions and see if they can help you find the answer to the original question.
|
||||
`);
|
||||
gaps.push(...newGapQuestions.slice(0, maxGapQuestions));
|
||||
allQuestions.push(...newGapQuestions.slice(0, maxGapQuestions));
|
||||
gaps.push(...newGapQuestions);
|
||||
allQuestions.push(...newGapQuestions);
|
||||
gaps.push(question); // always keep the original question in the gaps
|
||||
updateContext({
|
||||
totalStep,
|
||||
@ -522,7 +549,7 @@ You will now figure out the answers to these sub-questions and see if they can h
|
||||
|
||||
} 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(', ')}
|
||||
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: ${newGapQuestions.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({
|
||||
@ -536,90 +563,64 @@ But then you realized you have asked them before. You decided to to think out of
|
||||
} else if (thisStep.action === 'search' && thisStep.searchQuery) {
|
||||
// rewrite queries
|
||||
let {queries: keywordsQueries} = await rewriteQuery(thisStep, context);
|
||||
|
||||
// add the original query before rewrite to the keywordsQueries
|
||||
keywordsQueries.push(thisStep.searchQuery)
|
||||
|
||||
const oldKeywords = keywordsQueries;
|
||||
// avoid exisitng searched queries
|
||||
const {unique_queries: dedupedQueries} = await dedupQueries(keywordsQueries, allKeywords, context.tokenTracker);
|
||||
keywordsQueries = dedupedQueries;
|
||||
keywordsQueries = chooseK(dedupedQueries, MAX_QUERIES_PER_STEP);
|
||||
let anyResult = false;
|
||||
|
||||
if (keywordsQueries.length > 0) {
|
||||
// let googleGrounded = '';
|
||||
const searchResults = [];
|
||||
|
||||
|
||||
context.actionTracker.trackThink(`Let me search for "${keywordsQueries.join(', ')}" to gather more information.`)
|
||||
for (const query of keywordsQueries) {
|
||||
console.log(`Search query: ${query}`);
|
||||
|
||||
let results;
|
||||
let results: SearchResult[] = []
|
||||
|
||||
switch (SEARCH_PROVIDER) {
|
||||
case 'jina':
|
||||
// use jinaSearch
|
||||
results = {results: (await search(query, context.tokenTracker)).response?.data || []};
|
||||
// if (LLM_PROVIDER === 'gemini') {
|
||||
// googleGrounded = await grounding(query, context.tokenTracker);
|
||||
// }
|
||||
break;
|
||||
case 'duck':
|
||||
results = await duckSearch(query, {safeSearch: SafeSearchType.STRICT});
|
||||
break;
|
||||
case 'brave':
|
||||
try {
|
||||
const {response} = await braveSearch(query);
|
||||
results = {
|
||||
results: response.web?.results?.map(r => ({
|
||||
title: r.title,
|
||||
url: r.url,
|
||||
description: r.description
|
||||
})) || []
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Brave search failed:', error);
|
||||
results = {results: []};
|
||||
}
|
||||
await sleep(STEP_SLEEP)
|
||||
break;
|
||||
case 'serper':
|
||||
try {
|
||||
const {response} = await serperSearch(query);
|
||||
results = {
|
||||
results: response.organic?.map(r => ({
|
||||
title: r.title,
|
||||
url: r.link,
|
||||
description: r.snippet
|
||||
})) || []
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Serper search failed:', error);
|
||||
results = {results: []};
|
||||
}
|
||||
await sleep(STEP_SLEEP)
|
||||
break;
|
||||
default:
|
||||
results = {results: []};
|
||||
try {
|
||||
switch (SEARCH_PROVIDER) {
|
||||
case 'jina':
|
||||
results = (await search(query, context.tokenTracker)).response?.data || [];
|
||||
break;
|
||||
case 'duck':
|
||||
results = (await duckSearch(query, {safeSearch: SafeSearchType.STRICT})).results;
|
||||
break;
|
||||
case 'brave':
|
||||
results = (await braveSearch(query)).response.web?.results || [];
|
||||
break;
|
||||
case 'serper':
|
||||
results = (await serperSearch(query)).response.organic || [];
|
||||
break;
|
||||
default:
|
||||
results = [];
|
||||
}
|
||||
if (results.length === 0) {
|
||||
throw new Error('No results found');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`${SEARCH_PROVIDER} search failed for query "${query}":`, error);
|
||||
continue
|
||||
} finally {
|
||||
await sleep(STEP_SLEEP)
|
||||
}
|
||||
const minResults = results.results.map(r => ({
|
||||
|
||||
const minResults = (results).map(r => ({
|
||||
title: r.title,
|
||||
url: r.url,
|
||||
description: r.description
|
||||
url: normalizeUrl('url' in r ? r.url : r.link),
|
||||
description: 'description' in r ? r.description : r.snippet
|
||||
}));
|
||||
|
||||
Object.assign(allURLs, Object.fromEntries(
|
||||
minResults.map(r => [r.url, r.title])
|
||||
));
|
||||
searchResults.push({query, results: minResults});
|
||||
minResults.forEach(r => allURLs[r.url] = r);
|
||||
allKeywords.push(query);
|
||||
}
|
||||
|
||||
allKnowledge.push({
|
||||
question: `What do Internet say about ${thisStep.searchQuery}?`,
|
||||
answer: removeHTMLtags(searchResults.map(r => r.results.map(r => r.description).join('; ')).join('; ')),
|
||||
// answer: googleGrounded + removeHTMLtags(searchResults.map(r => r.results.map(r => r.description).join('; ')).join('; ')),
|
||||
type: 'side-info',
|
||||
updated: new Date().toISOString()
|
||||
});
|
||||
allKnowledge.push({
|
||||
question: `What do Internet say about "${query}"?`,
|
||||
answer: removeHTMLtags(minResults.map(r => r.description).join('; ')),
|
||||
type: 'side-info',
|
||||
updated: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
diaryContext.push(`
|
||||
At step ${step}, you took the **search** action and look for external information for the question: "${currentQuestion}".
|
||||
@ -631,17 +632,18 @@ You found quite some information and add them to your URL list and **visit** the
|
||||
totalStep,
|
||||
question: currentQuestion,
|
||||
...thisStep,
|
||||
result: searchResults
|
||||
result: result
|
||||
});
|
||||
} else {
|
||||
anyResult = true;
|
||||
}
|
||||
if (!anyResult || !keywordsQueries?.length) {
|
||||
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.
|
||||
But then you realized you have already searched for these keywords before, no new information is returned.
|
||||
You decided to think out of the box or cut from a completely different angle.
|
||||
`);
|
||||
|
||||
|
||||
updateContext({
|
||||
totalStep,
|
||||
...thisStep,
|
||||
@ -651,9 +653,11 @@ You decided to think out of the box or cut from a completely different angle.
|
||||
allowSearch = false;
|
||||
}
|
||||
} else if (thisStep.action === 'visit' && thisStep.URLTargets?.length) {
|
||||
const uniqueURLs = visitedURLs.length > 0
|
||||
? thisStep.URLTargets.filter(url => !visitedURLs.includes(url))
|
||||
: thisStep.URLTargets;
|
||||
// normalize URLs
|
||||
thisStep.URLTargets = thisStep.URLTargets.map(url => normalizeUrl(url));
|
||||
thisStep.URLTargets = chooseK(thisStep.URLTargets.filter(url => !visitedURLs.includes(url)), MAX_URLS_PER_STEP)
|
||||
|
||||
const uniqueURLs = thisStep.URLTargets;
|
||||
|
||||
if (uniqueURLs.length > 0) {
|
||||
context.actionTracker.trackThink(`Let me read ${uniqueURLs.join(', ')} to gather more information.`);
|
||||
@ -683,7 +687,6 @@ You decided to think out of the box or cut from a completely different angle.
|
||||
return null;
|
||||
} finally {
|
||||
visitedURLs.push(url);
|
||||
delete allURLs[url];
|
||||
}
|
||||
})
|
||||
).then(results => results.filter(Boolean));
|
||||
@ -723,12 +726,13 @@ You decided to think out of the box or cut from a completely different angle.`);
|
||||
allowRead = false;
|
||||
}
|
||||
} else if (thisStep.action === 'coding' && thisStep.codingIssue) {
|
||||
const sandbox = new CodeSandbox({allContext}, context.tokenTracker);
|
||||
const sandbox = new CodeSandbox({allContext, visitedURLs, allURLs, allKnowledge}, context);
|
||||
try {
|
||||
const result = await sandbox.solve(thisStep.codingIssue);
|
||||
allKnowledge.push({
|
||||
question: `What is the solution to the coding issue: ${thisStep.codingIssue}?`,
|
||||
answer: result.solution.output,
|
||||
sourceCode: result.solution.code,
|
||||
type: 'coding',
|
||||
updated: new Date().toISOString()
|
||||
});
|
||||
@ -752,14 +756,14 @@ But unfortunately, you failed to solve the issue. You need to think out of the b
|
||||
...thisStep,
|
||||
result: 'You have tried all possible solutions and found no new information. You must think out of the box or different angle!!!'
|
||||
});
|
||||
}
|
||||
finally {
|
||||
} finally {
|
||||
allowCoding = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
await storeContext(system, schema, [allContext, allKeywords, allQuestions, allKnowledge], totalStep);
|
||||
await sleep(STEP_SLEEP);
|
||||
}
|
||||
|
||||
await storeContext(system, schema, [allContext, allKeywords, allQuestions, allKnowledge], totalStep);
|
||||
@ -779,7 +783,7 @@ But unfortunately, you failed to solve the issue. You need to think out of the b
|
||||
false,
|
||||
badContext,
|
||||
allKnowledge,
|
||||
allURLs,
|
||||
getUnvisitedURLs(allURLs, visitedURLs),
|
||||
true,
|
||||
);
|
||||
|
||||
@ -802,7 +806,8 @@ But unfortunately, you failed to solve the issue. You need to think out of the b
|
||||
return {
|
||||
result: thisStep,
|
||||
context,
|
||||
visitedURLs: [...new Set([...visitedURLs, ...Object.keys(allURLs)])]
|
||||
visitedURLs: [...new Set([...visitedURLs, ...Object.keys(allURLs)])],
|
||||
readURLs: visitedURLs
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
11
src/app.ts
11
src/app.ts
@ -559,7 +559,7 @@ app.post('/v1/chat/completions', (async (req: Request, res: Response) => {
|
||||
}
|
||||
|
||||
try {
|
||||
const {result: finalStep, visitedURLs: visitedURLs} = await getResponse(undefined, tokenBudget, maxBadAttempts, context, body.messages)
|
||||
const {result: finalStep, visitedURLs: visitedURLs, readURLs: readURLs} = await getResponse(undefined, tokenBudget, maxBadAttempts, context, body.messages)
|
||||
|
||||
const usage = context.tokenTracker.getTotalUsageSnakeCase();
|
||||
if (body.stream) {
|
||||
@ -596,7 +596,8 @@ app.post('/v1/chat/completions', (async (req: Request, res: Response) => {
|
||||
finish_reason: 'stop'
|
||||
}],
|
||||
usage,
|
||||
visitedURLs
|
||||
visitedURLs,
|
||||
readURLs
|
||||
};
|
||||
res.write(`data: ${JSON.stringify(finalChunk)}\n\n`);
|
||||
res.end();
|
||||
@ -618,7 +619,8 @@ app.post('/v1/chat/completions', (async (req: Request, res: Response) => {
|
||||
finish_reason: 'stop'
|
||||
}],
|
||||
usage,
|
||||
visitedURLs
|
||||
visitedURLs,
|
||||
readURLs
|
||||
};
|
||||
|
||||
// Log final response (excluding full content for brevity)
|
||||
@ -627,7 +629,8 @@ app.post('/v1/chat/completions', (async (req: Request, res: Response) => {
|
||||
status: 200,
|
||||
contentLength: response.choices[0].message.content.length,
|
||||
usage: response.usage,
|
||||
visitedURLs: response.visitedURLs
|
||||
visitedURLs: response.visitedURLs,
|
||||
readURLs: response.readURLs
|
||||
});
|
||||
|
||||
res.json(response);
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import { z } from 'zod';
|
||||
import { TokenTracker } from "../utils/token-tracker";
|
||||
import { ObjectGeneratorSafe } from "../utils/safe-generator";
|
||||
import {TrackerContext} from "../types";
|
||||
|
||||
// Define the response schema for code generation
|
||||
const codeGenerationSchema = z.object({
|
||||
code: z.string().describe('The JavaScript code that solves the problem and always use \'return\' statement to return the result. Focus on solving the core problem; No need for error handling or try-catch blocks.'),
|
||||
think: z.string().describe('Short explain or comments on the thought process behind the code, in first person.').max(200),
|
||||
code: z.string().describe('The JavaScript code that solves the problem and always use \'return\' statement to return the result. Focus on solving the core problem; No need for error handling or try-catch blocks or code comments. No need to declare variables that are already available, especially big long strings or arrays.'),
|
||||
});
|
||||
|
||||
// Define the types
|
||||
@ -18,35 +19,27 @@ interface SandboxResult {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface AvailableVariable {
|
||||
name: string;
|
||||
type: string;
|
||||
sample?: string;
|
||||
}
|
||||
|
||||
function getPrompt(
|
||||
problem: string,
|
||||
availableVars: AvailableVariable[],
|
||||
availableVars: string,
|
||||
previousAttempts: Array<{ code: string; error?: string }> = []
|
||||
): string {
|
||||
const previousAttemptsContext = previousAttempts.map((attempt, index) => `
|
||||
Attempt ${index + 1}:
|
||||
<bad-attempt-${index + 1}>
|
||||
${attempt.code}
|
||||
${attempt.error ? `Error: ${attempt.error}` : ''}
|
||||
${attempt.error ? `Error: ${attempt.error}
|
||||
</bad-attempt-${index + 1}>
|
||||
` : ''}
|
||||
`).join('\n');
|
||||
|
||||
const varsContext = availableVars.map(v =>
|
||||
`${v.name} (${v.type})${v.sample ? ` e.g. ${v.sample}` : ''}`
|
||||
).join('\n');
|
||||
|
||||
return `You are an expert JavaScript programmer. Your task is to generate JavaScript code to solve the given problem.
|
||||
const prompt = `You are an expert JavaScript programmer. Your task is to generate JavaScript code to solve the given problem.
|
||||
|
||||
<rules>
|
||||
1. Generate plain JavaScript code that returns the result directly
|
||||
2. You can use any of these available variables directly:
|
||||
${varsContext}
|
||||
3. No need to declare variables that are already available, especially big long strings or arrays; try to always start with using "allContext" object
|
||||
4. Focus on solving the core problem; No need for error handling or try-catch blocks; Always use 'return' statement to return the result
|
||||
2. You can access any of these available variables directly:
|
||||
${availableVars}
|
||||
3. You don't have access to any third party libraries that need to be installed, so you must write complete, self-contained code.
|
||||
</rules>
|
||||
|
||||
${previousAttempts.length > 0 ? `Previous attempts and their errors:
|
||||
@ -68,78 +61,34 @@ Response:
|
||||
|
||||
Problem to solve:
|
||||
${problem}`;
|
||||
|
||||
console.log('Coding prompt', prompt)
|
||||
|
||||
return prompt;
|
||||
}
|
||||
|
||||
export class CodeSandbox {
|
||||
private tracker?: TokenTracker;
|
||||
private trackers?: TrackerContext;
|
||||
private generator: ObjectGeneratorSafe;
|
||||
private maxAttempts: number;
|
||||
private availableVars: AvailableVariable[];
|
||||
private context: Record<string, any>;
|
||||
|
||||
constructor(
|
||||
context: Record<string, any> = {},
|
||||
tracker?: TokenTracker,
|
||||
context: any = {},
|
||||
trackers?: TrackerContext,
|
||||
maxAttempts: number = 3
|
||||
) {
|
||||
this.tracker = tracker;
|
||||
this.generator = new ObjectGeneratorSafe(tracker);
|
||||
this.trackers = trackers;
|
||||
this.generator = new ObjectGeneratorSafe(trackers?.tokenTracker);
|
||||
this.maxAttempts = maxAttempts;
|
||||
this.context = context;
|
||||
this.availableVars = this.collectVariables(context);
|
||||
}
|
||||
|
||||
private collectVariables(context: Record<string, any>): AvailableVariable[] {
|
||||
const vars: AvailableVariable[] = [];
|
||||
|
||||
// Collect from provided context
|
||||
for (const [name, value] of Object.entries(context)) {
|
||||
vars.push(this.createVariableInfo(name, value));
|
||||
}
|
||||
|
||||
// Collect from global scope (window in browser, global in Node)
|
||||
const globalObj = typeof window !== 'undefined' ? window : global;
|
||||
for (const key of Object.keys(globalObj)) {
|
||||
if (key === 'window' || key === 'global' || key === 'globalThis') continue;
|
||||
const value = (globalObj as any)[key];
|
||||
if (typeof value === 'function') continue; // Skip functions
|
||||
if (!vars.some(v => v.name === key)) { // Avoid duplicates
|
||||
vars.push(this.createVariableInfo(key, value));
|
||||
}
|
||||
}
|
||||
|
||||
return vars;
|
||||
}
|
||||
|
||||
private createVariableInfo(name: string, value: any): AvailableVariable {
|
||||
const type = Array.isArray(value)
|
||||
? `Array<${typeof value[0]}>`
|
||||
: typeof value;
|
||||
|
||||
let sample: string | undefined;
|
||||
try {
|
||||
if (Array.isArray(value)) {
|
||||
sample = JSON.stringify(value.slice(0, 3));
|
||||
if (value.length > 3) sample = sample.replace(']', ', ...]');
|
||||
} else if (typeof value === 'object' && value !== null) {
|
||||
const entries = Object.entries(value).slice(0, 2);
|
||||
sample = JSON.stringify(Object.fromEntries(entries));
|
||||
if (Object.keys(value).length > 2) sample = sample.replace('}', ', ...}');
|
||||
} else if (value !== undefined && value !== null) {
|
||||
sample = JSON.stringify(value);
|
||||
}
|
||||
} catch (e) {
|
||||
// If we can't stringify the value, skip the sample
|
||||
}
|
||||
|
||||
return { name, type, sample };
|
||||
}
|
||||
|
||||
private async generateCode(
|
||||
problem: string,
|
||||
previousAttempts: Array<{ code: string; error?: string }> = []
|
||||
): Promise<CodeGenerationResponse> {
|
||||
const prompt = getPrompt(problem, this.availableVars, previousAttempts);
|
||||
const prompt = getPrompt(problem, analyzeStructure(this.context), previousAttempts);
|
||||
|
||||
const result = await this.generator.generateObject({
|
||||
model: 'coder',
|
||||
@ -147,6 +96,8 @@ export class CodeSandbox {
|
||||
prompt,
|
||||
});
|
||||
|
||||
this.trackers?.actionTracker.trackThink(result.object.think);
|
||||
|
||||
return result.object;
|
||||
}
|
||||
|
||||
@ -167,7 +118,7 @@ export class CodeSandbox {
|
||||
if (output === undefined) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'No value was returned'
|
||||
error: 'No value was returned, make sure to use "return" statement to return the result'
|
||||
};
|
||||
}
|
||||
|
||||
@ -197,6 +148,7 @@ export class CodeSandbox {
|
||||
console.log(`Coding attempt ${i + 1}:`, code);
|
||||
// Evaluate the code
|
||||
const result = this.evaluateCode(code);
|
||||
console.log(`Coding attempt ${i + 1} success:`, result);
|
||||
|
||||
if (result.success) {
|
||||
return {
|
||||
@ -225,4 +177,64 @@ export class CodeSandbox {
|
||||
// This should never be reached due to the throw above
|
||||
throw new Error('Unexpected end of execution');
|
||||
}
|
||||
}
|
||||
|
||||
function formatValue(value: any): string {
|
||||
if (value === null) return 'null';
|
||||
if (value === undefined) return 'undefined';
|
||||
|
||||
const type = typeof value;
|
||||
|
||||
if (type === 'string') {
|
||||
// Clean and truncate string value
|
||||
const cleaned = value.replace(/\n/g, ' ').replace(/\s+/g, ' ').trim();
|
||||
return cleaned.length > 50 ?
|
||||
`"${cleaned.slice(0, 47)}..."` :
|
||||
`"${cleaned}"`;
|
||||
}
|
||||
|
||||
if (type === 'number' || type === 'boolean') {
|
||||
return String(value);
|
||||
}
|
||||
|
||||
if (value instanceof Date) {
|
||||
return `"${value.toISOString()}"`;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
export function analyzeStructure(value: any, indent = ''): string {
|
||||
if (value === null) return 'null';
|
||||
if (value === undefined) return 'undefined';
|
||||
|
||||
const type = typeof value;
|
||||
|
||||
if (type === 'function') {
|
||||
return 'Function';
|
||||
}
|
||||
|
||||
// Handle atomic types with example values
|
||||
if (type !== 'object' || value instanceof Date) {
|
||||
const formattedValue = formatValue(value);
|
||||
return `${type}${formattedValue ? ` (example: ${formattedValue})` : ''}`;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length === 0) return 'Array<unknown>';
|
||||
const sampleItem = value[0];
|
||||
return `Array<${analyzeStructure(sampleItem, indent + ' ')}>`;
|
||||
}
|
||||
|
||||
const entries = Object.entries(value);
|
||||
if (entries.length === 0) return '{}';
|
||||
|
||||
const properties = entries
|
||||
.map(([key, val]) => {
|
||||
const analyzed = analyzeStructure(val, indent + ' ');
|
||||
return `${indent} "${key}": ${analyzed}`;
|
||||
})
|
||||
.join(',\n');
|
||||
|
||||
return `{\n${properties}\n${indent}}`;
|
||||
}
|
||||
@ -446,6 +446,7 @@ export async function evaluateAnswer(
|
||||
}
|
||||
|
||||
const allKnowledge = await fetchSourceContent(uniqueURLs, trackers);
|
||||
visitedURLs.push(...uniqueURLs);
|
||||
|
||||
if (!allKnowledge.trim()) {
|
||||
return {
|
||||
|
||||
@ -3,12 +3,13 @@ import {SearchAction, TrackerContext} from '../types';
|
||||
import {ObjectGeneratorSafe} from "../utils/safe-generator";
|
||||
|
||||
|
||||
const MAX_QUERIES = 5
|
||||
const responseSchema = z.object({
|
||||
think: z.string().describe('Strategic reasoning about query complexity and search approach').max(500),
|
||||
queries: z.array(z.string().describe('keyword-based search query, 2-3 words preferred, total length < 30 characters'))
|
||||
.min(1)
|
||||
.max(3)
|
||||
.describe('Array of search keywords queries, orthogonal to each other')
|
||||
.max(MAX_QUERIES)
|
||||
.describe(`'Array of search keywords queries, orthogonal to each other. Maximum ${MAX_QUERIES} queries allowed.'`)
|
||||
});
|
||||
|
||||
|
||||
|
||||
50
src/types.ts
50
src/types.ts
@ -31,6 +31,7 @@ export type KnowledgeItem = {
|
||||
}> | Array<any>;
|
||||
type: 'qa' | 'side-info' | 'chat-history' | 'url' | 'coding',
|
||||
updated: string,
|
||||
sourceCode?: string,
|
||||
}
|
||||
|
||||
export type ReflectAction = BaseAction & {
|
||||
@ -89,29 +90,29 @@ export interface BraveSearchResponse {
|
||||
|
||||
export interface SerperSearchResponse {
|
||||
knowledgeGraph?: {
|
||||
title: string;
|
||||
type: string;
|
||||
website: string;
|
||||
imageUrl: string;
|
||||
description: string;
|
||||
descriptionSource: string;
|
||||
descriptionLink: string;
|
||||
attributes: { [k: string]: string; };
|
||||
title: string;
|
||||
type: string;
|
||||
website: string;
|
||||
imageUrl: string;
|
||||
description: string;
|
||||
descriptionSource: string;
|
||||
descriptionLink: string;
|
||||
attributes: { [k: string]: string; };
|
||||
},
|
||||
organic: {
|
||||
title: string;
|
||||
link: string;
|
||||
snippet: string;
|
||||
date: string;
|
||||
siteLinks?: { title: string; link: string; }[];
|
||||
position: number,
|
||||
title: string;
|
||||
link: string;
|
||||
snippet: string;
|
||||
date: string;
|
||||
siteLinks?: { title: string; link: string; }[];
|
||||
position: number,
|
||||
}[];
|
||||
topStories?: {
|
||||
title: string;
|
||||
link: string;
|
||||
source: string;
|
||||
data: string;
|
||||
imageUrl: string;
|
||||
title: string;
|
||||
link: string;
|
||||
source: string;
|
||||
data: string;
|
||||
imageUrl: string;
|
||||
}[];
|
||||
relatedSearches?: string[];
|
||||
credits: number;
|
||||
@ -159,11 +160,10 @@ export type ErrorAnalysisResponse = {
|
||||
questionsToAnswer: string[];
|
||||
};
|
||||
|
||||
export interface SearchResult {
|
||||
title: string;
|
||||
url: string;
|
||||
description: string;
|
||||
}
|
||||
export type SearchResult =
|
||||
| { title: string; url: string; description: string }
|
||||
| { title: string; link: string; snippet: string };
|
||||
|
||||
|
||||
export interface QueryResult {
|
||||
query: string;
|
||||
@ -232,6 +232,7 @@ export interface ChatCompletionResponse {
|
||||
total_tokens: number;
|
||||
};
|
||||
visitedURLs?: string[];
|
||||
readURLs?: string[];
|
||||
}
|
||||
|
||||
export interface ChatCompletionChunk {
|
||||
@ -251,6 +252,7 @@ export interface ChatCompletionChunk {
|
||||
}>;
|
||||
usage?: any;
|
||||
visitedURLs?: string[];
|
||||
readURLs?: string[];
|
||||
}
|
||||
|
||||
// Tracker Types
|
||||
|
||||
95
src/utils/url-tools.ts
Normal file
95
src/utils/url-tools.ts
Normal file
@ -0,0 +1,95 @@
|
||||
export function normalizeUrl(urlString: string, debug = false): string {
|
||||
if (!urlString?.trim()) {
|
||||
throw new Error('Empty URL');
|
||||
}
|
||||
|
||||
urlString = urlString.trim();
|
||||
|
||||
if (!/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(urlString)) {
|
||||
urlString = 'https://' + urlString;
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(urlString);
|
||||
|
||||
url.hostname = url.hostname.toLowerCase();
|
||||
if (url.hostname.startsWith('www.')) {
|
||||
url.hostname = url.hostname.slice(4);
|
||||
}
|
||||
|
||||
if ((url.protocol === 'http:' && url.port === '80') ||
|
||||
(url.protocol === 'https:' && url.port === '443')) {
|
||||
url.port = '';
|
||||
}
|
||||
|
||||
// Path normalization with error tracking
|
||||
url.pathname = url.pathname
|
||||
.split('/')
|
||||
.map(segment => {
|
||||
try {
|
||||
return decodeURIComponent(segment);
|
||||
} catch (e) {
|
||||
if (debug) console.error(`Failed to decode path segment: ${segment}`, e);
|
||||
return segment;
|
||||
}
|
||||
})
|
||||
.join('/')
|
||||
.replace(/\/+/g, '/')
|
||||
.replace(/\/+$/, '') || '/';
|
||||
|
||||
// Query parameter normalization with error details
|
||||
const searchParams = new URLSearchParams(url.search);
|
||||
const sortedParams = Array.from(searchParams.entries())
|
||||
.map(([key, value]) => {
|
||||
if (value === '') return [key, ''];
|
||||
try {
|
||||
const decodedValue = decodeURIComponent(value);
|
||||
if (encodeURIComponent(decodedValue) === value) {
|
||||
return [key, decodedValue];
|
||||
}
|
||||
} catch (e) {
|
||||
if (debug) console.error(`Failed to decode query param ${key}=${value}`, e);
|
||||
}
|
||||
return [key, value];
|
||||
})
|
||||
.sort(([keyA], [keyB]) => keyA.localeCompare(keyB))
|
||||
.filter(([key]) => key !== '');
|
||||
|
||||
url.search = new URLSearchParams(sortedParams).toString();
|
||||
|
||||
// Fragment handling with validation
|
||||
if (url.hash === '#' || url.hash === '#top' || url.hash === '#/' || !url.hash) {
|
||||
url.hash = '';
|
||||
} else if (url.hash) {
|
||||
try {
|
||||
const decodedHash = decodeURIComponent(url.hash.slice(1));
|
||||
const encodedBack = encodeURIComponent(decodedHash);
|
||||
// Only use decoded version if it's safe
|
||||
if (encodedBack === url.hash.slice(1)) {
|
||||
url.hash = '#' + decodedHash;
|
||||
}
|
||||
} catch (e) {
|
||||
if (debug) console.error(`Failed to decode fragment: ${url.hash}`, e);
|
||||
}
|
||||
}
|
||||
|
||||
let normalizedUrl = url.toString();
|
||||
|
||||
// Final URL normalization with validation
|
||||
try {
|
||||
const decodedUrl = decodeURIComponent(normalizedUrl);
|
||||
const encodedBack = encodeURIComponent(decodedUrl);
|
||||
// Only use decoded version if it's safe
|
||||
if (encodedBack === normalizedUrl) {
|
||||
normalizedUrl = decodedUrl;
|
||||
}
|
||||
} catch (e) {
|
||||
if (debug) console.error('Failed to decode final URL', e);
|
||||
}
|
||||
|
||||
return normalizedUrl;
|
||||
} catch (error) {
|
||||
// Main URL parsing error - this one we should throw
|
||||
throw new Error(`Invalid URL "${urlString}": ${error}`);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user