diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..3f5ecd1 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,4 @@ +# These are supported funding model platforms + +github: [Fosowl ]# Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] + diff --git a/README.md b/README.md index b5d6564..e6afb45 100644 --- a/README.md +++ b/README.md @@ -159,9 +159,9 @@ Set the desired provider in the `config.ini`. See below for a list of API provid ```sh [MAIN] is_local = False -provider_name = openai -provider_model = gpt-4o -provider_server_address = 127.0.0.1:5000 +provider_name = google +provider_model = gemini-2.0-flash +provider_server_address = 127.0.0.1:5000 # doesn't matter ``` Warning: Make sure there is not trailing space in the config. @@ -179,6 +179,10 @@ Example: export `TOGETHER_API_KEY="xxxxx"` | togetherAI | No | Use together AI API (non-private) | | google | No | Use google gemini API (non-private) | +*We advice against using gpt-4o or other closedAI models*, performance are poor for web browsing and task planning. + +Please also note that coding/bash might fail with gemini, it seem to ignore our prompt for format to respect, which are optimized for deepseek r1. + Next step: [Start services and run AgenticSeek](#Start-services-and-Run) *See the **Known issues** section if you are having issues* diff --git a/config.ini b/config.ini index 4f5bc7c..fd2e9b7 100644 --- a/config.ini +++ b/config.ini @@ -1,9 +1,9 @@ [MAIN] is_local = True -provider_name = openai -provider_model = gpt-4o -provider_server_address = 192.168.1.6:11434/v1 -agent_name = Joey +provider_name = ollama +provider_model = deepseek-r1:14b +provider_server_address = 127.0.0.1:11434 +agent_name = Friday recover_last_session = False save_session = False speak = False diff --git a/requirements.txt b/requirements.txt index 638b085..55bedaf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ +kokoro==0.9.4 +certifi==2025.4.26 fastapi>=0.115.12 flask>=3.1.0 celery>=5.5.1 @@ -18,10 +20,10 @@ torch>=2.4.1 python-dotenv>=1.0.0 ollama>=0.4.7 scipy>=1.9.3 -kokoro>=0.7.12 soundfile>=0.13.1 protobuf>=3.20.3 termcolor>=2.4.0 +pypdf>=5.4.0 ipython>=8.13.0 pyaudio>=0.2.14 librosa>=0.10.2.post1 @@ -39,6 +41,7 @@ fake_useragent>=2.1.0 selenium_stealth>=1.0.6 undetected-chromedriver>=3.5.5 sentencepiece>=0.2.0 +tqdm>4 openai sniffio tqdm>4 @@ -46,5 +49,3 @@ python-dotenv>=1.0.0 # if use chinese ordered_set pypinyin -cn2an -jieba diff --git a/sources/agents/agent.py b/sources/agents/agent.py index 4e10c59..f3efb41 100644 --- a/sources/agents/agent.py +++ b/sources/agents/agent.py @@ -123,6 +123,8 @@ class Agent(): """ start_tag = "" end_tag = "" + if text is None: + return None start_idx = text.find(start_tag) end_idx = text.rfind(end_tag)+8 return text[start_idx:end_idx] diff --git a/sources/agents/planner_agent.py b/sources/agents/planner_agent.py index afb91d2..2b6a34c 100644 --- a/sources/agents/planner_agent.py +++ b/sources/agents/planner_agent.py @@ -151,7 +151,7 @@ class PlannerAgent(Agent): return [] agents_tasks = self.parse_agent_tasks(answer) if agents_tasks == []: - prompt = f"Failed to parse the tasks. Please make a plan within ```json. Do not ask for clarification.\n" + prompt = f"Failed to parse the tasks. Please write down your task followed by a json plan within ```json. Do not ask for clarification.\n" pretty_print("Failed to make plan. Retrying...", color="warning") continue self.show_plan(agents_tasks, answer) diff --git a/sources/browser.py b/sources/browser.py index 66ae583..9f7141d 100644 --- a/sources/browser.py +++ b/sources/browser.py @@ -13,6 +13,8 @@ from fake_useragent import UserAgent from selenium_stealth import stealth import undetected_chromedriver as uc import chromedriver_autoinstaller +import certifi +import ssl import time import random import os @@ -28,6 +30,7 @@ from sources.utility import pretty_print, animate_thinking from sources.logger import Logger + def get_chrome_path() -> str: """Get the path to the Chrome executable.""" if sys.platform.startswith("win"): diff --git a/sources/llm_provider.py b/sources/llm_provider.py index 4b55bcb..32b8279 100644 --- a/sources/llm_provider.py +++ b/sources/llm_provider.py @@ -72,6 +72,8 @@ class Provider: except ModuleNotFoundError as e: raise ModuleNotFoundError(f"{str(e)}\nA import related to provider {self.provider_name} was not found. Is it installed ?") except Exception as e: + if "try again later" in str(e).lower(): + return f"{self.provider_name} server is overloaded. Please try again later." if "refused" in str(e): return f"Server {self.server_ip} seem offline. Unable to answer." raise Exception(f"Provider {self.provider_name} failed: {str(e)}") from e @@ -214,7 +216,7 @@ class Provider: """ base_url = self.server_ip if self.is_local: - raise Exception("Google Gemini is not available for local use.") + raise Exception("Google Gemini is not available for local use. Change config.ini") client = OpenAI(api_key=self.api_key, base_url="https://generativelanguage.googleapis.com/v1beta/openai/") try: @@ -237,6 +239,8 @@ class Provider: """ from together import Together client = Together(api_key=self.api_key) + if self.is_local: + raise Exception("Together AI is not available for local use. Change config.ini") try: response = client.chat.completions.create( @@ -257,6 +261,8 @@ class Provider: Use deepseek api to generate text. """ client = OpenAI(api_key=self.api_key, base_url="https://api.deepseek.com") + if self.is_local: + raise Exception("Deepseek (API) is not available for local use. Change config.ini") try: response = client.chat.completions.create( model="deepseek-chat", diff --git a/sources/text_to_speech.py b/sources/text_to_speech.py index e881f50..dff9d3f 100644 --- a/sources/text_to_speech.py +++ b/sources/text_to_speech.py @@ -9,7 +9,10 @@ from kokoro import KPipeline from IPython.display import display, Audio import soundfile as sf -from sources.utility import pretty_print, animate_thinking +if __name__ == "__main__": + from utility import pretty_print, animate_thinking +else: + from sources.utility import pretty_print, animate_thinking class Speech(): """ @@ -140,6 +143,7 @@ class Speech(): return sentence if __name__ == "__main__": + # TODO add info message for cn2an, jieba chinese related import sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) speech = Speech() tosay_en = """ diff --git a/sources/tools/BashInterpreter.py b/sources/tools/BashInterpreter.py index cbc644a..e6ae113 100644 --- a/sources/tools/BashInterpreter.py +++ b/sources/tools/BashInterpreter.py @@ -1,15 +1,14 @@ -import sys +import os, sys import re from io import StringIO import subprocess -if __name__ == "__main__": - from tools import Tools - from safety import is_unsafe -else: - from sources.tools.tools import Tools - from sources.tools.safety import is_unsafe +if __name__ == "__main__": # if running as a script for individual testing + sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) + +from sources.tools.tools import Tools +from sources.tools.safety import is_unsafe class BashInterpreter(Tools): """ diff --git a/sources/tools/C_Interpreter.py b/sources/tools/C_Interpreter.py index 45c501f..2c0cbf9 100644 --- a/sources/tools/C_Interpreter.py +++ b/sources/tools/C_Interpreter.py @@ -1,12 +1,12 @@ import subprocess -import os +import os, sys import tempfile import re -if __name__ == "__main__": - from tools import Tools -else: - from sources.tools.tools import Tools +if __name__ == "__main__": # if running as a script for individual testing + sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) + +from sources.tools.tools import Tools class CInterpreter(Tools): """ diff --git a/sources/tools/GoInterpreter.py b/sources/tools/GoInterpreter.py index 2f6053b..ab23f76 100644 --- a/sources/tools/GoInterpreter.py +++ b/sources/tools/GoInterpreter.py @@ -1,12 +1,12 @@ import subprocess -import os +import os, sys import tempfile import re -if __name__ == "__main__": - from tools import Tools -else: - from sources.tools.tools import Tools +if __name__ == "__main__": # if running as a script for individual testing + sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) + +from sources.tools.tools import Tools class GoInterpreter(Tools): """ diff --git a/sources/tools/JavaInterpreter.py b/sources/tools/JavaInterpreter.py index a6bbc0a..4658c13 100644 --- a/sources/tools/JavaInterpreter.py +++ b/sources/tools/JavaInterpreter.py @@ -1,12 +1,12 @@ import subprocess -import os +import os, sys import tempfile import re -if __name__ == "__main__": - from tools import Tools -else: - from sources.tools.tools import Tools +if __name__ == "__main__": # if running as a script for individual testing + sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) + +from sources.tools.tools import Tools class JavaInterpreter(Tools): """ diff --git a/sources/tools/PyInterpreter.py b/sources/tools/PyInterpreter.py index 350e590..2b4f965 100644 --- a/sources/tools/PyInterpreter.py +++ b/sources/tools/PyInterpreter.py @@ -4,10 +4,10 @@ import os import re from io import StringIO -if __name__ == "__main__": - from tools import Tools -else: - from sources.tools.tools import Tools +if __name__ == "__main__": # if running as a script for individual testing + sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) + +from sources.tools.tools import Tools class PyInterpreter(Tools): """ diff --git a/sources/tools/fileFinder.py b/sources/tools/fileFinder.py index 90130b7..2b30667 100644 --- a/sources/tools/fileFinder.py +++ b/sources/tools/fileFinder.py @@ -1,13 +1,12 @@ -import os +import os, sys import stat import mimetypes import configparser -if __name__ == "__main__": - from tools import Tools -else: - from sources.tools.tools import Tools +if __name__ == "__main__": # if running as a script for individual testing + sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) +from sources.tools.tools import Tools class FileFinder(Tools): """ @@ -30,14 +29,46 @@ class FileFinder(Tools): return file.read() except Exception as e: return f"Error reading file: {e}" + + def read_arbitrary_file(self, file_path: str, file_type: str) -> str: + """ + Reads the content of a file with arbitrary encoding. + Args: + file_path (str): The path to the file to read + Returns: + str: The content of the file in markdown format + """ + mime_type, _ = mimetypes.guess_type(file_path) + if mime_type: + if mime_type.startswith(('image/', 'video/', 'audio/')): + return "can't read file type: image, video, or audio files are not supported." + content_raw = self.read_file(file_path) + if "text" in file_type: + content = content_raw + elif "pdf" in file_type: + from pypdf import PdfReader + reader = PdfReader(file_path) + content = '\n'.join([pt.extract_text() for pt in reader.pages]) + elif "binary" in file_type: + content = content_raw.decode('utf-8', errors='replace') + else: + content = content_raw + return content def get_file_info(self, file_path: str) -> str: + """ + Gets information about a file, including its name, path, type, content, and permissions. + Args: + file_path (str): The path to the file + Returns: + str: A dictionary containing the file information + """ if os.path.exists(file_path): stats = os.stat(file_path) permissions = oct(stat.S_IMODE(stats.st_mode)) file_type, _ = mimetypes.guess_type(file_path) file_type = file_type if file_type else "Unknown" - content = self.read_file(file_path) + content = self.read_arbitrary_file(file_path, file_type) result = { "filename": os.path.basename(file_path), diff --git a/sources/tools/flightSearch.py b/sources/tools/flightSearch.py index 43246f1..3c6f759 100644 --- a/sources/tools/flightSearch.py +++ b/sources/tools/flightSearch.py @@ -1,13 +1,13 @@ -import os +import os, sys import requests import dotenv dotenv.load_dotenv() -if __name__ == "__main__": - from tools import Tools -else: - from sources.tools.tools import Tools +if __name__ == "__main__": # if running as a script for individual testing + sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) + +from sources.tools.tools import Tools class FlightSearch(Tools): def __init__(self, api_key: str = None): diff --git a/sources/tools/mcpFinder.py b/sources/tools/mcpFinder.py index c055405..96e07f5 100644 --- a/sources/tools/mcpFinder.py +++ b/sources/tools/mcpFinder.py @@ -1,12 +1,13 @@ -import os +import os, sys import requests from urllib.parse import urljoin from typing import Dict, Any, Optional -if __name__ == "__main__": - from tools import Tools -else: - from sources.tools.tools import Tools +from sources.tools.tools import Tools + +if __name__ == "__main__": # if running as a script for individual testing + sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) + class MCP_finder(Tools): """ diff --git a/sources/tools/searxSearch.py b/sources/tools/searxSearch.py index 83a6203..15a4438 100644 --- a/sources/tools/searxSearch.py +++ b/sources/tools/searxSearch.py @@ -2,10 +2,10 @@ import requests from bs4 import BeautifulSoup import os -if __name__ == "__main__": - from tools import Tools -else: - from sources.tools.tools import Tools +if __name__ == "__main__": # if running as a script for individual testing + sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) + +from sources.tools.tools import Tools class searxSearch(Tools): def __init__(self, base_url: str = None): diff --git a/sources/tools/tools.py b/sources/tools/tools.py index 3b71a5f..80bf25a 100644 --- a/sources/tools/tools.py +++ b/sources/tools/tools.py @@ -14,13 +14,17 @@ For example: print("Hello world") ``` This is then executed by the tool with its own class implementation of execute(). -A tool is not just for code tool but also API, internet, etc.. +A tool is not just for code tool but also API, internet search, MCP, etc.. """ import sys import os import configparser from abc import abstractmethod + +if __name__ == "__main__": # if running as a script for individual testing + sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + from sources.logger import Logger class Tools(): diff --git a/sources/tools/webSearch.py b/sources/tools/webSearch.py index 6cf6e5f..3da083d 100644 --- a/sources/tools/webSearch.py +++ b/sources/tools/webSearch.py @@ -5,14 +5,8 @@ import dotenv dotenv.load_dotenv() -if __name__ == "__main__": - import sys - sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - from utility import animate_thinking, pretty_print - from tools import Tools -else: - from sources.tools.tools import Tools - from sources.utility import animate_thinking, pretty_print +from sources.tools.tools import Tools +from sources.utility import animate_thinking, pretty_print """ WARNING diff --git a/tests/test_browser_agent_parsing.py b/tests/test_browser_agent_parsing.py index e8c5b9f..aac95bc 100644 --- a/tests/test_browser_agent_parsing.py +++ b/tests/test_browser_agent_parsing.py @@ -17,13 +17,13 @@ class TestBrowserAgentParsing(unittest.TestCase): # Test various link formats test_text = """ Check this out: https://thriveonai.com/15-ai-startups-in-japan-to-take-note-of, and www.google.com! - Also try https://test.org/about?page=1, hey this one as well bro https://weatherstack.com/documentation. + Also try https://test.org/about?page=1, hey this one as well bro https://weatherstack.com/documentation/. """ expected = [ "https://thriveonai.com/15-ai-startups-in-japan-to-take-note-of", "www.google.com", "https://test.org/about?page=1", - "https://weatherstack.com/documentation" + "https://weatherstack.com/documentation", ] result = self.agent.extract_links(test_text) self.assertEqual(result, expected)