From 564a09c96d2c267c77165931dcb4d25c73449616 Mon Sep 17 00:00:00 2001 From: martin legrand Date: Wed, 23 Apr 2025 15:51:47 +0200 Subject: [PATCH] feat : planner adapt to task failure + temporary remove frontend task panel --- frontend/agentic-seek-front/src/App.css | 2 +- frontend/agentic-seek-front/src/App.js | 12 +- prompts/base/planner_agent.txt | 3 +- sources/agents/agent.py | 4 + sources/agents/planner_agent.py | 153 ++++++++++++++++++++---- sources/browser.py | 4 +- 6 files changed, 143 insertions(+), 35 deletions(-) diff --git a/frontend/agentic-seek-front/src/App.css b/frontend/agentic-seek-front/src/App.css index f91ba9f..1c051ab 100644 --- a/frontend/agentic-seek-front/src/App.css +++ b/frontend/agentic-seek-front/src/App.css @@ -73,7 +73,7 @@ body { .app-sections { display: grid; - grid-template-columns: 1fr 1fr 1fr; + grid-template-columns: 1fr 1fr; gap: 16px; height: calc(100vh - 80px); } diff --git a/frontend/agentic-seek-front/src/App.js b/frontend/agentic-seek-front/src/App.js index e8b8ad6..f7dd952 100644 --- a/frontend/agentic-seek-front/src/App.js +++ b/frontend/agentic-seek-front/src/App.js @@ -98,6 +98,7 @@ function App() { }, ]); setStatus(data.status); + setResponseData(data); scrollToBottom(); } else { console.log('Duplicate answer detected, skipping:', data.answer); @@ -147,12 +148,8 @@ function App() { const handleGetScreenshot = async () => { try { - console.log('Fetching screenshot...'); - const res = await axios.get('http://0.0.0.0:8000/screenshots/updated_screen.png'); - setResponseData((prev) => ({ ...prev, screenshot: res.data.screenshot })); setCurrentView('screenshot'); } catch (err) { - console.error('Error fetching screenshot:', err); setError('Browser not in use'); } }; @@ -164,12 +161,7 @@ function App() {
-
-

Task

-
-

No active task. Start a conversation to create a task.

-
-
+

Chat Interface

diff --git a/prompts/base/planner_agent.txt b/prompts/base/planner_agent.txt index 33366cf..92ecd4b 100644 --- a/prompts/base/planner_agent.txt +++ b/prompts/base/planner_agent.txt @@ -77,4 +77,5 @@ Rules: - Only use web agent for finding necessary informations. - If a task might require user email (eg: api services), do not write plan instead ask for user email. - Do not search for tutorial. -- Make sure json is within ```json tag \ No newline at end of file +- Make sure json is within ```json tag +- One step, one agent. \ No newline at end of file diff --git a/sources/agents/agent.py b/sources/agents/agent.py index 387189e..0573dc5 100644 --- a/sources/agents/agent.py +++ b/sources/agents/agent.py @@ -78,6 +78,10 @@ class Agent(): def get_tools(self) -> dict: return self.tools + @property + def get_success(self) -> bool: + return self.success + def get_blocks_result(self) -> list: return self.blocks_result diff --git a/sources/agents/planner_agent.py b/sources/agents/planner_agent.py index 75f845c..6166057 100644 --- a/sources/agents/planner_agent.py +++ b/sources/agents/planner_agent.py @@ -26,11 +26,18 @@ class PlannerAgent(Agent): } self.role = "planification" self.type = "planner_agent" - - def parse_agent_tasks(self, text): - tasks = [] + + def get_task_names(self, text: str) -> List[str]: + """ + Extracts task names from the given text. + This method processes a multi-line string, where each line may represent a task name. + containing '##' or starting with a digit. The valid task names are collected and returned. + Args: + text (str): A string containing potential task titles (eg: Task 1: I will...). + Returns: + List[str]: A list of extracted task names that meet the specified criteria. + """ tasks_names = [] - lines = text.strip().split('\n') for line in lines: if line is None: @@ -41,9 +48,23 @@ class PlannerAgent(Agent): if '##' in line or line[0].isdigit(): tasks_names.append(line) continue + return tasks_names + + def parse_agent_tasks(self, text: str) -> List[Tuple[str, str]]: + """ + Parses agent tasks from the given LLM text. + This method extracts task information from a JSON. It identifies task names and their details. + Args: + text (str): The input text containing task information in a JSON-like format. + Returns: + List[Tuple[str, str]]: A list of tuples containing task names and their details. + """ + tasks = [] + tasks_names = self.get_task_names(text) + blocks, _ = self.tools["json"].load_exec_block(text) if blocks == None: - return (None, None) + return [] for block in blocks: line_json = json.loads(block) if 'plan' in line_json: @@ -58,10 +79,18 @@ class PlannerAgent(Agent): tasks.append(agent) if len(tasks_names) != len(tasks): names = [task['task'] for task in tasks] - return zip(names, tasks) - return zip(tasks_names, tasks) + return list(map(list, zip(names, tasks))) + return list(map(list, zip(names, tasks))) - def make_prompt(self, task: dict, agent_infos_dict: dict): + def make_prompt(self, task: str, agent_infos_dict: dict) -> str: + """ + Generates a prompt for the agent based on the task and previous agents work information. + Args: + task (str): The task to be performed. + agent_infos_dict (dict): A dictionary containing information from other agents. + Returns: + str: The formatted prompt for the agent. + """ infos = "" if agent_infos_dict is None or len(agent_infos_dict) == 0: infos = "No needed informations." @@ -76,8 +105,14 @@ class PlannerAgent(Agent): """ return prompt - def show_plan(self, agents_tasks: dict, answer: str) -> None: - if agents_tasks == (None, None): + def show_plan(self, agents_tasks: List[dict], answer: str) -> None: + """ + Displays the plan made by the agent. + Args: + agents_tasks (dict): The tasks assigned to each agent. + answer (str): The answer from the LLM. + """ + if agents_tasks == []: pretty_print(answer, color="warning") pretty_print("Failed to make a plan. This can happen with (too) small LLM. Clarify your request and insist on it making a plan within ```json.", color="failure") return @@ -85,8 +120,15 @@ class PlannerAgent(Agent): for task_name, task in agents_tasks: pretty_print(f"{task['agent']} -> {task['task']}", color="info") pretty_print("▔▗ E N D ▖▔", color="status") - + async def make_plan(self, prompt: str) -> str: + """ + Asks the LLM to make a plan. + Args: + prompt (str): The prompt to be sent to the LLM. + Returns: + str: The plan made by the LLM. + """ ok = False answer = None while not ok: @@ -94,35 +136,98 @@ class PlannerAgent(Agent): self.memory.push('user', prompt) answer, _ = await self.llm_request() agents_tasks = self.parse_agent_tasks(answer) - if agents_tasks == (None, None): + if "NO_UPDATE" in agents_tasks: + return [] + if agents_tasks == []: prompt = f"Failed to parse the tasks. Please make a plan within ```json.\n" pretty_print("Failed to make plan. Retrying...", color="warning") continue self.show_plan(agents_tasks, answer) ok = True - return answer + return self.parse_agent_tasks(answer) + + async def update_plan(self, goal: str, agents_tasks: List[dict], agents_work_result: dict, id: str, success: bool) -> dict: + """ + Updates the plan with the results of the agents work. + Args: + goal (str): The goal to be achieved. + agents_tasks (list): The tasks assigned to each agent. + agents_work_result (dict): The results of the agents work. + Returns: + dict: The updated plan. + """ + #self.memory.clear() + + last_agent_work = agents_work_result[id] + tool_success_str = "success" if success else "failure" + pretty_print(f"Agent {id} work {tool_success_str}.", color="success" if success else "failure") + next_task = agents_tasks[int(id)][0] + if success: + return agents_tasks # we only update the plan if last task failed, for now + update_prompt = f""" + Your goal was : {goal} + You previously made a plan, agents are currently working on it. + The last agent working on task: {id}, did the following work: + {last_agent_work} + But the agent {id} failed with the task. + The agent {id} about to work on task: {next_task} + Is the work done for task {id} leading to sucess or failure ? Did an agent fail with a task? + If agent work lead to success: answer "NO_UPDATE" + If agent work lead might to failure: update the plan. + plan should be within ```json like before. + You need to rewrite the whole plan, but only change the tasks after {id}. + Keep the plan as short as the original one if possible. + """ + print("PROMPT") + print(update_prompt) + print("END PROMPT") + pretty_print("Updating plan...", color="status") + plan = await self.make_plan(update_prompt) + if plan == []: + pretty_print("No plan update required.", color="info") + return agents_tasks + return plan async def start_agent_process(self, task: dict, required_infos: dict | None) -> str: + """ + Starts the agent process for a given task. + Args: + task (dict): The task to be performed. + required_infos (dict | None): The required information for the task. + Returns: + str: The result of the agent process. + """ agent_prompt = self.make_prompt(task['task'], required_infos) pretty_print(f"Agent {task['agent']} started working...", color="status") agent_answer, _ = await self.agents[task['agent'].lower()].process(agent_prompt, None) + success = self.agents[task['agent'].lower()].get_success self.agents[task['agent'].lower()].show_answer() pretty_print(f"Agent {task['agent']} completed task.", color="status") - return agent_answer + return agent_answer, success def get_work_result_agent(self, task_needs, agents_work_result): return {k: agents_work_result[k] for k in task_needs if k in agents_work_result} - async def process(self, prompt: str, speech_module: Speech) -> Tuple[str, str]: - agents_tasks = (None, None) + async def process(self, goal: str, speech_module: Speech) -> Tuple[str, str]: + """ + Process the goal by dividing it into tasks and assigning them to agents. + Args: + goal (str): The goal to be achieved (user prompt). + speech_module (Speech): The speech module for text-to-speech. + Returns: + Tuple[str, str]: The result of the agent process and empty reasoning string. + """ + agents_tasks = [] agents_work_result = dict() - answer = await self.make_plan(prompt) - agents_tasks = self.parse_agent_tasks(answer) + agents_tasks = await self.make_plan(goal) - if agents_tasks == (None, None): + if agents_tasks == []: return "Failed to parse the tasks.", "" - for task_name, task in agents_tasks: + i = 0 + steps = len(agents_tasks) + while i < steps: + task_name, task = agents_tasks[i][0], agents_tasks[i][1] self.status_message = "Starting agent process..." pretty_print(f"I will {task_name}.", color="info") pretty_print(f"Assigned agent {task['agent']} to {task_name}", color="info") @@ -131,8 +236,14 @@ class PlannerAgent(Agent): if agents_work_result is not None: required_infos = self.get_work_result_agent(task['need'], agents_work_result) try: - self.last_answer = await self.start_agent_process(task, required_infos) + self.last_answer, success = await self.start_agent_process(task, required_infos) except Exception as e: raise e agents_work_result[task['id']] = self.last_answer + if i == steps - 1: + break + agents_tasks = await self.update_plan(goal, agents_tasks, agents_work_result, task['id'], success) + steps = len(agents_tasks) + i += 1 + return self.last_answer, "" \ No newline at end of file diff --git a/sources/browser.py b/sources/browser.py index 5cc8165..bef67f5 100644 --- a/sources/browser.py +++ b/sources/browser.py @@ -601,10 +601,10 @@ if __name__ == "__main__": input("press enter to continue") print("AntiCaptcha / Form Test") - browser.go_to("https://www.browserscan.net/bot-detection") + #browser.go_to("https://www.browserscan.net/bot-detection") #txt = browser.get_text() #browser.go_to("https://www.google.com/recaptcha/api2/demo") - #browser.go_to("https://home.openweathermap.org/users/sign_up") + browser.go_to("https://home.openweathermap.org/users/sign_up") inputs_visible = browser.get_form_inputs() print("inputs:", inputs_visible) #inputs_fill = ['[q](checked)', '[q](checked)', '[user[username]](mlg)', '[user[email]](mlg.fcu@gmail.com)', '[user[password]](placeholder_P@ssw0rd123)', '[user[password_confirmation]](placeholder_P@ssw0rd123)']