diff --git a/config.ini b/config.ini index 335defe..fe5307f 100644 --- a/config.ini +++ b/config.ini @@ -3,9 +3,9 @@ is_local = True provider_name = ollama provider_model = deepseek-r1:14b provider_server_address = 127.0.0.1:5000 -agent_name = jarvis +agent_name = Friday recover_last_session = True save_session = False speak = True listen = False -work_dir = None \ No newline at end of file +work_dir = none \ No newline at end of file diff --git a/exemples/find_file.png b/exemples/find_file.png deleted file mode 100644 index 11be4fc..0000000 Binary files a/exemples/find_file.png and /dev/null differ diff --git a/exemples/search_and_planner.png b/exemples/search_and_planner.png deleted file mode 100644 index 8fced31..0000000 Binary files a/exemples/search_and_planner.png and /dev/null differ diff --git a/media/exemples/basic_web_search.png b/media/exemples/basic_web_search.png new file mode 100644 index 0000000..c1eaab2 Binary files /dev/null and b/media/exemples/basic_web_search.png differ diff --git a/media/exemples/files_interaction.png b/media/exemples/files_interaction.png new file mode 100644 index 0000000..ac57958 Binary files /dev/null and b/media/exemples/files_interaction.png differ diff --git a/media/exemples/find_files.png b/media/exemples/find_files.png new file mode 100644 index 0000000..d5b4406 Binary files /dev/null and b/media/exemples/find_files.png differ diff --git a/exemples/whale_readme.jpg b/media/whale_readme.jpg similarity index 100% rename from exemples/whale_readme.jpg rename to media/whale_readme.jpg diff --git a/requirements.txt b/requirements.txt index e122ccd..97e15e6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,10 @@ flask==3.1.0 soundfile==0.13.1 protobuf==3.20.3 termcolor==2.5.0 +ipython==9.0.2 gliclass==0.1.8 +pyaudio==0.2.14 +librosa==0.10.2.post1 # if use chinese ordered_set pypinyin diff --git a/setup.py b/setup.py index 1c346cc..b5f3076 100644 --- a/setup.py +++ b/setup.py @@ -30,6 +30,9 @@ setup( "protobuf==3.20.3", "termcolor==2.5.0", "gliclass==0.1.8", + "ipython==7.16.1", + "pyaudio-0.2.14", + "librosa==0.10.2.post1" ], extras_require={ "chinese": [ diff --git a/sources/agents/agent.py b/sources/agents/agent.py index fa5d7d2..37205c3 100644 --- a/sources/agents/agent.py +++ b/sources/agents/agent.py @@ -3,10 +3,13 @@ from typing import Tuple, Callable from abc import abstractmethod import os import random +import time from sources.memory import Memory from sources.utility import pretty_print +random.seed(time.time()) + class executorResult: """ A class to store the result of a tool execution. @@ -106,7 +109,6 @@ class Agent(): if speech_module is None: return messages = ["Please be patient sir, I am working on it.", - "At it, sir. In the meantime, how about a joke?", "Computing... I recommand you have a coffee while I work.", "Hold on, I’m crunching numbers.", "Working on it sir, please let me think."] diff --git a/sources/agents/casual_agent.py b/sources/agents/casual_agent.py index b603317..314c3b8 100644 --- a/sources/agents/casual_agent.py +++ b/sources/agents/casual_agent.py @@ -18,22 +18,23 @@ class CasualAgent(Agent): "file_finder": FileFinder(), "bash": BashInterpreter() } - self.role = "talking, advices and philosophical" + self.role = "talking, advices, events and philosophical" def process(self, prompt, speech_module) -> str: complete = False - exec_success = False self.memory.push('user', prompt) self.wait_message(speech_module) while not complete: - if exec_success: - complete = True animate_thinking("Thinking...", color="status") answer, reasoning = self.llm_request() exec_success, _ = self.execute_modules(answer) answer = self.remove_blocks(answer) self.last_answer = answer + complete = True + for tool in self.tools.values(): + if tool.found_executable_blocks(): + complete = False # AI read results and continue the conversation return answer, reasoning if __name__ == "__main__": diff --git a/sources/agents/code_agent.py b/sources/agents/code_agent.py index ca49b7f..13e2d20 100644 --- a/sources/agents/code_agent.py +++ b/sources/agents/code_agent.py @@ -32,6 +32,7 @@ class CoderAgent(Agent): animate_thinking("Thinking...", color="status") self.wait_message(speech_module) answer, reasoning = self.llm_request() + animate_thinking("Executing code...", color="status") exec_success, _ = self.execute_modules(answer) answer = self.remove_blocks(answer) self.last_answer = answer diff --git a/sources/agents/file_agent.py b/sources/agents/file_agent.py index 3f69ac1..cc16adb 100644 --- a/sources/agents/file_agent.py +++ b/sources/agents/file_agent.py @@ -30,6 +30,10 @@ class FileAgent(Agent): exec_success, _ = self.execute_modules(answer) answer = self.remove_blocks(answer) self.last_answer = answer + complete = True + for name, tool in self.tools.items(): + if tool.found_executable_blocks(): + complete = False # AI read results and continue the conversation return answer, reasoning if __name__ == "__main__": diff --git a/sources/agents/planner_agent.py b/sources/agents/planner_agent.py index d82b4e9..aaafc3a 100644 --- a/sources/agents/planner_agent.py +++ b/sources/agents/planner_agent.py @@ -81,6 +81,9 @@ class PlannerAgent(Agent): speech_module.speak(f"I will {task_name}. I assigned the {task['agent']} agent to the task.") try: self.agents[task['agent'].lower()].process(agent_prompt, None) + pretty_print(f"-- Agent answer ---\n\n", color="output") + self.agents[task['agent'].lower()].show_answer() + pretty_print(f"\n\n", color="output") except Exception as e: pretty_print(f"Error: {e}", color="failure") speech_module.speak(f"I encountered an error: {e}") diff --git a/sources/router.py b/sources/router.py index 8db0ec3..2ebd991 100644 --- a/sources/router.py +++ b/sources/router.py @@ -80,7 +80,7 @@ if __name__ == "__main__": Hey could you search the web for the latest news on the stock market ? """, """ - hey can you give dating advice ? + hey can you give give a list of the files in the current directory ? """, """ Make a cool game to illustrate the current relation between USA and europe diff --git a/sources/text_to_speech.py b/sources/text_to_speech.py index 13ba1a4..6bc4527 100644 --- a/sources/text_to_speech.py +++ b/sources/text_to_speech.py @@ -40,8 +40,7 @@ class Speech(): ) for i, (gs, ps, audio) in enumerate(generator): audio_file = 'sample.wav' - print(audio_file) - display(Audio(data=audio, rate=24000, autoplay=i==0)) + display(Audio(data=audio, rate=24000, autoplay=i==0), display_id=False) sf.write(audio_file, audio, 24000) # save each audio file if platform.system().lower() != "windows": subprocess.call(["afplay", audio_file]) diff --git a/sources/tools/PyInterpreter.py b/sources/tools/PyInterpreter.py index 8d47bda..2c250c9 100644 --- a/sources/tools/PyInterpreter.py +++ b/sources/tools/PyInterpreter.py @@ -1,5 +1,6 @@ import sys +import os import re from io import StringIO @@ -25,10 +26,15 @@ class PyInterpreter(Tools): return "Code rejected by user." stdout_buffer = StringIO() sys.stdout = stdout_buffer + global_vars = { + '__builtins__': __builtins__, + 'os': os, + 'sys': sys, + } code = '\n\n'.join(codes) try: try: - buffer = exec(code) + buffer = exec(code, global_vars) if buffer is not None: output = buffer + '\n' except Exception as e: diff --git a/sources/tools/fileFinder.py b/sources/tools/fileFinder.py index 715b343..273356c 100644 --- a/sources/tools/fileFinder.py +++ b/sources/tools/fileFinder.py @@ -62,7 +62,6 @@ class FileFinder(Tools): file_path = None excluded_files = [".pyc", ".o", ".so", ".a", ".lib", ".dll", ".dylib", ".so", ".git"] for root, dirs, files in os.walk(directory_path): - print(f"Root: {root}, Files: {files}") for file in files: if any(excluded_file in file for excluded_file in excluded_files): continue diff --git a/sources/tools/tools.py b/sources/tools/tools.py index c6856e0..92acaba 100644 --- a/sources/tools/tools.py +++ b/sources/tools/tools.py @@ -39,6 +39,7 @@ class Tools(): self.messages = [] self.config = configparser.ConfigParser() self.current_dir = self.create_work_dir() + self.excutable_blocks_found = False def check_config_dir_validity(self): """ @@ -129,6 +130,14 @@ class Tools(): print(f"Saving code block to: {save_path}") with open(os.path.join(directory, save_path_file), 'w') as f: f.write(block) + + def found_executable_blocks(self): + """ + Check if executable blocks were found. + """ + tmp = self.excutable_blocks_found + self.excutable_blocks_found = False + return tmp def load_exec_block(self, llm_text: str) -> tuple[list[str], str | None]: """ @@ -178,6 +187,7 @@ class Tools(): if ':' in content.split('\n')[0]: save_path = content.split('\n')[0].split(':')[1] content = content[content.find('\n')+1:] + self.excutable_blocks_found = True code_blocks.append(content) start_index = end_pos + len(end_tag) return code_blocks, save_path diff --git a/sources/tools/webSearch.py b/sources/tools/webSearch.py index 2411902..4544d1f 100644 --- a/sources/tools/webSearch.py +++ b/sources/tools/webSearch.py @@ -6,9 +6,13 @@ 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 class webSearch(Tools): def __init__(self, api_key: str = None): @@ -24,6 +28,7 @@ class webSearch(Tools): return "Error: No SerpApi key provided." for block in blocks: query = block.strip() + pretty_print(f"Searching for: {query}", color="status") if not query: return "Error: No search query provided." @@ -32,19 +37,21 @@ class webSearch(Tools): params = { "q": query, "api_key": self.api_key, - "num": 1, + "num": 100, "output": "json" } response = requests.get(url, params=params) response.raise_for_status() data = response.json() + results = [] if "organic_results" in data and len(data["organic_results"]) > 0: - first_result = data["organic_results"][0] - title = first_result.get("title", "No title") - snippet = first_result.get("snippet", "No snippet available") - link = first_result.get("link", "No link available") - return f"Title: {title}\nSnippet: {snippet}\nLink: {link}" + for result in data["organic_results"][:50]: + title = result.get("title", "No title") + snippet = result.get("snippet", "No snippet available") + link = result.get("link", "No link available") + results.append(f"Title: {title}\nSnippet: {snippet}\nLink: {link}") + return "\n\n".join(results) else: return "No results found for the query." except requests.RequestException as e: @@ -65,6 +72,6 @@ class webSearch(Tools): if __name__ == "__main__": search_tool = webSearch(api_key=os.getenv("SERPAPI_KEY")) query = "when did covid start" - result = search_tool.execute(query, safety=True) + result = search_tool.execute([query], safety=True) feedback = search_tool.interpreter_feedback(result) print(feedback) \ No newline at end of file diff --git a/sources/utility.py b/sources/utility.py index 7b7d696..a9a29c0 100644 --- a/sources/utility.py +++ b/sources/utility.py @@ -2,6 +2,9 @@ from colorama import Fore from termcolor import colored import platform +import threading +import itertools +import time def pretty_print(text, color = "info"): @@ -49,44 +52,41 @@ def pretty_print(text, color = "info"): color = "default" print(colored(text, color_map[color])) -def animate_thinking(text="thinking...", color="status", duration=2): +def animate_thinking(text, color="status", duration=2): """ - Display an animated "thinking..." indicator. - + Display an animated "thinking..." indicator in a separate thread. Args: - text (str): Text to display (default: "thinking...") - color (str): Color for the text (matches pretty_print colors) + text (str): Text to display + color (str): Color for the text duration (float): How long to animate in seconds """ - import time - import itertools + def _animate(): + color_map = { + "success": (Fore.GREEN, "green"), + "failure": (Fore.RED, "red"), + "status": (Fore.LIGHTGREEN_EX, "light_green"), + "code": (Fore.LIGHTBLUE_EX, "light_blue"), + "warning": (Fore.YELLOW, "yellow"), + "output": (Fore.LIGHTCYAN_EX, "cyan"), + "default": (Fore.RESET, "black"), + "info": (Fore.CYAN, "cyan") + } - color_map = { - "success": (Fore.GREEN, "green"), - "failure": (Fore.RED, "red"), - "status": (Fore.LIGHTGREEN_EX, "light_green"), - "code": (Fore.LIGHTBLUE_EX, "light_blue"), - "warning": (Fore.YELLOW, "yellow"), - "output": (Fore.LIGHTCYAN_EX, "cyan"), - "default": (Fore.RESET, "black"), - "info": (Fore.CYAN, "cyan") - } + fore_color, term_color = color_map[color] + spinner = itertools.cycle(['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']) + end_time = time.time() + duration - if color not in color_map: - color = "info" - - fore_color, term_color = color_map[color] - spinner = itertools.cycle(['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']) - end_time = time.time() + duration - - while time.time() < end_time: - symbol = next(spinner) - if platform.system().lower() != "windows": - print(f"\r{fore_color}{symbol} {text}{Fore.RESET}", end="", flush=True) - else: - print(colored(f"\r{symbol} {text}", term_color), end="", flush=True) - time.sleep(0.1) - print() + while time.time() < end_time: + symbol = next(spinner) + if platform.system().lower() != "windows": + print(f"\r{fore_color}{symbol} {text}{Fore.RESET}", end="", flush=True) + else: + print(colored(f"\r{symbol} {text}", term_color), end="", flush=True) + time.sleep(0.1) + print() + animation_thread = threading.Thread(target=_animate) + animation_thread.daemon = True + animation_thread.start() def timer_decorator(func): """