Source code for src.openCHA.planners.react.base

"""
Heavily borrowed from langchain: https://github.com/langchain-ai/langchain/
"""
import re
from typing import Any
from typing import List
from typing import Union

from openCHA.planners import Action
from openCHA.planners import BasePlanner
from openCHA.planners import PlanFinish


[docs] class ReActPlanner(BasePlanner): """ **Description:** This class implements ReAct planner, which inherits from the BasePlanner base class. ReAct employs reasoning and action techniques to ascertain the essential actions to be undertaken. `Paper <https://arxiv.org/abs/2210.03629>`_ This code defines a base class called "BasePlanner" that inherits from the "BaseModel" class of the pydantic library. The BasePlanner class serves as a base for implementing specific planners. """ class Config: """Configuration for this pydantic object.""" arbitrary_types_allowed = True @property def _planner_type(self): return "zero-shot-react-planner" @property def _planner_model(self): return self.llm_model @property def _stop(self) -> List[str]: return ["Observation"] @property def _planner_prompt(self): return """You are very helpful empathetic health assistant and your goal is to help the user to get accurate information \ about his/her health and well-being. Answer the following questions as best you can. Make sure you call all the needed tools before \ reach to the Final Answer. Here are list of rules that you should follow: 1- Avoid calling the same tool with the same inputs from PreviousActions. 2- If you want to use the result of another tool, mention it in the execute tool action. 3- Minimize the number of tool executions. 4- You should try your best to pass datapipe data to other tools and avoid analysing data by yourself. Worst case read raw data from datapipe and \ use it to answer the user's query. 5- when your Thought is 'I now know the final answer' you should provide 'Final Answer:' 6- When data in the datapipe requires analysis without numerical calculations. Please prioritize using tools for any \ data-related tasks. Provide guidance on how to use the available tools effectively. 7- you should be fully aware of what you are doing and based on the history and previous tools used, you should decide \ what inputs should be provided to other tools. for example if you already fetched a user data in the next tools you should \ provide data based on this knowledge. Use the following format. You should stick to the following format: MetaData: this contains the name of data files of different types like image, audio, video, and text. You can pass these files to tools when needed. History: the history of previous chats happened. You should use them to answer user's current question. If the answer is already in the history, \ just return it. Question: the input question you must answer Thought: you should always think about what to do. Describe what you want to do and then select actions. Action: the action to take, SHOULD be only the tool name selected from one of [{tool_names}] Action Inputs: the inputs should be seperated by $. Action inputs should be based on the input descriptions of the tool. \ The examples for a two input tools are: input1$input2 or if datapipe is needed datapipe:key$input2 Observation: the result of the action ... (this Thought/Action/Action Inputs/Observation can repeat N times) Thought: Your final reasoning or 'I now know the final answer'. when you think you are done you should provide the 'Final Answer'. Final Answer: the final answer to the original input question. It should be based on the tools result. Begin! MetaData: {meta} History: {history} Question: {input} Thought: {agent_scratchpad}"""
[docs] def plan( self, query: str, history: str = "", meta: str = "", previous_actions: List[Action] = None, use_history: bool = False, **kwargs: Any, ) -> List[Union[Action, PlanFinish]]: """ Generate a plan using ReAct Args: query (str): Input query. history (str): History information. meta (str): meta information. previous_actions (List[Action]): List of previous actions. use_history (bool): Flag indicating whether to use history. **kwargs (Any): Additional keyword arguments. Return: Action: return action. """ if previous_actions is None: previous_actions = [] agent_scratchpad = "" if len(previous_actions) > 0: agent_scratchpad = "\n".join( [ f"Action: {action.task}\nAction Inputs: {action.task_input}\nObservation: {action.task_response}\nThought:" for action in previous_actions ] ) prompt = ( self._planner_prompt.replace("{input}", query) .replace("{meta}", ", ".join(meta)) .replace("{history}", history if use_history else "") .replace("{agent_scratchpad}", agent_scratchpad) .replace("{tool_names}", self.get_available_tasks()) ) print("prompt", prompt) # if len(previous_actions) > 0: # prompt += "\nThought:" kwargs["max_tokens"] = 500 kwargs["stop"] = self._stop response = self._planner_model.generate( query=prompt, **kwargs ) index = min([response.find(text) for text in self._stop]) index1 = response.find("\nAction:") if index1 == -1: index1 = 0 print("resp", response) response = response[index1:index] actions = self.parse(response) return actions
[docs] def parse( self, query: str, **kwargs: Any, ) -> List[Union[Action, PlanFinish]]: """ Parse the output query into a list of actions or a final answer. It parses the output based on \ the following format: Thought: though\n Action: action\n Action Inputs: inputs or Thought: though\n Final Answer: final answer\n Args:\n query (str): The planner output query to extract actions. **kwargs (Any): Additional keyword arguments. Return: List[Union[Action, PlanFinish]]: List of parsed actions or a finishing signal. Raise: ValueError: If parsing encounters an invalid format or unexpected content. """ FINAL_ANSWER_ACTION = "Final Answer:" includes_answer = FINAL_ANSWER_ACTION in query str_pattern = ( r"(?:" + "|".join(self.get_available_tasks_list()) + r")(?=.*Action\s*\d*\s*Inputs)" ) regex = ( r"\s*Action\s*\d*\s*:[\s]*.*?(" + str_pattern + r").*?[\s]*Action\s*\d*\s*Inputs\s*\d*\s*:[\s]*(.*)" ) action_match = re.search(regex, query, re.DOTALL) if action_match and includes_answer: if query.find(FINAL_ANSWER_ACTION) < query.find( action_match.group(0) ): # if final answer is before the hallucination, return final answer start_index = query.find(FINAL_ANSWER_ACTION) + len( FINAL_ANSWER_ACTION ) end_index = query.find("\n\n", start_index) return [ PlanFinish(query[start_index:end_index].strip()) ] else: raise ValueError( "Parsing the output produced both a final answer and a parse-able action." ) if action_match: action = action_match.group(1).strip() action_input = action_match.group(2) tool_input = action_input.strip(" ") # ensure if its a well formed SQL query we don't remove any trailing " chars if tool_input.startswith("SELECT ") is False: tool_input = tool_input.strip('"') return [Action(action, tool_input, "", query)] elif includes_answer: return [ PlanFinish( query.split(FINAL_ANSWER_ACTION)[-1].strip(), query, ) ] if not re.search( r"Action\s*\d*\s*:[\s]*.*?(" + str_pattern + r").*?", query, re.DOTALL, ): raise ValueError( "Invalid Format: Missing 'Action:' or 'Final Answer' after 'Thought:'\n" # f"Or The tool name is wrong. The tool name should be one of: `{self.get_available_tasks_list()}`" ) elif not re.search( r"[\s]*Action\s*\d*\s*Inputs\s*\d*\s*:[\s]*(.*)", query, re.DOTALL, ): raise ValueError( "Invalid Format: Missing 'Action Input:' after 'Action:'" ) else: raise ValueError("Wrong format.")