feat: optimize prompt, coding, reflect

This commit is contained in:
Han Xiao 2025-02-24 13:16:18 +08:00
parent 5c91d5b4ad
commit c02588a92c
9 changed files with 338 additions and 219 deletions

View File

@ -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 },

View File

@ -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": {

View File

@ -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
};
}

View File

@ -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);

View File

@ -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}}`;
}

View File

@ -446,6 +446,7 @@ export async function evaluateAnswer(
}
const allKnowledge = await fetchSourceContent(uniqueURLs, trackers);
visitedURLs.push(...uniqueURLs);
if (!allKnowledge.trim()) {
return {

View File

@ -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.'`)
});

View File

@ -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
View 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}`);
}
}