Skip to content

Commit

Permalink
Refactor web search skill to use new web browsing tool
Browse files Browse the repository at this point in the history
  • Loading branch information
miurla committed Aug 12, 2023
1 parent a8cd966 commit 7772acc
Show file tree
Hide file tree
Showing 17 changed files with 490 additions and 40 deletions.
3 changes: 1 addition & 2 deletions src/agents/base/TestExecuter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,10 @@ export class TestExecuter extends Executer {
const task = new Promise<void>((resolve) => {
setTimeout(async () => {
const id = `task + ${i}`;
await this.handleMessage({
await this.handlers.handleMessage({
content: `Test message ${i}`,
title: `Task description ${i}`,
type: 'task',
style: 'task',
taskId: `${i}`,
id,
});
Expand Down
4 changes: 2 additions & 2 deletions src/agents/elf/registory/skillRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
// SkillSaver,
TextCompletion,
// WebLoader,
// WebSearch,
WebSearch,
// YoutubeSearch,
} from '../skills';
import { Skill } from '../skills/skill';
Expand Down Expand Up @@ -64,7 +64,7 @@ export class SkillRegistry {
static getSkillClasses(): (typeof Skill)[] {
const skills: (typeof Skill)[] = [
TextCompletion,
// WebSearch,
WebSearch,
// AirtableSaver,
// CodeReader,
// CodeWriter,
Expand Down
3 changes: 1 addition & 2 deletions src/agents/elf/registory/taskRegistry.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import _ from 'lodash';
import { AgentTask, AgentMessage, TaskOutputs } from '@/types';
import { ChatOpenAI } from 'langchain/chat_models/openai';
import { parseTasks } from '@/utils/task';
Expand Down Expand Up @@ -133,7 +132,7 @@ export class TaskRegistry {
}

reorderTasks(): void {
this.tasks = _.sortBy(this.tasks, ['priority', 'task_id']);
this.tasks.sort((a, b) => a.id - b.id);
}

async reflectOnOutput(
Expand Down
8 changes: 2 additions & 6 deletions src/agents/elf/skills/presets/webSearch.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AgentTask } from '@/types';
import { webBrowsing } from '@/agents/babydeeragi/tools/webBrowsing';
import { webBrowsing } from '@/agents/elf/tools/search/webBrowsing';
import { Skill } from '../skill';

// This skill is Specialized for web browsing
Expand All @@ -17,19 +17,15 @@ export class WebSearch extends Skill {
objective: string,
): Promise<string> {
if (!this.valid) return '';

const taskOutput =
(await webBrowsing(
objective,
task,
dependentTaskOutputs,
this.messageCallback,
undefined,
this.isRunningRef,
this.handleMessage,
this.verbose,
undefined,
this.language,
this.abortController.signal,
)) ?? '';

return taskOutput;
Expand Down
6 changes: 1 addition & 5 deletions src/agents/elf/skills/skill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { HumanChatMessage } from 'langchain/schema';
import { v4 as uuidv4 } from 'uuid';

export type SkillType = 'normal' | 'dev';
export type SkillExecutionLocation = 'client' | 'server';

export class Skill {
name: string = 'base_kill';
Expand All @@ -15,7 +14,6 @@ export class Skill {
apiKeysRequired: Array<string | Array<string>> = [];
valid: boolean;
apiKeys: { [key: string]: string };
executionLocation: SkillExecutionLocation = 'client'; // 'client' or 'server'
// for UI
handleMessage: (message: AgentMessage) => void;
verbose: boolean;
Expand Down Expand Up @@ -112,7 +110,6 @@ export class Skill {
id,
content: token,
title: `${task.task}`,
style: 'task',
type: task.skill,
icon: '🤖',
taskId: task.id.toString(),
Expand All @@ -136,11 +133,10 @@ export class Skill {
content: response.text,
title: task.task,
icon: '✅',
style: 'task',
type: task.skill,
status: 'complete',
options: {
dependentTaskIds: task.dependentTaskIds?.join(',') ?? '',
dependentTaskIds: task.dependentTaskIds?.join(', ') ?? '',
},
});
return response.text;
Expand Down
39 changes: 39 additions & 0 deletions src/agents/elf/tools/search/largeTextExtract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { AgentMessage, AgentTask } from '@/types';
import { relevantInfoExtractionAgent } from './relevantInfoExtraction/agent';

export const largeTextExtract = async (
id: string,
objective: string,
largeString: string,
task: AgentTask,
callback?: (message: AgentMessage) => void,
) => {
const chunkSize = 15000;
const overlap = 500;
let notes = '';

// for status message
const total = Math.ceil(largeString.length / (chunkSize - overlap));

for (let i = 0; i < largeString.length; i += chunkSize - overlap) {
callback?.({
content: ` - chunk ${i / (chunkSize - overlap) + 1} of ${total}\n`,
style: 'log',
type: task.skill,
id: id,
taskId: task.id.toString(),
status: 'running',
});

const chunk = largeString.slice(i, i + chunkSize);

const response = await relevantInfoExtractionAgent(
objective,
task.task,
notes,
chunk,
);
notes += response;
}
return notes;
};
34 changes: 34 additions & 0 deletions src/agents/elf/tools/search/prompt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
export const searchQueryPrompt = (task: string, dependent_task: string) => {
return `You are an AI assistant tasked with generating a Google search query based on the following task: ${task}.
${
dependent_task.length > 0
? `Consider the results of dependent tasks: ${dependent_task}.`
: ''
}
If the task looks like a search query, return the identical search query as your response.
Search Query:`;
};

export const analystPrompt = (results: string, language: string) => {
return `You are an expert analyst. Rewrite the following information as one report without removing any facts.
Report must be answered in ${language}.
\n###INFORMATION:${results}.\n###REPORT:`;
};

export const textCompletionToolPrompt = (
objective: string,
language: string,
task: string,
dependentTaskOutput: string,
) => {
let prompt = `Complete your assigned task based on the objective and only based on information provided in the dependent task output, if provided.
Your objective: ${objective}. Your task: ${task}
Output must be answered in ${language}.\n
`;
if (dependentTaskOutput !== '') {
prompt += `Your dependent tasks: ${dependentTaskOutput}\n OUTPUT:`;
} else {
prompt += '\nOUTPUT:';
}
return prompt;
};
44 changes: 44 additions & 0 deletions src/agents/elf/tools/search/relevantInfoExtraction/agent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { OpenAIChat } from 'langchain/llms/openai';
import { relevantInfoExtractionPrompt } from './prompt';
import { LLMChain } from 'langchain/chains';
import axios from 'axios';

// TODO: Only client-side requests are allowed.
// To use the environment variable API key, the request must be implemented from the server side.

export const relevantInfoExtractionAgent = async (
objective: string,
task: string,
notes: string,
chunk: string,
signal?: AbortSignal,
) => {
const modelName = 'gpt-3.5-turbo-16k-0613'; // use a fixed model
const prompt = relevantInfoExtractionPrompt();
const llm = new OpenAIChat(
{
modelName,
temperature: 0.7,
maxTokens: 800,
topP: 1,
stop: ['###'],
},
{ baseOptions: { signal: signal } },
);
const chain = new LLMChain({ llm: llm, prompt });
try {
const response = await chain.call({
objective,
task,
notes,
chunk,
});
return response.text;
} catch (error: any) {
if (error.name === 'AbortError') {
return null;
}
console.log('error: ', error);
return 'Failed to extract relevant information.';
}
};
18 changes: 18 additions & 0 deletions src/agents/elf/tools/search/relevantInfoExtraction/prompt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {
ChatPromptTemplate,
HumanMessagePromptTemplate,
SystemMessagePromptTemplate,
} from 'langchain/prompts';

export const relevantInfoExtractionPrompt = () => {
const systemTemplate = `Objective: {objective}\nCurrent Task:{task}`;
const relevantInfoExtractionTemplate = `Analyze the following text and extract information relevant to our objective and current task, and only information relevant to our objective and current task. If there is no relevant information do not say that there is no relevant informaiton related to our objective. ### Then, update or start our notes provided here (keep blank if currently blank): {notes}.### Text to analyze: {chunk}.### Updated Notes:`;
const prompt = ChatPromptTemplate.fromPromptMessages([
SystemMessagePromptTemplate.fromTemplate(systemTemplate),
HumanMessagePromptTemplate.fromTemplate(relevantInfoExtractionTemplate),
]);

prompt.inputVariables = ['objective', 'task', 'notes', 'chunk'];

return prompt;
};
142 changes: 142 additions & 0 deletions src/agents/elf/tools/search/webBrowsing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { simplifySearchResults } from '@/agents/common/tools/webSearch';
import { AgentTask, AgentMessage } from '@/types';
import { analystPrompt, searchQueryPrompt } from './prompt';
import { textCompletionTool } from '../textCompletionTool';
import { largeTextExtract } from './largeTextExtract';
import { v4 as uuidv4 } from 'uuid';
import { webSearch } from './webSearch';
import { webScrape } from '../webScrape';

export const webBrowsing = async (
objective: string,
task: AgentTask,
dependentTasksOutput: string,
messageCallback: (message: AgentMessage) => void,
verbose: boolean = false,
modelName: string = 'gpt-3.5-turbo',
language: string = 'en',
) => {
let id = uuidv4();
const prompt = searchQueryPrompt(
task.task,
dependentTasksOutput.slice(0, 3500),
);
const searchQuery = await textCompletionTool(prompt, id, task, modelName);
const trimmedQuery = searchQuery?.replace(/^"|"$/g, ''); // remove quotes from the search query

let message = `Search query: ${trimmedQuery}\n`;
callbackSearchStatus(id, message, task, messageCallback, verbose);
const searchResults = await webSearch(trimmedQuery || '');
if (!searchResults) {
return 'Failed to search.';
}

const simplifiedSearchResults = simplifySearchResults(searchResults);
message = `✅ Completed search. \nNow reading content.\n`;
callbackSearchStatus(id, message, task, messageCallback, verbose);

let results = '';
let index = 1;
let completedCount = 0;
const MaxCompletedCount = 3;
// Loop through search results
for (const searchResult of simplifiedSearchResults) {
if (completedCount >= MaxCompletedCount) break;

// Extract the URL from the search result
const url = searchResult.link;
let title = `${index}. Reading: ${url} ...`;

message = `${title}\n`;
callbackSearchStatus(id, message, task, messageCallback, verbose);

const content = await webScrape(url);
if (!content) {
let message = ` - Failed to read content. Skipped. \n`;
callbackSearchStatus(id, message, task, messageCallback, verbose);
continue;
}

title = `${index}. Extracting relevant info...`;
message = ` - Content reading completed. Length:${content?.length}. Now extracting relevant info...\n`;
callbackSearchStatus(id, message, task, messageCallback, verbose);

if (content?.length === 0) {
let message = ` - Content too short. Skipped. \n`;
callbackSearchStatus(id, message, task, messageCallback, verbose);
index += 1;
continue;
}

message = ` - Extracting relevant information\n`;
title = `${index}. Extracting relevant info...`;
callbackSearchStatus(id, message, task, messageCallback, verbose);
const info = await largeTextExtract(
id,
objective,
content.slice(0, 20000),
task,
messageCallback,
);

message = ` - Relevant info: ${info
.slice(0, 100)
.replace(/\r?\n/g, '')} ...\n`;

title = `${index}. Relevant info...`;
callbackSearchStatus(id, message, task, messageCallback, verbose);

results += `${info}. `;
index += 1;
completedCount += 1;
}

message = 'Analyzing results...\n';
callbackSearchStatus(id, message, task, messageCallback, verbose);

const outputId = uuidv4();
const ap = analystPrompt(results, language);
const analyzedResults = await textCompletionTool(
ap,
outputId,
task,
modelName,
messageCallback,
);

// callback to search logs
const msg: AgentMessage = {
id,
taskId: task.id.toString(),
type: task.skill,
content: message,
title: task.task,
status: 'complete',
};
messageCallback(msg);

return analyzedResults;
};

const callbackSearchStatus = (
id: string,
message: string,
task: AgentTask,
messageCallback: (message: AgentMessage) => void,
verbose: boolean = false,
) => {
if (verbose) {
console.log(message);
}

messageCallback({
id,
taskId: task.id.toString(),
type: task.skill,
icon: '🔎',
style: 'log',
content: message,
title: task.task,
status: 'running',
});
};
Loading

0 comments on commit 7772acc

Please sign in to comment.