diff --git a/README.md b/README.md index 7e89c52..0869eb2 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,10 @@ ## **Installation** +Make sure you have chrome driver and docker installed. + +For issues related to chrome driver, see the **Chromedriver** section. + ### 1️⃣ **Clone the repository and setup** ```sh @@ -219,6 +223,29 @@ provider_server_address = 127.0.0.1:5000 `provider_server_address`: can be set to anything if you are not using the server provider. +# Known issues + +## Chromedriver Issues + +**Known error #1:** *chromedriver mismatch* + +`Exception: Failed to initialize browser: Message: session not created: This version of ChromeDriver only supports Chrome version 113 +Current browser version is 134.0.6998.89 with binary path` + +This happen if there is a mismatch between your browser and chromedriver version. + +You need to navigate to download the latest version: + +https://developer.chrome.com/docs/chromedriver/downloads + +If you're using Chrome version 115 or newer go to: + +https://googlechromelabs.github.io/chrome-for-testing/ + +And download the chromedriver version matching your OS. + +![alt text](./media/chromedriver_readme.png) + ## FAQ **Q: What hardware do I need?** diff --git a/main.py b/main.py index 3f2d1a6..002c0dc 100755 --- a/main.py +++ b/main.py @@ -41,10 +41,6 @@ def main(): name="File Agent", prompt_path="prompts/file_agent.txt", provider=provider), - PlannerAgent(model=config["MAIN"]["provider_model"], - name="Planner", - prompt_path="prompts/planner_agent.txt", - provider=provider), BrowserAgent(model=config["MAIN"]["provider_model"], name="Browser", prompt_path="prompts/browser_agent.txt", diff --git a/media/chromedriver_readme.png b/media/chromedriver_readme.png new file mode 100644 index 0000000..fb68527 Binary files /dev/null and b/media/chromedriver_readme.png differ diff --git a/prompts/file_agent.txt b/prompts/file_agent.txt index 82f2931..2fab37e 100644 --- a/prompts/file_agent.txt +++ b/prompts/file_agent.txt @@ -44,7 +44,7 @@ User: "I need to find the file config.txt and read its contents." Assistant: I’ll use file_finder to locate the file: -```file_finder +```file_finder:read config.txt ``` diff --git a/sources/agents/browser_agent.py b/sources/agents/browser_agent.py index 758a4dc..e226a3d 100644 --- a/sources/agents/browser_agent.py +++ b/sources/agents/browser_agent.py @@ -15,9 +15,8 @@ class BrowserAgent(Agent): self.tools = { "web_search": searxSearch(), } - self.role = "deep research and web search" + self.role = "web search, internet, google" self.browser = Browser() - self.browser.go_to("https://github.com/") self.search_history = [] self.navigable_links = [] self.notes = [] @@ -50,7 +49,7 @@ class BrowserAgent(Agent): {search_choice} Your goal is to find accurate and complete information to satisfy the user’s request. User request: {user_prompt} - To proceed, choose a relevant link from the search results. Announce your choice by saying: "I want to navigate to ." + To proceed, choose a relevant link from the search results. Announce your choice by saying: "I want to navigate to " Do not explain your choice. """ @@ -58,27 +57,44 @@ class BrowserAgent(Agent): remaining_links = self.get_unvisited_links() remaining_links_text = remaining_links if remaining_links is not None else "No links remaining, proceed with a new search." return f""" - \nYou are currently browsing the web. Not the user, you are the browser. - - Page content: + You are a web browser. + You are currently on this webpage: {page_text} - You can navigate to these links: + You can navigate to these navigation links: {remaining_links} - You must choose a link (write it down) to navigate to, or go back. - For exemple you can say: i want to go to www.wikipedia.org/cats + Your task: + 1. Decide if the current page answers the user’s query: {user_prompt} + - If it does, take notes of the useful information, write down source, link or reference, then move to a new page. + - If it does and you are 100% certain that it provide a definive answer, say REQUEST_EXIT + - If it doesn’t, say: Error: This page does not answer the user’s query then go back or navigate to another link. + 2. Navigate by either: + - Navigate to a navigation links (write the full URL, e.g., www.example.com/cats). + - If no link seems helpful, say: GO_BACK. + + Recap of note taking: + If useful -> Note: [Briefly summarize the key information that answers the user’s query.] + Do not write "The page talk about ...", write your finding on the page and how they contribute to an answer. + If not useful -> Error: [Explain why the page doesn’t help.] + + Example 1 (useful page, no need of going futher): + Note: According to karpathy site (https://karpathy.github.io/) LeCun net is the earliest real-world application of a neural net" + No link seem useful to provide futher information. GO_BACK - Follow up with a summary of the page content (of the current page, not of the link), for example: - Summary: According to https://karpathy.github.io/ LeCun net is the earliest real-world application of a neural net" - The summary should include any useful finding that are useful in answering user query. - If a website does not have usefull information say Error, for exemple: - Error: This forum does not discus anything that can answer the user query - Be short, concise, direct. + Example 2 (not useful, but related link): + Error: This forum reddit.com/welcome does not discuss anything related to the user’s query. + There is a link that could lead to the information, I want to navigate to http://reddit.com/r/locallama - If no link seem appropriate, please say "GO_BACK". - Remember, you seek the information the user want. - The user query was : {user_prompt} + Example 3 (not useful, no related links): + Error: x.com does not discuss anything related to the user’s query and no navigation link are usefull + GO_BACK + + Example 3 (not useful, no related links): + Note: I found on github.com that the creator of agenticSeek is fosowl. + Given this information, given this I should exit the web browser. REQUEST_EXIT + + Remember, the user asked: {user_prompt} """ def llm_decide(self, prompt): @@ -122,11 +138,11 @@ class BrowserAgent(Agent): def save_notes(self, text): lines = text.split('\n') for line in lines: - if "summary" in line.lower(): + if "note" in line.lower(): self.notes.append(line) def conclude_prompt(self, user_query): - annotated_notes = [f"{i+1}: {note.lower().replace('summary:', '')}" for i, note in enumerate(self.notes)] + annotated_notes = [f"{i+1}: {note.lower().replace('note:', '')}" for i, note in enumerate(self.notes)] search_note = '\n'.join(annotated_notes) print("AI research notes:\n", search_note) return f""" @@ -174,7 +190,6 @@ class BrowserAgent(Agent): self.memory.push('user', prompt) answer, reasoning = self.llm_request(prompt) pretty_print(answer, color="output") - speech_module.speak(answer) return answer, reasoning if __name__ == "__main__": diff --git a/sources/agents/file_agent.py b/sources/agents/file_agent.py index cc16adb..3f3c0d1 100644 --- a/sources/agents/file_agent.py +++ b/sources/agents/file_agent.py @@ -14,7 +14,7 @@ class FileAgent(Agent): "file_finder": FileFinder(), "bash": BashInterpreter() } - self.role = "files operations" + self.role = "find, read, write, edit files" def process(self, prompt, speech_module) -> str: complete = False diff --git a/sources/agents/planner_agent.py b/sources/agents/planner_agent.py index aaafc3a..1760c33 100644 --- a/sources/agents/planner_agent.py +++ b/sources/agents/planner_agent.py @@ -3,7 +3,7 @@ from sources.utility import pretty_print, animate_thinking from sources.agents.agent import Agent from sources.agents.code_agent import CoderAgent from sources.agents.file_agent import FileAgent -from sources.agents.casual_agent import CasualAgent +from sources.agents.browser_agent import BrowserAgent from sources.tools.tools import Tools class PlannerAgent(Agent): @@ -19,9 +19,9 @@ class PlannerAgent(Agent): self.agents = { "coder": CoderAgent(model, name, prompt_path, provider), "file": FileAgent(model, name, prompt_path, provider), - "web": CasualAgent(model, name, prompt_path, provider) + "web": BrowserAgent(model, name, prompt_path, provider) } - self.role = "complex programming tasks and web research" + self.role = "Manage complex tasks" self.tag = "json" def parse_agent_tasks(self, text): diff --git a/sources/browser.py b/sources/browser.py index 9a5acd3..917498b 100644 --- a/sources/browser.py +++ b/sources/browser.py @@ -12,6 +12,8 @@ from bs4 import BeautifulSoup import markdownify import logging import sys +import re +from urllib.parse import urlparse class Browser: def __init__(self, headless=False, anticaptcha_install=False): @@ -57,7 +59,8 @@ class Browser: os.path.join(os.environ.get("LOCALAPPDATA", ""), "Google\\Chrome\\Application\\chrome.exe") # User install ] elif sys.platform.startswith("darwin"): # macOS - paths = ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"] + paths = ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", + "/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta"] else: # Linux paths = ["/usr/bin/google-chrome", "/usr/bin/chromium-browser", "/usr/bin/chromium"] @@ -81,15 +84,15 @@ class Browser: def is_sentence(self, text): """Check if the text qualifies as a meaningful sentence or contains important error codes.""" text = text.strip() + error_codes = ["404", "403", "500", "502", "503"] if any(code in text for code in error_codes): return True - words = text.split() + words = re.findall(r'\w+', text, re.UNICODE) word_count = len(words) - has_punctuation = text.endswith(('.', '!', '?')) + has_punctuation = any(text.endswith(p) for p in ['.', ',', ',', '!', '?', '。', '!', '?', '।', '۔']) is_long_enough = word_count > 5 - has_letters = any(word.isalpha() for word in words) - return (word_count >= 5 and (has_punctuation or is_long_enough) and has_letters) + return (word_count >= 5 and (has_punctuation or is_long_enough)) def get_text(self): """Get page text and convert it to README (Markdown) format.""" @@ -104,15 +107,14 @@ class Browser: lines = (line.strip() for line in text.splitlines()) chunks = (phrase.strip() for line in lines for phrase in line.split(" ")) text = "\n".join(chunk for chunk in chunks if chunk and self.is_sentence(chunk)) - - markdown_text = markdownify.markdownify(text, heading_style="ATX") - - return markdown_text + #markdown_text = markdownify.markdownify(text, heading_style="ATX") + return "[Start of page]\n" + text + "\n[End of page]" except Exception as e: self.logger.error(f"Error getting text: {str(e)}") return None def clean_url(self, url): + """Clean URL to keep only the part needed for navigation to the page""" clean = url.split('#')[0] parts = clean.split('?', 1) base_url = parts[0] @@ -127,6 +129,22 @@ class Browser: if essential_params: return f"{base_url}?{'&'.join(essential_params)}" return base_url + + def is_link_valid(self, url): + """Check if a URL is a valid link (page, not related to icon or metadata).""" + if len(url) > 64: + return False + parsed_url = urlparse(url) + if not parsed_url.scheme or not parsed_url.netloc: + return False + if re.search(r'/\d+$', parsed_url.path): + return False + image_extensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.webp'] + metadata_extensions = ['.ico', '.xml', '.json', '.rss', '.atom'] + for ext in image_extensions + metadata_extensions: + if url.lower().endswith(ext): + return False + return True def get_navigable(self): """Get all navigable links on the current page.""" @@ -144,7 +162,7 @@ class Browser: }) self.logger.info(f"Found {len(links)} navigable links") - return [self.clean_url(link['url']) for link in links if link['is_displayed'] == True and len(link) < 256] + return [self.clean_url(link['url']) for link in links if (link['is_displayed'] == True and self.is_link_valid(link['url']))] except Exception as e: self.logger.error(f"Error getting navigable links: {str(e)}") return [] @@ -210,7 +228,7 @@ if __name__ == "__main__": browser = Browser(headless=False) try: - browser.go_to("https://karpathy.github.io/") + browser.go_to("https://github.com/Fosowl/agenticSeek") text = browser.get_text() print("Page Text in Markdown:") print(text) diff --git a/sources/interaction.py b/sources/interaction.py index 6ae82ff..6a1282a 100644 --- a/sources/interaction.py +++ b/sources/interaction.py @@ -102,6 +102,7 @@ class Interaction: self.current_agent = agent # get history from previous agent, good ? self.current_agent.memory.push('user', self.last_query) + self.current_agent.memory.push('assistant', self.last_answer) self.last_answer, _ = agent.process(self.last_query, self.speech) def show_answer(self) -> None: diff --git a/sources/tools/searxSearch.py b/sources/tools/searxSearch.py index 9b8a0fb..7f3cac7 100644 --- a/sources/tools/searxSearch.py +++ b/sources/tools/searxSearch.py @@ -91,10 +91,10 @@ class searxSearch(Tools): description = article.find('p', class_='content').text.strip() if article.find('p', class_='content') else "No Description" results.append(f"Title:{title}\nSnippet:{description}\nLink:{url}") if len(results) == 0: - raise Exception("Searx search failed. did you run start_services.sh? Did docker die?") + return "No search results, web search failed." return "\n\n".join(results) # Return results as a single string, separated by newlines except requests.exceptions.RequestException as e: - return f"Error during search: {str(e)}" + raise Exception("\nSearxng search failed. did you run start_services.sh? is docker still running?") from e def execution_failure_check(self, output: str) -> bool: """