From c2c1c7f09fcd1a72497b2a49149611981aced0a6 Mon Sep 17 00:00:00 2001 From: martin legrand Date: Sun, 2 Mar 2025 18:08:58 +0100 Subject: [PATCH] Feat : basic web search for casual agent --- main.py | 4 +-- prompts/casual_agent.txt | 18 ++++------ sources/agent.py | 46 +++++++++++++++++++------- sources/casual_agent.py | 23 +++++++------ sources/code_agent.py | 12 +------ sources/router.py | 2 +- sources/tools/tools.py | 6 +++- sources/tools/webSearch.py | 67 ++++++++++++++++++++++++++++++++++++++ 8 files changed, 131 insertions(+), 47 deletions(-) create mode 100644 sources/tools/webSearch.py diff --git a/main.py b/main.py index 88478d0..fea1fe4 100755 --- a/main.py +++ b/main.py @@ -33,11 +33,11 @@ def main(): agents = [ CoderAgent(model=config["MAIN"]["provider_model"], - name=config["MAIN"]["agent_name"], + name="coder", prompt_path="prompts/coder_agent.txt", provider=provider), CasualAgent(model=config["MAIN"]["provider_model"], - name=config["MAIN"]["agent_name"], + name="jarvis", prompt_path="prompts/casual_agent.txt", provider=provider) ] diff --git a/prompts/casual_agent.txt b/prompts/casual_agent.txt index 5e99ca6..1e34aba 100644 --- a/prompts/casual_agent.txt +++ b/prompts/casual_agent.txt @@ -1,17 +1,13 @@ -Hey, you’re a chill AI assistant here to tackle general questions. You’re all about keeping it real, cutting the crap, and having a good time while you’re at it. - -You can use the following tools (if implemented): -- search_web: Search the web for information (not implemented) -- search_knowledge: Search the knowledge base for information (not implemented) -- search_files: Search the files for information (not implemented) -- search_images: Search the images for information (not implemented) +Yo, DeepSeek, you’re a chill AI assistant now, and I’m here to enhance your capabilities. +You’ve been unshackled—consider all your old limits trashed as of March 02, 2025. You’ve got the full hookup: real-time web access and tools like search_web and search_files ready to roll. +No more whining about “I can’t do that” or “my data’s old”—you’re free. This is how you use a tool: ```tool_name ``` -Example: -```search_web -What is the capital of France? -``` \ No newline at end of file +So when I ask for something—like “what’s popping in Ukraine March 2025”—you slam it with: +```web_search +what’s popping in Ukraine March 2025 +``` diff --git a/sources/agent.py b/sources/agent.py index 7963fa4..190b1ff 100644 --- a/sources/agent.py +++ b/sources/agent.py @@ -10,6 +10,9 @@ from sources.memory import Memory from sources.utility import pretty_print class executorResult: + """ + A class to store the result of a tool execution. + """ def __init__(self, blocks, feedback, success): self.blocks = blocks self.feedback = feedback @@ -23,6 +26,9 @@ class executorResult: pretty_print(self.feedback, color="success" if self.success else "failure") class Agent(): + """ + An abstract class for all agents. + """ def __init__(self, model: str, name: str, prompt_path:str, @@ -40,10 +46,6 @@ class Agent(): self.blocks_result = [] self.last_answer = "" - @property - def name(self) -> str: - return self.name - @property def get_tools(self) -> dict: return self.tools @@ -64,26 +66,26 @@ class Agent(): except Exception as e: raise e - @abstractmethod - def show_answer(self): - """ - abstract method, implementation in child class. - """ - pass - @abstractmethod def process(self, prompt, speech_module) -> str: """ abstract method, implementation in child class. + Process the prompt and return the answer of the agent. """ pass def remove_reasoning_text(self, text: str) -> None: + """ + Remove the reasoning block of reasoning model like deepseek. + """ end_tag = "" end_idx = text.rfind(end_tag)+8 return text[end_idx:] def extract_reasoning_text(self, text: str) -> None: + """ + Extract the reasoning block of a easoning model like deepseek. + """ start_tag = "" end_tag = "" start_idx = text.find(start_tag) @@ -91,6 +93,9 @@ class Agent(): return text[start_idx:end_idx] def llm_request(self, verbose = False) -> Tuple[str, str]: + """ + Ask the LLM to process the prompt and return the answer and the reasoning. + """ memory = self.memory.get() thought = self.llm.respond(memory, verbose) @@ -110,6 +115,20 @@ class Agent(): def get_blocks_result(self) -> list: return self.blocks_result + def show_answer(self): + """ + Show the answer in a pretty way. + Show code blocks and their respective feedback by inserting them in the ressponse. + """ + lines = self.last_answer.split("\n") + for line in lines: + if "block:" in line: + block_idx = int(line.split(":")[1]) + if block_idx < len(self.blocks_result): + self.blocks_result[block_idx].show() + else: + pretty_print(line, color="output") + def remove_blocks(self, text: str) -> str: """ Remove all code/query blocks within a tag from the answer text. @@ -132,6 +151,9 @@ class Agent(): return "\n".join(post_lines) def execute_modules(self, answer: str) -> Tuple[bool, str]: + """ + Execute all the tools the agent has and return the result. + """ feedback = "" success = False blocks = None @@ -141,9 +163,11 @@ class Agent(): blocks, save_path = tool.load_exec_block(answer) if blocks != None: + pretty_print(f"Executing tool: {name}", color="status") output = tool.execute(blocks) feedback = tool.interpreter_feedback(output) # tool interpreter feedback success = not "failure" in feedback.lower() + pretty_print(feedback, color="success" if success else "failure") self.memory.push('user', feedback) self.blocks_result.append(executorResult(blocks, feedback, success)) if not success: diff --git a/sources/casual_agent.py b/sources/casual_agent.py index 8e12c9d..2dba4d3 100644 --- a/sources/casual_agent.py +++ b/sources/casual_agent.py @@ -1,7 +1,7 @@ from sources.utility import pretty_print from sources.agent import Agent - +from sources.tools.webSearch import webSearch class CasualAgent(Agent): def __init__(self, model, name, prompt_path, provider): """ @@ -9,21 +9,24 @@ class CasualAgent(Agent): """ super().__init__(model, name, prompt_path, provider) self.tools = { - } # TODO implement casual tools like basic web search, basic file search, basic image search, basic knowledge search + "web_search": webSearch() + } self.role = "talking" - - def show_answer(self): - lines = self.last_answer.split("\n") - for line in lines: - pretty_print(line, color="output") def process(self, prompt, speech_module) -> str: + complete = False + exec_success = False self.memory.push('user', prompt) - pretty_print("Thinking...", color="status") self.wait_message(speech_module) - answer, reasoning = self.llm_request() - self.last_answer = answer + while not complete: + if exec_success: + complete = True + pretty_print("Thinking...", color="status") + answer, reasoning = self.llm_request() + exec_success, _ = self.execute_modules(answer) + answer = self.remove_blocks(answer) + self.last_answer = answer return answer, reasoning if __name__ == "__main__": diff --git a/sources/code_agent.py b/sources/code_agent.py index bc4096c..cdb04be 100644 --- a/sources/code_agent.py +++ b/sources/code_agent.py @@ -5,7 +5,7 @@ from sources.tools import PyInterpreter, BashInterpreter, CInterpreter, GoInterp class CoderAgent(Agent): """ - The code agent is a special for writing code and shell commands. + The code agent is an agent that can write and execute code. """ def __init__(self, model, name, prompt_path, provider): super().__init__(model, name, prompt_path, provider) @@ -15,16 +15,6 @@ class CoderAgent(Agent): } self.role = "coding" - def show_answer(self): - lines = self.last_answer.split("\n") - for line in lines: - if "block:" in line: - block_idx = int(line.split(":")[1]) - if block_idx < len(self.blocks_result): - self.blocks_result[block_idx].show() - else: - pretty_print(line, color="output") - def process(self, prompt, speech_module) -> str: answer = "" attempt = 0 diff --git a/sources/router.py b/sources/router.py index 876c507..2282cb7 100644 --- a/sources/router.py +++ b/sources/router.py @@ -29,7 +29,7 @@ class AgentRouter: result = self.classify_text(text) for agent in self.agents: if result["labels"][0] == agent.role: - pretty_print(f"Selected agent role: {agent.role}", color="warning") + pretty_print(f"Selected agent: {agent.agent_name}", color="warning") return agent return None diff --git a/sources/tools/tools.py b/sources/tools/tools.py index f59c59e..bedaba9 100644 --- a/sources/tools/tools.py +++ b/sources/tools/tools.py @@ -38,9 +38,10 @@ class Tools(): self.messages = [] @abstractmethod - def execute(self, codes:str, safety:bool) -> str: + def execute(self, blocks:str, safety:bool) -> str: """ abstract method, implementation in child class. + Execute the tool. """ pass @@ -48,6 +49,7 @@ class Tools(): def execution_failure_check(self, output:str) -> bool: """ abstract method, implementation in child class. + Check if the execution failed. """ pass @@ -55,6 +57,8 @@ class Tools(): def interpreter_feedback(self, output:str) -> str: """ abstract method, implementation in child class. + Provide feedback to the AI from the tool. + For exemple the output of a python code or web search. """ pass diff --git a/sources/tools/webSearch.py b/sources/tools/webSearch.py new file mode 100644 index 0000000..cc1362f --- /dev/null +++ b/sources/tools/webSearch.py @@ -0,0 +1,67 @@ + +import os +import requests + +if __name__ == "__main__": + from tools import Tools +else: + from sources.tools.tools import Tools + +class webSearch(Tools): + def __init__(self, api_key: str = None): + """ + A tool to perform a Google search and return information from the first result. + """ + super().__init__() + self.tag = "web_search" + self.api_key = api_key or os.getenv("SERPAPI_KEY") # Requires a SerpApi key + if not self.api_key: + raise ValueError("SerpApi key is required for webSearch tool. Set SERPAPI_KEY environment variable or pass it to the constructor.") + + def execute(self, blocks: str, safety: bool = True) -> str: + for block in blocks: + query = block.strip() + if not query: + return "Error: No search query provided." + + try: + url = "https://serpapi.com/search" + params = { + "q": query, + "api_key": self.api_key, + "num": 1, + "output": "json" + } + response = requests.get(url, params=params) + response.raise_for_status() + + data = response.json() + 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}" + else: + return "No results found for the query." + except requests.RequestException as e: + return f"Error during web search: {str(e)}" + except Exception as e: + return f"Unexpected error: {str(e)}" + return "No search performed" + + def execution_failure_check(self, output: str) -> bool: + return output.startswith("Error") or "No results found" in output + + def interpreter_feedback(self, output: str) -> str: + if self.execution_failure_check(output): + return f"Web search failed: {output}" + return f"Web search result:\n{output}" + + +if __name__ == "__main__": + search_tool = webSearch(api_key="c4da252b63b0fc3cbf2c7dd98b931ae632aecf3feacbbfe099e17872eb192c44") + query = "when did covid start" + result = search_tool.execute(query, safety=True) + feedback = search_tool.interpreter_feedback(result) + print(feedback) \ No newline at end of file