        self.pages_viewed: List[Any] = []
        self.plan: str = plan

    @property
    def finish(self):
        return True if len(self.actions_taken) > 0 and "finish" in self.actions_taken[-1] else False

    @property
    def interrupt(self):
        return True if len(self.actions_taken) > 0 and "interrupt" in self.actions_taken[-1] else False

    @property
    def error(self):
        return True if len(self.actions_taken) > 0 and "error" in self.actions_taken[-1] else False

    @property
    def fail(self):
        return True if len(self.actions_taken) > 0 and "fail" in self.actions_taken[-1] else False

    @property
    def action_history(self):
        if self.mode == "basic":
            action_history = "Action: "
            for action in self.actions_taken:
                action_history += action + " -> "
            return action_history
        elif self.mode == "react":
            action_history = ""
            for thought, action in zip(self.thoughts_taken, self.actions_taken):
                action_history += thought + " -> " + action + " -> "
            return action_history
        else:
            raise ValueError(f"The mode {self.mode} is not supported")

    def run(
            self,
            page_info: Any,
            llm: BaseLanguageModel,
    ) -> Dict[str, Any]:
        model = HTMLDataModel.from_raw_data(raw_data=page_info)
        processed_html = model.get_llm_side_data()
        if self.mode == "basic":
            method = WebotChain.from_llm(llm)
            self.pages_viewed.append(processed_html)
            action_element = method(
                {"user_query": self.instruction, "previous_actions": self.actions_taken, "page_info": processed_html}
            )
        elif self.mode == "react":
            method = ReActWebotChain.from_llm(llm)
            self.pages_viewed.append(processed_html)
            print("self.plan:", self.plan)

            # example: {'success': True, 'message' = 'success', 'thought': "I should first set the value in the search field to '...'", 'action': 'setValue(93, "...")', 'parsedAction': {'name': 'setValue', 'args': {'elementId': 93, 'value': '...'}}}


run

    @property
    def fail(self):
        return True if len(self.actions_taken) > 0 and "fail" in self.actions_taken[-1] else False

    @property
    def action_history(self):
        if self.mode == "basic":
            action_history = "Action: "
            for action in self.actions_taken:
                action_history += action + " -> "
            return action_history
        elif self.mode == "react":
            action_history = ""
            for thought, action in zip(self.thoughts_taken, self.actions_taken):
                action_history += thought + " -> " + action + " -> "
            return action_history
        else:
            raise ValueError(f"The mode {self.mode} is not supported")

    def run(
            self,
            page_info: Any,
            llm: BaseLanguageModel,
    ) -> Dict[str, Any]:
        model = HTMLDataModel.from_raw_data(raw_data=page_info)
        processed_html = model.get_llm_side_data()
        if self.mode == "basic":
            method = WebotChain.from_llm(llm)
            self.pages_viewed.append(processed_html)
            action_element = method(
                {"user_query": self.instruction, "previous_actions": self.actions_taken, "page_info": processed_html}
            )
        elif self.mode == "react":
            method = ReActWebotChain.from_llm(llm)
            self.pages_viewed.append(processed_html)
            print("self.plan:", self.plan)

            # example: {'success': True, 'message' = 'success', 'thought': "I should first set the value in the search field to '...'", 'action': 'setValue(93, "...")', 'parsedAction': {'name': 'setValue', 'args': {'elementId': 93, 'value': '...'}}}
            webot_chain_return = method(
                {
                    "user_query": self.instruction,
                    "plan": self.plan,
                    "previous_actions": self.actions_taken,
                    "previous_thoughts": self.thoughts_taken,
                    "page_info": processed_html,
                }
            )
        else:
            raise ValueError(f"The mode {self.mode} is not supported")

        # "I should first set the value in the search field to '...'"
        self.thoughts_taken.append(webot_chain_return["thought"])

        # setValue(93, "...")
        self.actions_taken.append(webot_chain_return["action"])

        print("actions_taken:", self.actions_taken)
        return webot_chain_return


"""
Implementation of the WebotExecutor class.
WebotExecutor takes user's intent as input, return the start_url and instruction as the input for web browsing plugin
"""
from __future__ import annotations

from typing import Any, Dict, Union

from langchain.base_language import BaseLanguageModel
from pydantic import BaseModel, Extra

from real_agents.web_agent.web_browsing.base import WebotCallingChain

# This executor is for chat interface usage and not for the extension usage.
# For the extension usage, refer to xlang/real_agents/web_agent/executors/web_browsing_executor.py
class WebotExecutor(BaseModel):
    """
    WebotExecutor takes user's intent as input, return the start_url and instruction as the input for web browsing plugin (tool).
    """
    name: str
    description: str

    class Config:
        """Configuration for this pydantic object."""

        extra = Extra.forbid
        arbitrary_types_allowed = True

    @classmethod
    def from_webot(cls) -> WebotExecutor:
        return cls(
            name="WeBot",
            description="Use the web navigation agent to perform actions on the web, including information retrieval, task completion(e.g. write an email or tweet or organize a meeting), etc. The action input should contain the action and the start url.\For example:\nUse xxx.com to search xxx.",
        )

    def run(
            self,
            user_intent: str,
            llm: BaseLanguageModel,
    ) -> Union[str, Dict[str, Any]]:
        inputs = {"input_str": user_intent}
        method = WebotCallingChain.from_llm(
            llm,
        )
        output = method(inputs)
        return output


Config

"""
from __future__ import annotations

from typing import Any, Dict, Union

from langchain.base_language import BaseLanguageModel
from pydantic import BaseModel, Extra

from real_agents.web_agent.web_browsing.base import WebotCallingChain

# This executor is for chat interface usage and not for the extension usage.
# For the extension usage, refer to xlang/real_agents/web_agent/executors/web_browsing_executor.py
class WebotExecutor(BaseModel):
    """
    WebotExecutor takes user's intent as input, return the start_url and instruction as the input for web browsing plugin (tool).
    """
    name: str
    description: str

    class Config:
        """Configuration for this pydantic object."""

        extra = Extra.forbid
        arbitrary_types_allowed = True

    @classmethod
    def from_webot(cls) -> WebotExecutor:
        return cls(
            name="WeBot",
            description="Use the web navigation agent to perform actions on the web, including information retrieval, task completion(e.g. write an email or tweet or organize a meeting), etc. The action input should contain the action and the start url.\For example:\nUse xxx.com to search xxx.",
        )

    def run(
            self,
            user_intent: str,
            llm: BaseLanguageModel,
    ) -> Union[str, Dict[str, Any]]:
        inputs = {"input_str": user_intent}
        method = WebotCallingChain.from_llm(
            llm,
        )
        output = method(inputs)
        return output


from real_agents.web_agent.web_browsing.base import WebotCallingChain

# This executor is for chat interface usage and not for the extension usage.
# For the extension usage, refer to xlang/real_agents/web_agent/executors/web_browsing_executor.py
class WebotExecutor(BaseModel):
    """
    WebotExecutor takes user's intent as input, return the start_url and instruction as the input for web browsing plugin (tool).
    """
    name: str
    description: str

    class Config:
        """Configuration for this pydantic object."""

        extra = Extra.forbid
        arbitrary_types_allowed = True

    @classmethod
    def from_webot(cls) -> WebotExecutor:
        return cls(
            name="WeBot",
            description="Use the web navigation agent to perform actions on the web, including information retrieval, task completion(e.g. write an email or tweet or organize a meeting), etc. The action input should contain the action and the start url.\For example:\nUse xxx.com to search xxx.",
        )

    def run(
            self,
            user_intent: str,
            llm: BaseLanguageModel,
    ) -> Union[str, Dict[str, Any]]:
        inputs = {"input_str": user_intent}
        method = WebotCallingChain.from_llm(
            llm,
        )
        output = method(inputs)
        return output


run

class WebotExecutor(BaseModel):
    """
    WebotExecutor takes user's intent as input, return the start_url and instruction as the input for web browsing plugin (tool).
    """
    name: str
    description: str

    class Config:
        """Configuration for this pydantic object."""

        extra = Extra.forbid
        arbitrary_types_allowed = True

    @classmethod
    def from_webot(cls) -> WebotExecutor:
        return cls(
            name="WeBot",
            description="Use the web navigation agent to perform actions on the web, including information retrieval, task completion(e.g. write an email or tweet or organize a meeting), etc. The action input should contain the action and the start url.\For example:\nUse xxx.com to search xxx.",
        )

    def run(
            self,
            user_intent: str,
            llm: BaseLanguageModel,
    ) -> Union[str, Dict[str, Any]]:
        inputs = {"input_str": user_intent}
        method = WebotCallingChain.from_llm(
            llm,
        )
        output = method(inputs)
        return output


"""Implementation of the base webot calling chain."""
from __future__ import annotations

import re
import traceback
from typing import Dict, List, Optional

import json5
from langchain.base_language import BaseLanguageModel
from langchain.callbacks.manager import CallbackManagerForChainRun
from langchain.chains import LLMChain
from langchain.chains.base import Chain
from langchain.prompts.base import BasePromptTemplate
from langchain.prompts.chat import (
    ChatPromptTemplate,
    HumanMessagePromptTemplate,
    SystemMessagePromptTemplate,
)
from pydantic import BaseModel, Extra

from real_agents.adapters.memory import ReadOnlySharedStringMemory
from real_agents.web_agent.web_browsing.prompt import (
    SYSTEM_PROMPT,
    USER_PROMPT,
)

class WebotCallingChain(Chain, BaseModel):
    """
    Basic prompt based webot call chain.

    The chain is initialized from a webot
    """

    llm_basic_chain: LLMChain

    memory: Optional[ReadOnlySharedStringMemory] = None  # fixme:
    verbose = True

    chat_id: Optional[str] = None
    user_id: Optional[str] = None

    class Config:
        """Configuration for this pydantic object."""

        extra = Extra.forbid
        arbitrary_types_allowed = True

    @property
    def input_keys(self) -> List[str]:
        """Return the singular input key.

        :meta private:
        """
        return ["input_str"]

    @property
    def output_keys(self) -> List[str]:
        """Return the output keys.

        :meta private:
        """
        return ["instruction", "start_url"]

    def parse_response(self, response: str):
        """Parse the endpoint and input_json"""
        instruction = None
        start_url = None
        success = False

        try:
            json_content = json5.loads(response)
            instruction = json_content["instruction"]
            start_url = json_content["start_url"]
            success = True
        except Exception:
            pattern = r"\```json\n(.+?)\n```" if "```json" in response else r"\```\n(.+?)\n```"
            match = re.search(pattern, response, re.DOTALL)

            if match:
                json_content = json5.loads(match.group(1))
                instruction = json_content["instruction"]
                start_url = json_content["start_url"]
                success = True
        if success:
            return {"instruction": instruction, "start_url": start_url }
        else:
            raise Exception("Parsing error")

    def _call(
        self,
        inputs: Dict[str, str],
        run_manager: Optional[CallbackManagerForChainRun] = None,
    ) -> Dict[str, str]:
        _run_manager = run_manager or CallbackManagerForChainRun.get_noop_manager()

        input_str = inputs["input_str"]
        try:
            response_content = self.llm_basic_chain.run(**{"input_str": input_str})
            parsed_return = self.parse_response(response_content)
        except Exception as e:
            _run_manager.on_text(str(e) + "\n", color="red", verbose=self.verbose)
            _run_manager.on_text(traceback.format_exc(), color="red", verbose=self.verbose)

        return parsed_return

    @classmethod
    def create_basic_prompt(cls, system_prompt, user_prompt) -> BasePromptTemplate:
        # Call the LLM to get the predicted instruction and start_url
        input_variables = ["input_str"]
        messages = [
            SystemMessagePromptTemplate.from_template(system_prompt),
            HumanMessagePromptTemplate.from_template(user_prompt),
        ]

        return ChatPromptTemplate(input_variables=input_variables, messages=messages)

    @classmethod
    def from_llm(
        cls,
        llm: BaseLanguageModel,
    ) -> WebotCallingChain:
        llm_basic_chain = LLMChain(
            llm=llm,
            prompt=cls.create_basic_prompt(
                system_prompt=SYSTEM_PROMPT,
                user_prompt=USER_PROMPT,
            ),
        )

        return cls(
            llm_basic_chain=llm_basic_chain,
        )


WebotCallingChain

import json5
from langchain.base_language import BaseLanguageModel
from langchain.callbacks.manager import CallbackManagerForChainRun
from langchain.chains import LLMChain
from langchain.chains.base import Chain
from langchain.prompts.base import BasePromptTemplate
from langchain.prompts.chat import (
    ChatPromptTemplate,
    HumanMessagePromptTemplate,
    SystemMessagePromptTemplate,
)
from pydantic import BaseModel, Extra

from real_agents.adapters.memory import ReadOnlySharedStringMemory
from real_agents.web_agent.web_browsing.prompt import (
    SYSTEM_PROMPT,
    USER_PROMPT,
)

class WebotCallingChain(Chain, BaseModel):
    """
    Basic prompt based webot call chain.

    The chain is initialized from a webot
    """

    llm_basic_chain: LLMChain

    memory: Optional[ReadOnlySharedStringMemory] = None  # fixme:
    verbose = True

    chat_id: Optional[str] = None
    user_id: Optional[str] = None

    class Config:
        """Configuration for this pydantic object."""

        extra = Extra.forbid
        arbitrary_types_allowed = True

    @property
    def input_keys(self) -> List[str]:
        """Return the singular input key.

        :meta private:
        """
        return ["input_str"]

    @property
    def output_keys(self) -> List[str]:
        """Return the output keys.

        :meta private:
        """
        return ["instruction", "start_url"]

    def parse_response(self, response: str):
        """Parse the endpoint and input_json"""
        instruction = None
        start_url = None
        success = False

        try:
            json_content = json5.loads(response)
            instruction = json_content["instruction"]
            start_url = json_content["start_url"]
            success = True
        except Exception:
            pattern = r"\```json\n(.+?)\n```" if "```json" in response else r"\```\n(.+?)\n```"
            match = re.search(pattern, response, re.DOTALL)

            if match:
                json_content = json5.loads(match.group(1))
                instruction = json_content["instruction"]
                start_url = json_content["start_url"]
                success = True
        if success:
            return {"instruction": instruction, "start_url": start_url }
        else:
            raise Exception("Parsing error")

    def _call(
        self,
        inputs: Dict[str, str],
        run_manager: Optional[CallbackManagerForChainRun] = None,
    ) -> Dict[str, str]:
        _run_manager = run_manager or CallbackManagerForChainRun.get_noop_manager()

        input_str = inputs["input_str"]
        try:
            response_content = self.llm_basic_chain.run(**{"input_str": input_str})
            parsed_return = self.parse_response(response_content)
        except Exception as e:
            _run_manager.on_text(str(e) + "\n", color="red", verbose=self.verbose)
            _run_manager.on_text(traceback.format_exc(), color="red", verbose=self.verbose)

        return parsed_return

    @classmethod
    def create_basic_prompt(cls, system_prompt, user_prompt) -> BasePromptTemplate:
        # Call the LLM to get the predicted instruction and start_url
        input_variables = ["input_str"]
        messages = [
            SystemMessagePromptTemplate.from_template(system_prompt),
            HumanMessagePromptTemplate.from_template(user_prompt),
        ]

        return ChatPromptTemplate(input_variables=input_variables, messages=messages)

    @classmethod
    def from_llm(
        cls,
        llm: BaseLanguageModel,
    ) -> WebotCallingChain:
        llm_basic_chain = LLMChain(
            llm=llm,
            prompt=cls.create_basic_prompt(
                system_prompt=SYSTEM_PROMPT,
                user_prompt=USER_PROMPT,
            ),
        )

        return cls(
            llm_basic_chain=llm_basic_chain,
        )


Config

    SYSTEM_PROMPT,
    USER_PROMPT,
)

class WebotCallingChain(Chain, BaseModel):
    """
    Basic prompt based webot call chain.

    The chain is initialized from a webot
    """

    llm_basic_chain: LLMChain

    memory: Optional[ReadOnlySharedStringMemory] = None  # fixme:
    verbose = True

    chat_id: Optional[str] = None
    user_id: Optional[str] = None

    class Config:
        """Configuration for this pydantic object."""

        extra = Extra.forbid
        arbitrary_types_allowed = True

    @property
    def input_keys(self) -> List[str]:
        """Return the singular input key.

        :meta private:
        """
        return ["input_str"]

    @property
    def output_keys(self) -> List[str]:
        """Return the output keys.

        :meta private:
        """
        return ["instruction", "start_url"]

    def parse_response(self, response: str):
        """Parse the endpoint and input_json"""
        instruction = None


    Basic prompt based webot call chain.

    The chain is initialized from a webot
    """

    llm_basic_chain: LLMChain

    memory: Optional[ReadOnlySharedStringMemory] = None  # fixme:
    verbose = True

    chat_id: Optional[str] = None
    user_id: Optional[str] = None

    class Config:
        """Configuration for this pydantic object."""

        extra = Extra.forbid
        arbitrary_types_allowed = True

    @property
    def input_keys(self) -> List[str]:
        """Return the singular input key.

        :meta private:
        """
        return ["input_str"]

    @property
    def output_keys(self) -> List[str]:
        """Return the output keys.

        :meta private:
        """
        return ["instruction", "start_url"]

    def parse_response(self, response: str):
        """Parse the endpoint and input_json"""
        instruction = None
        start_url = None
        success = False

        try:
            json_content = json5.loads(response)
            instruction = json_content["instruction"]
            start_url = json_content["start_url"]
            success = True


    verbose = True

    chat_id: Optional[str] = None
    user_id: Optional[str] = None

    class Config:
        """Configuration for this pydantic object."""

        extra = Extra.forbid
        arbitrary_types_allowed = True

    @property
    def input_keys(self) -> List[str]:
        """Return the singular input key.

        :meta private:
        """
        return ["input_str"]

    @property
    def output_keys(self) -> List[str]:
        """Return the output keys.

        :meta private:
        """
        return ["instruction", "start_url"]

    def parse_response(self, response: str):
        """Parse the endpoint and input_json"""
        instruction = None
        start_url = None
        success = False

        try:
            json_content = json5.loads(response)
            instruction = json_content["instruction"]
            start_url = json_content["start_url"]
            success = True
        except Exception:
            pattern = r"\```json\n(.+?)\n```" if "```json" in response else r"\```\n(.+?)\n```"
            match = re.search(pattern, response, re.DOTALL)

            if match:
                json_content = json5.loads(match.group(1))
                instruction = json_content["instruction"]
                start_url = json_content["start_url"]


        extra = Extra.forbid
        arbitrary_types_allowed = True

    @property
    def input_keys(self) -> List[str]:
        """Return the singular input key.

        :meta private:
        """
        return ["input_str"]

    @property
    def output_keys(self) -> List[str]:
        """Return the output keys.

        :meta private:
        """
        return ["instruction", "start_url"]

    def parse_response(self, response: str):
        """Parse the endpoint and input_json"""
        instruction = None
        start_url = None
        success = False

        try:
            json_content = json5.loads(response)
            instruction = json_content["instruction"]
            start_url = json_content["start_url"]
            success = True
        except Exception:
            pattern = r"\```json\n(.+?)\n```" if "```json" in response else r"\```\n(.+?)\n```"
            match = re.search(pattern, response, re.DOTALL)

            if match:
                json_content = json5.loads(match.group(1))
                instruction = json_content["instruction"]
                start_url = json_content["start_url"]
                success = True
        if success:
            return {"instruction": instruction, "start_url": start_url }
        else:
            raise Exception("Parsing error")

    def _call(
        self,
        inputs: Dict[str, str],
        run_manager: Optional[CallbackManagerForChainRun] = None,
    ) -> Dict[str, str]:
        _run_manager = run_manager or CallbackManagerForChainRun.get_noop_manager()

        input_str = inputs["input_str"]
        try:
            response_content = self.llm_basic_chain.run(**{"input_str": input_str})
            parsed_return = self.parse_response(response_content)
        except Exception as e:
            _run_manager.on_text(str(e) + "\n", color="red", verbose=self.verbose)
            _run_manager.on_text(traceback.format_exc(), color="red", verbose=self.verbose)

        return parsed_return

    @classmethod
    def create_basic_prompt(cls, system_prompt, user_prompt) -> BasePromptTemplate:


        try:
            json_content = json5.loads(response)
            instruction = json_content["instruction"]
            start_url = json_content["start_url"]
            success = True
        except Exception:
            pattern = r"\```json\n(.+?)\n```" if "```json" in response else r"\```\n(.+?)\n```"
            match = re.search(pattern, response, re.DOTALL)

            if match:
                json_content = json5.loads(match.group(1))
                instruction = json_content["instruction"]
                start_url = json_content["start_url"]
                success = True
        if success:
            return {"instruction": instruction, "start_url": start_url }
        else:
            raise Exception("Parsing error")

    def _call(
        self,
        inputs: Dict[str, str],
        run_manager: Optional[CallbackManagerForChainRun] = None,
    ) -> Dict[str, str]:
        _run_manager = run_manager or CallbackManagerForChainRun.get_noop_manager()

        input_str = inputs["input_str"]
        try:
            response_content = self.llm_basic_chain.run(**{"input_str": input_str})
            parsed_return = self.parse_response(response_content)
        except Exception as e:
            _run_manager.on_text(str(e) + "\n", color="red", verbose=self.verbose)
            _run_manager.on_text(traceback.format_exc(), color="red", verbose=self.verbose)

        return parsed_return

    @classmethod
    def create_basic_prompt(cls, system_prompt, user_prompt) -> BasePromptTemplate:
        # Call the LLM to get the predicted instruction and start_url
        input_variables = ["input_str"]
        messages = [
            SystemMessagePromptTemplate.from_template(system_prompt),
            HumanMessagePromptTemplate.from_template(user_prompt),
        ]

        return ChatPromptTemplate(input_variables=input_variables, messages=messages)

    @classmethod
    def from_llm(
        cls,
        llm: BaseLanguageModel,
    ) -> WebotCallingChain:
        llm_basic_chain = LLMChain(
            llm=llm,
            prompt=cls.create_basic_prompt(


            raise Exception("Parsing error")

    def _call(
        self,
        inputs: Dict[str, str],
        run_manager: Optional[CallbackManagerForChainRun] = None,
    ) -> Dict[str, str]:
        _run_manager = run_manager or CallbackManagerForChainRun.get_noop_manager()

        input_str = inputs["input_str"]
        try:
            response_content = self.llm_basic_chain.run(**{"input_str": input_str})
            parsed_return = self.parse_response(response_content)
        except Exception as e:
            _run_manager.on_text(str(e) + "\n", color="red", verbose=self.verbose)
            _run_manager.on_text(traceback.format_exc(), color="red", verbose=self.verbose)

        return parsed_return

    @classmethod
    def create_basic_prompt(cls, system_prompt, user_prompt) -> BasePromptTemplate:
        # Call the LLM to get the predicted instruction and start_url
        input_variables = ["input_str"]
        messages = [
            SystemMessagePromptTemplate.from_template(system_prompt),
            HumanMessagePromptTemplate.from_template(user_prompt),
        ]

        return ChatPromptTemplate(input_variables=input_variables, messages=messages)

    @classmethod
    def from_llm(
        cls,
        llm: BaseLanguageModel,
    ) -> WebotCallingChain:
        llm_basic_chain = LLMChain(
            llm=llm,
            prompt=cls.create_basic_prompt(
                system_prompt=SYSTEM_PROMPT,
                user_prompt=USER_PROMPT,
            ),
        )

        return cls(
            llm_basic_chain=llm_basic_chain,
        )


            response_content = self.llm_basic_chain.run(**{"input_str": input_str})
            parsed_return = self.parse_response(response_content)
        except Exception as e:
            _run_manager.on_text(str(e) + "\n", color="red", verbose=self.verbose)
            _run_manager.on_text(traceback.format_exc(), color="red", verbose=self.verbose)

        return parsed_return

    @classmethod
    def create_basic_prompt(cls, system_prompt, user_prompt) -> BasePromptTemplate:
        # Call the LLM to get the predicted instruction and start_url
        input_variables = ["input_str"]
        messages = [
            SystemMessagePromptTemplate.from_template(system_prompt),
            HumanMessagePromptTemplate.from_template(user_prompt),
        ]

        return ChatPromptTemplate(input_variables=input_variables, messages=messages)

    @classmethod
    def from_llm(
        cls,
        llm: BaseLanguageModel,
    ) -> WebotCallingChain:
        llm_basic_chain = LLMChain(
            llm=llm,
            prompt=cls.create_basic_prompt(
                system_prompt=SYSTEM_PROMPT,
                user_prompt=USER_PROMPT,
            ),
        )

        return cls(
            llm_basic_chain=llm_basic_chain,
        )


SYSTEM_PROMPT = (
    """You are an expert at browsing website and you know a lot of website in case the user didn't explicitly mention the website they would like to search.""".strip()
    + "\n"
)

USER_PROMPT = (
    """
Here is the user's intent:
```
{input_str}
```
Now imagine you can use a tool called web agent to navigate on the web for you.
you should act as a user to tell the web agent instruction and the url to start
then it will take in the instruction and start at the url to navigate on the web.
Remember:
1. If you know the detailed url to take the action, you may set this as the start url. e.g. https://www.twitter.com/compose/tweet for writing a tweet on twitter
2. If you don't know the detailed url, you may set the start url as the homepage of the website. e.g. https://www.imdb.com/ for movie related question
3. If you are not sure whether the homepage will contain info that you need. Use https://www.google.com/ as the start url instead.
Here is an example for your reference:
the user intent is to write a blog post on medium
you should out put like this:
```
{{
    "instruction": "write a blog post",
    "start_url": "https://medium.com/"
}}
```
You should return me the user's instruction and start_url, formatted as:
```
{{
    "instruction": "xxx",
    "start_url": "xxx"
}}
```
""".strip()
    + "\n"
)


# The schema (action space) for the web browsing task is defined here:
ACTIONS = [
    {
        "name": "click",
        "description": "Clicks on an element",
        "args": [{"name": "elementId", "type": "number"}],
    },
    {
        "name": "setValue",
        "description": "Focuses on and sets the `value` of an input element.",
        "args": [{"name": "elementId", "type": "number"}, {"name": "value", "type": "string"}],
    },
    {"name": "finish", "description": "Indicates the task is finished", "args": []},
    {"name": "fail", "description": "Indicates that you are unable to complete the task", "args": []},
]


"""Implementation for prompt based end2end web bots."""
from __future__ import annotations

import datetime
import re
from typing import Any, Dict, List, Optional
from loguru import logger
from pydantic import BaseModel, Extra

from langchain.base_language import BaseLanguageModel
from langchain.callbacks.manager import CallbackManagerForChainRun
from langchain.chains.base import Chain
from langchain.chains.llm import LLMChain
from langchain.prompts.base import BasePromptTemplate
from langchain.prompts.chat import (
    ChatPromptTemplate,
    HumanMessagePromptTemplate,
    SystemMessagePromptTemplate,
)

from real_agents.adapters.memory import ReadOnlySharedStringMemory
from real_agents.web_agent.web_browsing.end2end.prompt import (
    RETRY_PROMPT,
    SYSTEM_PROMPT,
    USER_PROMPT,
)
from real_agents.web_agent.web_browsing.schema import ACTIONS
from real_agents.adapters.data_model.html import HTMLDataModel

class WebotChain(Chain, BaseModel):
    """Basic prompt based web bot that interact with websites. This implementation is highly motivated by Taxy.ai"""

    llm_basic_chain: LLMChain
    llm_retry_chain: LLMChain

    memory: Optional[ReadOnlySharedStringMemory] = None
    output_key: str = "action"

    chat_id: Optional[str] = None
    user_id: Optional[str] = None

    class Config:
        """Configuration for this pydantic object."""

        extra = Extra.forbid
        arbitrary_types_allowed = True

    @property
    def input_keys(self) -> List[str]:
        """Return the singular input key.

        :meta private:
        """
        return ["user_query", "previous_actions", "page_info"]

    @property
    def output_keys(self) -> List[str]:
        """Return the singular output key.

        :meta private:
        """
        return [self.output_key]

    @property
    def formatted_actions(self) -> str:
        formatted_actions = ""
        for i, action in enumerate(ACTIONS):
            args_str = ""
            for arg in action["args"]:
                if args_str != "":
                    args_str += ", "
                args_str += f'{arg["name"]}: {arg["type"]}'
            formatted_action = f"{i + 1}. {action['name']}({args_str}): {action['description']}"
            if formatted_actions != "":
                formatted_actions += "\n"
            formatted_actions += formatted_action
        return formatted_actions

    def parse_response(self, text):
        class Argument:
            def __init__(self, name, arg_type):
                self.name = name
                self.type = arg_type

        class Action:
            def __init__(self, name, description, args):
                self.name = name
                self.description = description
                self.args = [Argument(arg["name"], arg["type"]) for arg in args]

        available_actions = [
            Action("click", "Clicks on an element", [{"name": "elementId", "type": "number"}]),
            Action(
                "setValue",
                "Focuses on and sets the value of an input element",
                [{"name": "elementId", "type": "number"}, {"name": "value", "type": "string"}],
            ),
            Action("finish", "Indicates the task is finished", []),
            Action("fail", "Indicates that you are unable to complete the task", []),
        ]
        thought_match = re.search("<Thought>(.*?)</Thought>", text)
        action_match = re.search("<Action>(.*?)</Action>", text)

        if thought_match is None:
            return {"error": "Invalid response: Thought not found in the model response."}

        if action_match is None:
            return {"error": "Invalid response: Action not found in the model response."}

        thought = thought_match.group(1)
        action_string = action_match.group(1)
        action_pattern = re.compile("(\w+)\((.*?)\)")
        action_parts = action_pattern.match(action_string)

        if action_parts is None:
            return {"error": "Invalid action format: Action should be in the format functionName(arg1, arg2, ...)."}

        action_name = action_parts.group(1)
        action_args_string = action_parts.group(2)

        available_action = next((action for action in available_actions if action.name == action_name), None)

        if available_action is None:
            return {"error": f'Invalid action: "{action_name}" is not a valid action.'}

        args_array = [arg.strip() for arg in action_args_string.split(",") if arg.strip() != ""]
        parsed_args = {}

        if len(args_array) != len(available_action.args):
            return {
                "error": f'Invalid number of arguments: Expected {len(available_action.args)} for action "{action_name}", but got {len(args_array)}.'
            }

        for i in range(len(args_array)):
            arg = args_array[i]
            expected_arg = available_action.args[i]

            if expected_arg.type == "number":
                try:
                    number_value = int(arg)
                    parsed_args[expected_arg.name] = number_value
                except ValueError:
                    return {
                        "error": f'Invalid argument type: Expected a number for argument "{expected_arg.name}", but got "{arg}".'
                    }
            elif expected_arg.type == "string":
                if (arg.startswith('"') and arg.endswith('"')) or (arg.startswith("'") and arg.endswith("'")):
                    parsed_args[expected_arg.name] = arg[1:-1]
                else:
                    return {
                        "error": f'Invalid argument type: Expected a string for argument "{expected_arg.name}", but got "{arg}".'
                    }
            else:
                return {
                    "error": f'Invalid argument type: Unknown type "{expected_arg.type}" for argument "{expected_arg.name}".'
                }

        parsed_action = {"name": available_action.name, "args": parsed_args}

        return {"thought": thought, "action": action_string, "parsedAction": parsed_action}

    def _call(
        self,
        inputs: Dict[str, str],
        run_manager: Optional[CallbackManagerForChainRun] = None,
    ) -> Dict[str, str]:
        logger.bind(msg_head="WebotChain inputs").trace(inputs)

        _run_manager = run_manager or CallbackManagerForChainRun.get_noop_manager()

        # Get  ["user_query", "previous_actions", "page_info"], and other raw data
        plan = inputs["plan"]
        previous_actions = inputs["previous_actions"]
        user_query = inputs["user_query"]
        page_info = inputs["page_info"]

        model = HTMLDataModel.from_raw_data(page_info)
        processed_html = model.get_llm_side_data()

        current_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")

        # Generate the prompt
        previous_actions_string = "\n".join(["<Action>{}</Action>".format(action) for action in previous_actions])

        user_prompt = f"The user requests the following task:\n {user_query}\n{previous_actions_string}\nCurrent time: {current_time}\nCurrent page contents:\n{processed_html}"

        print("user_prompt",user_prompt)

        action = self.llm_basic_chain.run(
            **{
                "formattedActions": self.formatted_actions,
                "plan": plan,
                "user_query": user_query,
                "previous_actions_string": previous_actions_string,
                "current_time": current_time,
                "processed_html": processed_html,
            }
        )

        # Extract from "<Action>{}</Action>"
        parsed_return = self.parse_response(action)
        print("parsed_return",parsed_return)
        retry_count = 0
        while "error" in parsed_return and retry_count < 5:
            current_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
            action = self.retry(user_query, previous_actions_string, current_time, page_info, action)
            retry_count += 1
            previous_actions_string += "\n" + action
            parsed_return = self.parse_response(action)

        parsed_action = parsed_return["parsedAction"]

        logger.bind(msg_head="WebotChain generated action").trace(parsed_action)

        return parsed_return

    def retry(self, plan, user_query, previous_actions_string, current_time, page_info: str, last_action: str) -> str:
        action = self.llm_retry_chain.run(
            **{
                "formattedActions": self.formatted_actions,
                "plan": plan,
                "user_query": user_query,
                "previous_actions_string": previous_actions_string,
                "current_time": current_time,
                "processed_html": page_info,
                "last_action": last_action,
            }
        )
        return action

    @classmethod
    def create_basic_prompt(cls, system_prompt, user_prompt) -> BasePromptTemplate:
        # Call the LLM to get the predicted end_point and input_json
        input_variables = [
            "formattedActions",
            "plan",
            "user_query",
            "previous_actions_string",
            "current_time",
            "processed_html",
        ]
        messages = [
            SystemMessagePromptTemplate.from_template(system_prompt),
            HumanMessagePromptTemplate.from_template(user_prompt),
        ]

        return ChatPromptTemplate(input_variables=input_variables, messages=messages)

    @classmethod
    def create_retry_prompt(cls, system_prompt, retry_prompt) -> BasePromptTemplate:
        # Call the LLM to get the predicted end_point and input_json in retry
        input_variables = [
            "formattedActions",
            "plan",
            "user_query",
            "previous_actions_string",
            "current_time",
            "processed_html",
            "retry_message",
        ]
        messages = [
            SystemMessagePromptTemplate.from_template(system_prompt),
            HumanMessagePromptTemplate.from_template(retry_prompt),
        ]

        return ChatPromptTemplate(input_variables=input_variables, messages=messages)

    @classmethod
    def from_llm(cls, llm: BaseLanguageModel, **kwargs: Any) -> WebotChain:
        """Load from the initial web page and user instruction"""
        llm_basic_chain = LLMChain(
            llm=llm,
            prompt=cls.create_retry_prompt(
                system_prompt=SYSTEM_PROMPT,
                retry_prompt=USER_PROMPT,
            ),
        )

        llm_retry_chain = LLMChain(
            llm=llm,
            prompt=cls.create_retry_prompt(
                system_prompt=SYSTEM_PROMPT,
                retry_prompt=RETRY_PROMPT,
            ),
        )

        return cls(
            llm_basic_chain=llm_basic_chain,
            llm_retry_chain=llm_retry_chain,
            **kwargs,
        )


WebotChain

from langchain.callbacks.manager import CallbackManagerForChainRun
from langchain.chains.base import Chain
from langchain.chains.llm import LLMChain
from langchain.prompts.base import BasePromptTemplate
from langchain.prompts.chat import (
    ChatPromptTemplate,
    HumanMessagePromptTemplate,
    SystemMessagePromptTemplate,
)

from real_agents.adapters.memory import ReadOnlySharedStringMemory
from real_agents.web_agent.web_browsing.end2end.prompt import (
    RETRY_PROMPT,
    SYSTEM_PROMPT,
    USER_PROMPT,
)
from real_agents.web_agent.web_browsing.schema import ACTIONS
from real_agents.adapters.data_model.html import HTMLDataModel

class WebotChain(Chain, BaseModel):
    """Basic prompt based web bot that interact with websites. This implementation is highly motivated by Taxy.ai"""

    llm_basic_chain: LLMChain
    llm_retry_chain: LLMChain

    memory: Optional[ReadOnlySharedStringMemory] = None
    output_key: str = "action"

    chat_id: Optional[str] = None
    user_id: Optional[str] = None

    class Config:
        """Configuration for this pydantic object."""

        extra = Extra.forbid
        arbitrary_types_allowed = True

    @property
    def input_keys(self) -> List[str]:
        """Return the singular input key.

        :meta private:
        """
        return ["user_query", "previous_actions", "page_info"]

    @property
    def output_keys(self) -> List[str]:
        """Return the singular output key.

        :meta private:
        """
        return [self.output_key]

    @property
    def formatted_actions(self) -> str:
        formatted_actions = ""
        for i, action in enumerate(ACTIONS):
            args_str = ""
            for arg in action["args"]:
                if args_str != "":
                    args_str += ", "
                args_str += f'{arg["name"]}: {arg["type"]}'
            formatted_action = f"{i + 1}. {action['name']}({args_str}): {action['description']}"
            if formatted_actions != "":
                formatted_actions += "\n"
            formatted_actions += formatted_action
        return formatted_actions

    def parse_response(self, text):
        class Argument:
            def __init__(self, name, arg_type):
                self.name = name
                self.type = arg_type

        class Action:
            def __init__(self, name, description, args):
                self.name = name
                self.description = description
                self.args = [Argument(arg["name"], arg["type"]) for arg in args]

        available_actions = [
            Action("click", "Clicks on an element", [{"name": "elementId", "type": "number"}]),
            Action(
                "setValue",
                "Focuses on and sets the value of an input element",
                [{"name": "elementId", "type": "number"}, {"name": "value", "type": "string"}],
            ),
            Action("finish", "Indicates the task is finished", []),
            Action("fail", "Indicates that you are unable to complete the task", []),
        ]
        thought_match = re.search("<Thought>(.*?)</Thought>", text)
        action_match = re.search("<Action>(.*?)</Action>", text)

        if thought_match is None:
            return {"error": "Invalid response: Thought not found in the model response."}

        if action_match is None:
            return {"error": "Invalid response: Action not found in the model response."}

        thought = thought_match.group(1)
        action_string = action_match.group(1)
        action_pattern = re.compile("(\w+)\((.*?)\)")
        action_parts = action_pattern.match(action_string)

        if action_parts is None:
            return {"error": "Invalid action format: Action should be in the format functionName(arg1, arg2, ...)."}

        action_name = action_parts.group(1)
        action_args_string = action_parts.group(2)

        available_action = next((action for action in available_actions if action.name == action_name), None)

        if available_action is None:
            return {"error": f'Invalid action: "{action_name}" is not a valid action.'}

        args_array = [arg.strip() for arg in action_args_string.split(",") if arg.strip() != ""]
        parsed_args = {}

        if len(args_array) != len(available_action.args):
            return {
                "error": f'Invalid number of arguments: Expected {len(available_action.args)} for action "{action_name}", but got {len(args_array)}.'
            }

        for i in range(len(args_array)):
            arg = args_array[i]
            expected_arg = available_action.args[i]

            if expected_arg.type == "number":
                try:
                    number_value = int(arg)
                    parsed_args[expected_arg.name] = number_value
                except ValueError:
                    return {
                        "error": f'Invalid argument type: Expected a number for argument "{expected_arg.name}", but got "{arg}".'
                    }
            elif expected_arg.type == "string":
                if (arg.startswith('"') and arg.endswith('"')) or (arg.startswith("'") and arg.endswith("'")):
                    parsed_args[expected_arg.name] = arg[1:-1]
                else:
                    return {
                        "error": f'Invalid argument type: Expected a string for argument "{expected_arg.name}", but got "{arg}".'
                    }
            else:
                return {
                    "error": f'Invalid argument type: Unknown type "{expected_arg.type}" for argument "{expected_arg.name}".'
                }

        parsed_action = {"name": available_action.name, "args": parsed_args}

        return {"thought": thought, "action": action_string, "parsedAction": parsed_action}

    def _call(
        self,
        inputs: Dict[str, str],
        run_manager: Optional[CallbackManagerForChainRun] = None,
    ) -> Dict[str, str]:
        logger.bind(msg_head="WebotChain inputs").trace(inputs)

        _run_manager = run_manager or CallbackManagerForChainRun.get_noop_manager()

        # Get  ["user_query", "previous_actions", "page_info"], and other raw data
        plan = inputs["plan"]
        previous_actions = inputs["previous_actions"]
        user_query = inputs["user_query"]
        page_info = inputs["page_info"]

        model = HTMLDataModel.from_raw_data(page_info)
        processed_html = model.get_llm_side_data()

        current_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")

        # Generate the prompt
        previous_actions_string = "\n".join(["<Action>{}</Action>".format(action) for action in previous_actions])

        user_prompt = f"The user requests the following task:\n {user_query}\n{previous_actions_string}\nCurrent time: {current_time}\nCurrent page contents:\n{processed_html}"

        print("user_prompt",user_prompt)

        action = self.llm_basic_chain.run(
            **{
                "formattedActions": self.formatted_actions,
                "plan": plan,
                "user_query": user_query,
                "previous_actions_string": previous_actions_string,
                "current_time": current_time,
                "processed_html": processed_html,
            }
        )

        # Extract from "<Action>{}</Action>"
        parsed_return = self.parse_response(action)
        print("parsed_return",parsed_return)
        retry_count = 0
        while "error" in parsed_return and retry_count < 5:
            current_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
            action = self.retry(user_query, previous_actions_string, current_time, page_info, action)
            retry_count += 1
            previous_actions_string += "\n" + action
            parsed_return = self.parse_response(action)

        parsed_action = parsed_return["parsedAction"]

        logger.bind(msg_head="WebotChain generated action").trace(parsed_action)

        return parsed_return

    def retry(self, plan, user_query, previous_actions_string, current_time, page_info: str, last_action: str) -> str:
        action = self.llm_retry_chain.run(
            **{
                "formattedActions": self.formatted_actions,
                "plan": plan,
                "user_query": user_query,
                "previous_actions_string": previous_actions_string,
                "current_time": current_time,
                "processed_html": page_info,
                "last_action": last_action,
            }
        )
        return action

    @classmethod
    def create_basic_prompt(cls, system_prompt, user_prompt) -> BasePromptTemplate:
        # Call the LLM to get the predicted end_point and input_json
        input_variables = [
            "formattedActions",
            "plan",
            "user_query",
            "previous_actions_string",
            "current_time",
            "processed_html",
        ]
        messages = [
            SystemMessagePromptTemplate.from_template(system_prompt),
            HumanMessagePromptTemplate.from_template(user_prompt),
        ]

        return ChatPromptTemplate(input_variables=input_variables, messages=messages)

    @classmethod
    def create_retry_prompt(cls, system_prompt, retry_prompt) -> BasePromptTemplate:
        # Call the LLM to get the predicted end_point and input_json in retry
        input_variables = [
            "formattedActions",
            "plan",
            "user_query",
            "previous_actions_string",
            "current_time",
            "processed_html",
            "retry_message",
        ]
        messages = [
            SystemMessagePromptTemplate.from_template(system_prompt),
            HumanMessagePromptTemplate.from_template(retry_prompt),
        ]

        return ChatPromptTemplate(input_variables=input_variables, messages=messages)

    @classmethod
    def from_llm(cls, llm: BaseLanguageModel, **kwargs: Any) -> WebotChain:
        """Load from the initial web page and user instruction"""
        llm_basic_chain = LLMChain(
            llm=llm,
            prompt=cls.create_retry_prompt(
                system_prompt=SYSTEM_PROMPT,
                retry_prompt=USER_PROMPT,
            ),
        )

        llm_retry_chain = LLMChain(
            llm=llm,
            prompt=cls.create_retry_prompt(
                system_prompt=SYSTEM_PROMPT,
                retry_prompt=RETRY_PROMPT,
            ),
        )

        return cls(
            llm_basic_chain=llm_basic_chain,
            llm_retry_chain=llm_retry_chain,
            **kwargs,
        )


Config

    RETRY_PROMPT,
    SYSTEM_PROMPT,
    USER_PROMPT,
)
from real_agents.web_agent.web_browsing.schema import ACTIONS
from real_agents.adapters.data_model.html import HTMLDataModel

class WebotChain(Chain, BaseModel):
    """Basic prompt based web bot that interact with websites. This implementation is highly motivated by Taxy.ai"""

    llm_basic_chain: LLMChain
    llm_retry_chain: LLMChain

    memory: Optional[ReadOnlySharedStringMemory] = None
    output_key: str = "action"

    chat_id: Optional[str] = None
    user_id: Optional[str] = None

    class Config:
        """Configuration for this pydantic object."""

        extra = Extra.forbid
        arbitrary_types_allowed = True

    @property
    def input_keys(self) -> List[str]:
        """Return the singular input key.

        :meta private:
        """
        return ["user_query", "previous_actions", "page_info"]

    @property
    def output_keys(self) -> List[str]:
        """Return the singular output key.

        :meta private:
        """
        return [self.output_key]

    @property
    def formatted_actions(self) -> str:
        formatted_actions = ""


class WebotChain(Chain, BaseModel):
    """Basic prompt based web bot that interact with websites. This implementation is highly motivated by Taxy.ai"""

    llm_basic_chain: LLMChain
    llm_retry_chain: LLMChain

    memory: Optional[ReadOnlySharedStringMemory] = None
    output_key: str = "action"

    chat_id: Optional[str] = None
    user_id: Optional[str] = None

    class Config:
        """Configuration for this pydantic object."""

        extra = Extra.forbid
        arbitrary_types_allowed = True

    @property
    def input_keys(self) -> List[str]:
        """Return the singular input key.

        :meta private:
        """
        return ["user_query", "previous_actions", "page_info"]

    @property
    def output_keys(self) -> List[str]:
        """Return the singular output key.

        :meta private:
        """
        return [self.output_key]

    @property
    def formatted_actions(self) -> str:
        formatted_actions = ""
        for i, action in enumerate(ACTIONS):
            args_str = ""
            for arg in action["args"]:
                if args_str != "":
                    args_str += ", "
                args_str += f'{arg["name"]}: {arg["type"]}'
            formatted_action = f"{i + 1}. {action['name']}({args_str}): {action['description']}"
            if formatted_actions != "":


    output_key: str = "action"

    chat_id: Optional[str] = None
    user_id: Optional[str] = None

    class Config:
        """Configuration for this pydantic object."""

        extra = Extra.forbid
        arbitrary_types_allowed = True

    @property
    def input_keys(self) -> List[str]:
        """Return the singular input key.

        :meta private:
        """
        return ["user_query", "previous_actions", "page_info"]

    @property
    def output_keys(self) -> List[str]:
        """Return the singular output key.

        :meta private:
        """
        return [self.output_key]

    @property
    def formatted_actions(self) -> str:
        formatted_actions = ""
        for i, action in enumerate(ACTIONS):
            args_str = ""
            for arg in action["args"]:
                if args_str != "":
                    args_str += ", "
                args_str += f'{arg["name"]}: {arg["type"]}'
            formatted_action = f"{i + 1}. {action['name']}({args_str}): {action['description']}"
            if formatted_actions != "":
                formatted_actions += "\n"
            formatted_actions += formatted_action
        return formatted_actions

    def parse_response(self, text):
        class Argument:
            def __init__(self, name, arg_type):
                self.name = name


        extra = Extra.forbid
        arbitrary_types_allowed = True

    @property
    def input_keys(self) -> List[str]:
        """Return the singular input key.

        :meta private:
        """
        return ["user_query", "previous_actions", "page_info"]

    @property
    def output_keys(self) -> List[str]:
        """Return the singular output key.

        :meta private:
        """
        return [self.output_key]

    @property
    def formatted_actions(self) -> str:
        formatted_actions = ""
        for i, action in enumerate(ACTIONS):
            args_str = ""
            for arg in action["args"]:
                if args_str != "":
                    args_str += ", "
                args_str += f'{arg["name"]}: {arg["type"]}'
            formatted_action = f"{i + 1}. {action['name']}({args_str}): {action['description']}"
            if formatted_actions != "":
                formatted_actions += "\n"
            formatted_actions += formatted_action
        return formatted_actions

    def parse_response(self, text):
        class Argument:
            def __init__(self, name, arg_type):
                self.name = name
                self.type = arg_type

        class Action:
            def __init__(self, name, description, args):
                self.name = name
                self.description = description
                self.args = [Argument(arg["name"], arg["type"]) for arg in args]

        available_actions = [
            Action("click", "Clicks on an element", [{"name": "elementId", "type": "number"}]),
            Action(
                "setValue",
                "Focuses on and sets the value of an input element",
                [{"name": "elementId", "type": "number"}, {"name": "value", "type": "string"}],
            ),


        :meta private:
        """
        return [self.output_key]

    @property
    def formatted_actions(self) -> str:
        formatted_actions = ""
        for i, action in enumerate(ACTIONS):
            args_str = ""
            for arg in action["args"]:
                if args_str != "":
                    args_str += ", "
                args_str += f'{arg["name"]}: {arg["type"]}'
            formatted_action = f"{i + 1}. {action['name']}({args_str}): {action['description']}"
            if formatted_actions != "":
                formatted_actions += "\n"
            formatted_actions += formatted_action
        return formatted_actions

    def parse_response(self, text):
        class Argument:
            def __init__(self, name, arg_type):
                self.name = name
                self.type = arg_type

        class Action:
            def __init__(self, name, description, args):
                self.name = name
                self.description = description
                self.args = [Argument(arg["name"], arg["type"]) for arg in args]

        available_actions = [
            Action("click", "Clicks on an element", [{"name": "elementId", "type": "number"}]),
            Action(
                "setValue",
                "Focuses on and sets the value of an input element",
                [{"name": "elementId", "type": "number"}, {"name": "value", "type": "string"}],
            ),
            Action("finish", "Indicates the task is finished", []),
            Action("fail", "Indicates that you are unable to complete the task", []),
        ]
        thought_match = re.search("<Thought>(.*?)</Thought>", text)
        action_match = re.search("<Action>(.*?)</Action>", text)

        if thought_match is None:
            return {"error": "Invalid response: Thought not found in the model response."}

        if action_match is None:
            return {"error": "Invalid response: Action not found in the model response."}

        thought = thought_match.group(1)
        action_string = action_match.group(1)
        action_pattern = re.compile("(\w+)\((.*?)\)")
        action_parts = action_pattern.match(action_string)

        if action_parts is None:
            return {"error": "Invalid action format: Action should be in the format functionName(arg1, arg2, ...)."}

        action_name = action_parts.group(1)
        action_args_string = action_parts.group(2)

        available_action = next((action for action in available_actions if action.name == action_name), None)

        if available_action is None:
            return {"error": f'Invalid action: "{action_name}" is not a valid action.'}

        args_array = [arg.strip() for arg in action_args_string.split(",") if arg.strip() != ""]
        parsed_args = {}

        if len(args_array) != len(available_action.args):
            return {
                "error": f'Invalid number of arguments: Expected {len(available_action.args)} for action "{action_name}", but got {len(args_array)}.'
            }

        for i in range(len(args_array)):
            arg = args_array[i]
            expected_arg = available_action.args[i]

            if expected_arg.type == "number":
                try:
                    number_value = int(arg)
                    parsed_args[expected_arg.name] = number_value
                except ValueError:
                    return {
                        "error": f'Invalid argument type: Expected a number for argument "{expected_arg.name}", but got "{arg}".'
                    }
            elif expected_arg.type == "string":
                if (arg.startswith('"') and arg.endswith('"')) or (arg.startswith("'") and arg.endswith("'")):
                    parsed_args[expected_arg.name] = arg[1:-1]
                else:
                    return {
                        "error": f'Invalid argument type: Expected a string for argument "{expected_arg.name}", but got "{arg}".'
                    }
            else:
                return {
                    "error": f'Invalid argument type: Unknown type "{expected_arg.type}" for argument "{expected_arg.name}".'
                }

        parsed_action = {"name": available_action.name, "args": parsed_args}

        return {"thought": thought, "action": action_string, "parsedAction": parsed_action}

    def _call(
        self,
        inputs: Dict[str, str],
        run_manager: Optional[CallbackManagerForChainRun] = None,
    ) -> Dict[str, str]:
        logger.bind(msg_head="WebotChain inputs").trace(inputs)

        _run_manager = run_manager or CallbackManagerForChainRun.get_noop_manager()

        # Get  ["user_query", "previous_actions", "page_info"], and other raw data
        plan = inputs["plan"]
        previous_actions = inputs["previous_actions"]
        user_query = inputs["user_query"]
        page_info = inputs["page_info"]

        model = HTMLDataModel.from_raw_data(page_info)
        processed_html = model.get_llm_side_data()

        current_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")


                except ValueError:
                    return {
                        "error": f'Invalid argument type: Expected a number for argument "{expected_arg.name}", but got "{arg}".'
                    }
            elif expected_arg.type == "string":
                if (arg.startswith('"') and arg.endswith('"')) or (arg.startswith("'") and arg.endswith("'")):
                    parsed_args[expected_arg.name] = arg[1:-1]
                else:
                    return {
                        "error": f'Invalid argument type: Expected a string for argument "{expected_arg.name}", but got "{arg}".'
                    }
            else:
                return {
                    "error": f'Invalid argument type: Unknown type "{expected_arg.type}" for argument "{expected_arg.name}".'
                }

        parsed_action = {"name": available_action.name, "args": parsed_args}

        return {"thought": thought, "action": action_string, "parsedAction": parsed_action}

    def _call(
        self,
        inputs: Dict[str, str],
        run_manager: Optional[CallbackManagerForChainRun] = None,
    ) -> Dict[str, str]:
        logger.bind(msg_head="WebotChain inputs").trace(inputs)

        _run_manager = run_manager or CallbackManagerForChainRun.get_noop_manager()

        # Get  ["user_query", "previous_actions", "page_info"], and other raw data
        plan = inputs["plan"]
        previous_actions = inputs["previous_actions"]
        user_query = inputs["user_query"]
        page_info = inputs["page_info"]

        model = HTMLDataModel.from_raw_data(page_info)
        processed_html = model.get_llm_side_data()

        current_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")

        # Generate the prompt
        previous_actions_string = "\n".join(["<Action>{}</Action>".format(action) for action in previous_actions])

        user_prompt = f"The user requests the following task:\n {user_query}\n{previous_actions_string}\nCurrent time: {current_time}\nCurrent page contents:\n{processed_html}"

        print("user_prompt",user_prompt)

        action = self.llm_basic_chain.run(
            **{
                "formattedActions": self.formatted_actions,
                "plan": plan,
                "user_query": user_query,
                "previous_actions_string": previous_actions_string,
                "current_time": current_time,
                "processed_html": processed_html,
            }
        )

        # Extract from "<Action>{}</Action>"
        parsed_return = self.parse_response(action)
        print("parsed_return",parsed_return)
        retry_count = 0
        while "error" in parsed_return and retry_count < 5:
            current_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
            action = self.retry(user_query, previous_actions_string, current_time, page_info, action)
            retry_count += 1
            previous_actions_string += "\n" + action
            parsed_return = self.parse_response(action)

        parsed_action = parsed_return["parsedAction"]

        logger.bind(msg_head="WebotChain generated action").trace(parsed_action)

        return parsed_return

    def retry(self, plan, user_query, previous_actions_string, current_time, page_info: str, last_action: str) -> str:
        action = self.llm_retry_chain.run(
            **{
                "formattedActions": self.formatted_actions,
                "plan": plan,
                "user_query": user_query,
                "previous_actions_string": previous_actions_string,
                "current_time": current_time,
                "processed_html": page_info,
                "last_action": last_action,
            }
        )
        return action

    @classmethod
    def create_basic_prompt(cls, system_prompt, user_prompt) -> BasePromptTemplate:
        # Call the LLM to get the predicted end_point and input_json
        input_variables = [
            "formattedActions",


retry

            }
        )

        # Extract from "<Action>{}</Action>"
        parsed_return = self.parse_response(action)
        print("parsed_return",parsed_return)
        retry_count = 0
        while "error" in parsed_return and retry_count < 5:
            current_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
            action = self.retry(user_query, previous_actions_string, current_time, page_info, action)
            retry_count += 1
            previous_actions_string += "\n" + action
            parsed_return = self.parse_response(action)

        parsed_action = parsed_return["parsedAction"]

        logger.bind(msg_head="WebotChain generated action").trace(parsed_action)

        return parsed_return

    def retry(self, plan, user_query, previous_actions_string, current_time, page_info: str, last_action: str) -> str:
        action = self.llm_retry_chain.run(
            **{
                "formattedActions": self.formatted_actions,
                "plan": plan,
                "user_query": user_query,
                "previous_actions_string": previous_actions_string,
                "current_time": current_time,
                "processed_html": page_info,
                "last_action": last_action,
            }
        )
        return action

    @classmethod
    def create_basic_prompt(cls, system_prompt, user_prompt) -> BasePromptTemplate:
        # Call the LLM to get the predicted end_point and input_json
        input_variables = [
            "formattedActions",
            "plan",
            "user_query",
            "previous_actions_string",
            "current_time",
            "processed_html",
        ]
        messages = [
            SystemMessagePromptTemplate.from_template(system_prompt),
            HumanMessagePromptTemplate.from_template(user_prompt),
        ]

        return ChatPromptTemplate(input_variables=input_variables, messages=messages)

    @classmethod


        logger.bind(msg_head="WebotChain generated action").trace(parsed_action)

        return parsed_return

    def retry(self, plan, user_query, previous_actions_string, current_time, page_info: str, last_action: str) -> str:
        action = self.llm_retry_chain.run(
            **{
                "formattedActions": self.formatted_actions,
                "plan": plan,
                "user_query": user_query,
                "previous_actions_string": previous_actions_string,
                "current_time": current_time,
                "processed_html": page_info,
                "last_action": last_action,
            }
        )
        return action

    @classmethod
    def create_basic_prompt(cls, system_prompt, user_prompt) -> BasePromptTemplate:
        # Call the LLM to get the predicted end_point and input_json
        input_variables = [
            "formattedActions",
            "plan",
            "user_query",
            "previous_actions_string",
            "current_time",
            "processed_html",
        ]
        messages = [
            SystemMessagePromptTemplate.from_template(system_prompt),
            HumanMessagePromptTemplate.from_template(user_prompt),
        ]

        return ChatPromptTemplate(input_variables=input_variables, messages=messages)

    @classmethod
    def create_retry_prompt(cls, system_prompt, retry_prompt) -> BasePromptTemplate:
        # Call the LLM to get the predicted end_point and input_json in retry
        input_variables = [
            "formattedActions",
            "plan",
            "user_query",
            "previous_actions_string",
            "current_time",
            "processed_html",
            "retry_message",
        ]
        messages = [
            SystemMessagePromptTemplate.from_template(system_prompt),
            HumanMessagePromptTemplate.from_template(retry_prompt),
        ]

        return ChatPromptTemplate(input_variables=input_variables, messages=messages)


    @classmethod
    def create_basic_prompt(cls, system_prompt, user_prompt) -> BasePromptTemplate:
        # Call the LLM to get the predicted end_point and input_json
        input_variables = [
            "formattedActions",
            "plan",
            "user_query",
            "previous_actions_string",
            "current_time",
            "processed_html",
        ]
        messages = [
            SystemMessagePromptTemplate.from_template(system_prompt),
            HumanMessagePromptTemplate.from_template(user_prompt),
        ]

        return ChatPromptTemplate(input_variables=input_variables, messages=messages)

    @classmethod
    def create_retry_prompt(cls, system_prompt, retry_prompt) -> BasePromptTemplate:
        # Call the LLM to get the predicted end_point and input_json in retry
        input_variables = [
            "formattedActions",
            "plan",
            "user_query",
            "previous_actions_string",
            "current_time",
            "processed_html",
            "retry_message",
        ]
        messages = [
            SystemMessagePromptTemplate.from_template(system_prompt),
            HumanMessagePromptTemplate.from_template(retry_prompt),
        ]

        return ChatPromptTemplate(input_variables=input_variables, messages=messages)

    @classmethod
    def from_llm(cls, llm: BaseLanguageModel, **kwargs: Any) -> WebotChain:
        """Load from the initial web page and user instruction"""
        llm_basic_chain = LLMChain(
            llm=llm,
            prompt=cls.create_retry_prompt(
                system_prompt=SYSTEM_PROMPT,
                retry_prompt=USER_PROMPT,
            ),
        )

        llm_retry_chain = LLMChain(
            llm=llm,
            prompt=cls.create_retry_prompt(
                system_prompt=SYSTEM_PROMPT,
                retry_prompt=RETRY_PROMPT,
            ),
        )


    @classmethod
    def create_retry_prompt(cls, system_prompt, retry_prompt) -> BasePromptTemplate:
        # Call the LLM to get the predicted end_point and input_json in retry
        input_variables = [
            "formattedActions",
            "plan",
            "user_query",
            "previous_actions_string",
            "current_time",
            "processed_html",
            "retry_message",
        ]
        messages = [
            SystemMessagePromptTemplate.from_template(system_prompt),
            HumanMessagePromptTemplate.from_template(retry_prompt),
        ]

        return ChatPromptTemplate(input_variables=input_variables, messages=messages)

    @classmethod
    def from_llm(cls, llm: BaseLanguageModel, **kwargs: Any) -> WebotChain:
        """Load from the initial web page and user instruction"""
        llm_basic_chain = LLMChain(
            llm=llm,
            prompt=cls.create_retry_prompt(
                system_prompt=SYSTEM_PROMPT,
                retry_prompt=USER_PROMPT,
            ),
        )

        llm_retry_chain = LLMChain(
            llm=llm,
            prompt=cls.create_retry_prompt(
                system_prompt=SYSTEM_PROMPT,
                retry_prompt=RETRY_PROMPT,
            ),
        )

        return cls(
            llm_basic_chain=llm_basic_chain,
            llm_retry_chain=llm_retry_chain,
            **kwargs,
        )


Argument

        :meta private:
        """
        return [self.output_key]

    @property
    def formatted_actions(self) -> str:
        formatted_actions = ""
        for i, action in enumerate(ACTIONS):
            args_str = ""
            for arg in action["args"]:
                if args_str != "":
                    args_str += ", "
                args_str += f'{arg["name"]}: {arg["type"]}'
            formatted_action = f"{i + 1}. {action['name']}({args_str}): {action['description']}"
            if formatted_actions != "":
                formatted_actions += "\n"
            formatted_actions += formatted_action
        return formatted_actions

    def parse_response(self, text):
        class Argument:
            def __init__(self, name, arg_type):
                self.name = name
                self.type = arg_type

        class Action:
            def __init__(self, name, description, args):
                self.name = name
                self.description = description
                self.args = [Argument(arg["name"], arg["type"]) for arg in args]

        available_actions = [
            Action("click", "Clicks on an element", [{"name": "elementId", "type": "number"}]),
            Action(
                "setValue",
                "Focuses on and sets the value of an input element",
                [{"name": "elementId", "type": "number"}, {"name": "value", "type": "string"}],
            ),
            Action("finish", "Indicates the task is finished", []),
            Action("fail", "Indicates that you are unable to complete the task", []),
        ]
        thought_match = re.search("<Thought>(.*?)</Thought>", text)
        action_match = re.search("<Action>(.*?)</Action>", text)


Action

    def formatted_actions(self) -> str:
        formatted_actions = ""
        for i, action in enumerate(ACTIONS):
            args_str = ""
            for arg in action["args"]:
                if args_str != "":
                    args_str += ", "
                args_str += f'{arg["name"]}: {arg["type"]}'
            formatted_action = f"{i + 1}. {action['name']}({args_str}): {action['description']}"
            if formatted_actions != "":
                formatted_actions += "\n"
            formatted_actions += formatted_action
        return formatted_actions

    def parse_response(self, text):
        class Argument:
            def __init__(self, name, arg_type):
                self.name = name
                self.type = arg_type

        class Action:
            def __init__(self, name, description, args):
                self.name = name
                self.description = description
                self.args = [Argument(arg["name"], arg["type"]) for arg in args]

        available_actions = [
            Action("click", "Clicks on an element", [{"name": "elementId", "type": "number"}]),
            Action(
                "setValue",
                "Focuses on and sets the value of an input element",
                [{"name": "elementId", "type": "number"}, {"name": "value", "type": "string"}],
            ),
            Action("finish", "Indicates the task is finished", []),
            Action("fail", "Indicates that you are unable to complete the task", []),
        ]
        thought_match = re.search("<Thought>(.*?)</Thought>", text)
        action_match = re.search("<Action>(.*?)</Action>", text)

        if thought_match is None:
            return {"error": "Invalid response: Thought not found in the model response."}

        if action_match is None:
            return {"error": "Invalid response: Action not found in the model response."}


        formatted_actions = ""
        for i, action in enumerate(ACTIONS):
            args_str = ""
            for arg in action["args"]:
                if args_str != "":
                    args_str += ", "
                args_str += f'{arg["name"]}: {arg["type"]}'
            formatted_action = f"{i + 1}. {action['name']}({args_str}): {action['description']}"
            if formatted_actions != "":
                formatted_actions += "\n"
            formatted_actions += formatted_action
        return formatted_actions

    def parse_response(self, text):
        class Argument:
            def __init__(self, name, arg_type):
                self.name = name
                self.type = arg_type

        class Action:
            def __init__(self, name, description, args):
                self.name = name
                self.description = description
                self.args = [Argument(arg["name"], arg["type"]) for arg in args]

        available_actions = [
            Action("click", "Clicks on an element", [{"name": "elementId", "type": "number"}]),
            Action(
                "setValue",
                "Focuses on and sets the value of an input element",
                [{"name": "elementId", "type": "number"}, {"name": "value", "type": "string"}],
            ),
            Action("finish", "Indicates the task is finished", []),
            Action("fail", "Indicates that you are unable to complete the task", []),
        ]
        thought_match = re.search("<Thought>(.*?)</Thought>", text)
        action_match = re.search("<Action>(.*?)</Action>", text)

        if thought_match is None:
            return {"error": "Invalid response: Thought not found in the model response."}

        if action_match is None:
            return {"error": "Invalid response: Action not found in the model response."}


SYSTEM_PROMPT = (
    """
You are a browser automation assistant.

You MUST take one of the following actions. NEVER EVER EVER make up actions that do not exist:

{formattedActions}

You will be be given a task to perform and the current state of the DOM. You will also be given previous actions that you have taken. You may retry a failed action up to one time.

This is an example of an action:

<Action>click(223)</Action>

You MUST always include the <Thought> and <Action> open/close tags or else your response will be marked as invalid.

Rules you MUST follow:
1. You must only take one step at a time. You cannot take multiple actions in a single response.
2. You should not consider the action to present the result to the user. You only need to do available actions. If info in current page is enough for the user to solve the problem, you should finish.
""".strip()
    + "\n"
)

USER_PROMPT = (
    """
The user requests the following task:

{user_query}

{previous_actions_string}

Current time: {current_time}

Current page contents:
{processed_html}
""".strip()
    + "\n"
)
# You MUST break your actions up and CAN ONLY return one action at a time.
# If the user ask you about information. After you go to a page that have enough information, you MUST return <Action>finish()</Action>. The other one will do summarize for you.

RETRY_PROMPT = (
    """
The user requests the following task:

{user_query}

{previous_actions_string}

Current time: {current_time}

Current page contents:
{processed_html}

Your last answer has some problem:
{retry_message}
This answer format is incorrect, You MUST always include the <Action> open/close tags and only do one thing at a time, or else your response will be marked as invalid.
""".strip()
    + "\n"
)


"""Implementation for prompt based react web bots."""
from __future__ import annotations

import datetime
import re
from typing import Any, Dict, List, Optional, Tuple
from loguru import logger

from langchain.base_language import BaseLanguageModel
from langchain.callbacks.manager import CallbackManagerForChainRun
from langchain.chains.llm import LLMChain

from real_agents.web_agent.web_browsing.end2end.base import WebotChain
from real_agents.web_agent.web_browsing.react.prompt import (
    RETRY_PROMPT,
    SYSTEM_PROMPT,
    USER_PROMPT,
)

class ReActWebotChain(WebotChain):
    """Basic prompt based web bot that interact with websites."""

    max_retry_times: int = 3

    @property
    def input_keys(self) -> List[str]:
        """Return the singular input key.

        :meta private:
        """
        return ["plan", "user_query", "previous_actions", "previous_thoughts", "page_info"]

    @property
    def output_keys(self) -> List[str]:
        """Return the singular output key.

        :meta private:
        """
        # success: boolean, message: string, action: string, thought: string
        # success is a boolean indicating whether the action was successful
        # message is the error message that will be displayed to the user if success == false
        # example: {'success': True, 'message' = 'success', 'thought': "I should first set the value in the search field to '...'", 'action': 'setValue(93, "...")', 'parsedAction': {'name': 'setValue', 'args': {'elementId': 93, 'value': '...'}}}
        return ["success", "message", "action", "thought", "parsedAction"]

    #example case: _format_error_output({"error": "This model's maximum context length is 8192 tokens. However, your messages resulted in 8243 tokens. Please reduce the length of the messages."})
    #you need to input like the example, i.e. stringfy the error thrown and put it in a dict with the key "error"
    def _format_error_output(self, error_output: Dict[str, str]) -> Dict[str, Any]:
        return {
            "success": False,
            "message": error_output["error"],
            "action": "error",
            "thought": error_output["error"],
            "parsedAction": {"name": "error", "args": {}},
        }

    def parse_response(self, text):
        if "finish" in text:
            thought_match = re.search("<Thought>(.*?)</Thought>", text)
            if thought_match is None:
                return self._format_error_output({"error": "Invalid response: Thought not found in the model response."})
            thought = thought_match.group(1)
            action_string = "finish"
            parsed_action = "finish"
            return {"thought": thought, "action": action_string, "parsedAction": {"name": "finish", "args": {}}}

        class Argument:
            def __init__(self, name, arg_type):
                self.name = name
                self.type = arg_type

        class Action:
            def __init__(self, name, description, args):
                self.name = name
                self.description = description
                self.args = [Argument(arg["name"], arg["type"]) for arg in args]

        available_actions = [
            Action("click", "Clicks on an element", [{"name": "elementId", "type": "number"}]),
            Action(
                "setValue",
                "Focuses on and sets the value of an input element. Must set a proper value which aligns to the user's request and the function of the input box.",
                [{"name": "elementId", "type": "number"}, {"name": "value", "type": "string"}],
            ),
            Action("finish", "Indicates the task is finished(i.e. the info in the new page is enough for user's request or the task has been successfully completed).", []),
            Action("fail", "Indicates that you are unable to complete the task", []),
        ]
        thought_match = re.search("<Thought>\s*(.*?)\s*</Thought>", text)
        action_match = re.search("<Action>\s*(.*?)\s*</Action>", text)

        if thought_match is None:
            return self._format_error_output({"error": "Invalid response: Thought not found in the model response."})

        if action_match is None:
            return self._format_error_output({"error": "Invalid response: Action not found in the model response."})

        thought = thought_match.group(1)
        action_string = action_match.group(1)
        action_pattern = re.compile("(\w+)\((.*?)\)")
        action_parts = action_pattern.match(action_string)

        if action_parts is None:
            return self._format_error_output({"error": "Invalid action format: Action should be in the format functionName(arg1, arg2, ...)."})

        action_name = action_parts.group(1)
        action_args_string = action_parts.group(2)

        available_action = next((action for action in available_actions if action.name == action_name), None)

        if available_action is None:
            return self._format_error_output({"error": f'Invalid action: "{action_name}" is not a valid action.'})

        # split by "," but only split in the first instance
        args_array = [arg.strip() for arg in action_args_string.split(",", 1) if arg.strip() != ""]
        parsed_args = {}

        if len(args_array) != len(available_action.args):
            return self._format_error_output({
                "error": f'Invalid number of arguments: Expected {len(available_action.args)} for action "{action_name}", but got {len(args_array)}.'
            })

        for i in range(len(args_array)):
            arg = args_array[i]
            expected_arg = available_action.args[i]

            if expected_arg.type == "number":
                try:
                    number_value = int(arg)
                    parsed_args[expected_arg.name] = number_value
                except ValueError:
                    return self._format_error_output({
                        "error": f'Invalid argument type: Expected a number for argument "{expected_arg.name}", but got "{arg}".'
                    })
            elif expected_arg.type == "string":
                match_single = re.match(r"^'((?:[^']|\\')*)'$", arg)
                match_double = re.match(r'^"((?:[^"]|\\")*)"$', arg)
                if match_single is not None:
                    parsed_args[expected_arg.name] = match_single.group(1)
                elif match_double is not None:
                    parsed_args[expected_arg.name] = match_double.group(1)
                else:
                    return self._format_error_output({
                        "error": f'Invalid argument type: Expected a string for argument "{expected_arg.name}", but got "{arg}".'
                    })
            else:
                return self._format_error_output({
                    "error": f'Invalid argument type: Unknown type "{expected_arg.type}" for argument "{expected_arg.name}".'
                })

        parsed_action = {"name": available_action.name, "args": parsed_args}

        return {"thought": thought, "action": action_string, "parsedAction": parsed_action}

    # check the validity of the action, if ok, return (True, ""), else return (False, error message)
    def _check_valid_action(self, html: str, parsed_return: dict) -> Tuple(bool, str):
        parsedAction = parsed_return["parsedAction"]
        action = parsed_return["action"]
        retry_message = ""

        # check if the element id exists in the html (fixme: maybe more validity checking methods can be applied)
        if parsedAction["name"] == "click":
            elementId = parsedAction["args"].get("elementId", None)
            if elementId != None and str(elementId) in html:
                return True, ""
            else:
                retry_message = "The elementId of your last action does not exist in the html, please try again."
                return False, retry_message
        elif parsedAction["name"] == "setValue":
            elementId = parsedAction["args"].get("elementId", None)
            value = parsedAction["args"].get("value", None)
            if elementId != None and value != None and str(elementId) in html:
                return True, ""
            else:
                retry_message = "The elementId of your last action does not exist in the html, please try again."
                return False, retry_message
        elif "finish" in parsedAction["name"]:
            return True, ""
        elif "fail" in parsedAction["name"]:
            return True, ""
        elif "interrupt" in parsedAction["name"]:
            return True, ""
        # parse error
        elif "error" in parsedAction["name"]:
            retry_message = "Action parse error, please follow the output format and try again."
            return False, retry_message
        else:
            retry_message = "Action not in available actions, please try again."
            return False, retry_message

    def _call(
        self,
        inputs: Dict[str, str],
        run_manager: Optional[CallbackManagerForChainRun] = None,
    ) -> Dict[str, str]:
        # logger.bind(msg_head="ReActWebotChain inputs").trace(inputs)

        _run_manager = run_manager or CallbackManagerForChainRun.get_noop_manager()

        # Get  ["user_query", "previous_actions", "previous_thoughts", "page_info"], and other raw data
        plan = inputs["plan"]
        previous_actions = inputs["previous_actions"]
        previous_thoughts = inputs["previous_thoughts"]
        user_query = inputs["user_query"]
        processed_html = inputs["page_info"]

        current_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")

        # Generate the prompt
        previous_actions_string = "\n".join(
            [
                "<Thought>{}</Thought>\n<Action>{}</Action>".format(thought, action)
                for thought, action in zip(previous_thoughts, previous_actions)
            ]
        )
        if len(previous_actions_string) > 0:
            previous_actions_string  = "You have already taken the following thoughts and actions:\n"+previous_actions_string

        user_prompt = f"The user requests the following task:\n {user_query}\nyou have taken these{previous_actions_string}\nCurrent time: {current_time}\nCurrent page contents:\n{processed_html}"
        # print(user_prompt)

        valid_action = False
        count = 0
        retry_message = ""

        while not valid_action and count < self.max_retry_times:
            try:
                if count == 0:
                    action = self.llm_basic_chain.run(
                    **{
                        "formattedActions": self.formatted_actions,
                        "plan": plan,
                        "user_query": user_query,
                        "previous_actions_string": previous_actions_string,
                        "current_time": current_time,
                        "processed_html": processed_html,
                    }
                )
                else:
                    action = self.llm_retry_chain.run(
                    **{
                        "formattedActions": self.formatted_actions,
                        "plan": plan,
                        "user_query": user_query,
                        "previous_actions_string": previous_actions_string,
                        "current_time": current_time,
                        "processed_html": processed_html,
                        "retry_message": retry_message,
                    }
                )
            except Exception as e:
                print("*"*50)
                print("llm error:",e)
                print("*"*50)
                error_output = {"error": str(e)}
                return self._format_error_output(error_output)

            # print("llm output:",action)

            # Extract from "<Thought>{}</Thought>\n<Action>{}</Action>"
            action = action.split("</Action>")[0] + "</Action>"
            # action: <Thought>I should first set the value in the search field to '...'</Thought>
            #         <Action>setValue(93, "Tao Yu")</Action>
            # print("action:",action)

            # parsed return: {'thought': "I should first set the value in the search field to '...'", 'action': 'setValue(93, "...")', 'parsedAction': {'name': 'setValue', 'args': {'elementId': 93, 'value': '...'}}}
            parsed_return = self.parse_response(action)

            # If parse error, there will be  "success" and "message" key in parsed_return. If no problem, no "success" and "message" key. set its value by setdefault
            parsed_return.setdefault("success", True)
            parsed_return.setdefault("message", "success")

            valid_action, retry_message = self._check_valid_action(processed_html, parsed_return)

        # if "error" not in parsed_return:
            # logger.bind(msg_head="ReActWebotChain generated thought").trace(parsed_return["thought"])
            # logger.bind(msg_head="ReActWebotChain generated action").trace(parsed_return["parsedAction"])

        output = parsed_return

        return output

    @classmethod
    def from_llm(cls, llm: BaseLanguageModel, **kwargs: Any) -> WebotChain:
        """Load from the initial web page and user instruction"""
        llm_basic_chain = LLMChain(
            llm=llm,
            prompt=cls.create_basic_prompt(
                system_prompt=SYSTEM_PROMPT,
                user_prompt=USER_PROMPT,
            ),
        )

        llm_retry_chain = LLMChain(
            llm=llm,
            prompt=cls.create_retry_prompt(
                system_prompt=SYSTEM_PROMPT,
                retry_prompt=RETRY_PROMPT,
            ),
        )

        return cls(
            llm_basic_chain=llm_basic_chain,
            llm_retry_chain=llm_retry_chain,
            **kwargs,
        )


from typing import Any, Dict, List, Optional, Tuple
from loguru import logger

from langchain.base_language import BaseLanguageModel
from langchain.callbacks.manager import CallbackManagerForChainRun
from langchain.chains.llm import LLMChain

from real_agents.web_agent.web_browsing.end2end.base import WebotChain
from real_agents.web_agent.web_browsing.react.prompt import (
    RETRY_PROMPT,
    SYSTEM_PROMPT,
    USER_PROMPT,
)

class ReActWebotChain(WebotChain):
    """Basic prompt based web bot that interact with websites."""

    max_retry_times: int = 3

    @property
    def input_keys(self) -> List[str]:
        """Return the singular input key.

        :meta private:
        """
        return ["plan", "user_query", "previous_actions", "previous_thoughts", "page_info"]

    @property
    def output_keys(self) -> List[str]:
        """Return the singular output key.

        :meta private:
        """
        # success: boolean, message: string, action: string, thought: string
        # success is a boolean indicating whether the action was successful
        # message is the error message that will be displayed to the user if success == false
        # example: {'success': True, 'message' = 'success', 'thought': "I should first set the value in the search field to '...'", 'action': 'setValue(93, "...")', 'parsedAction': {'name': 'setValue', 'args': {'elementId': 93, 'value': '...'}}}
        return ["success", "message", "action", "thought", "parsedAction"]

    #example case: _format_error_output({"error": "This model's maximum context length is 8192 tokens. However, your messages resulted in 8243 tokens. Please reduce the length of the messages."})
    #you need to input like the example, i.e. stringfy the error thrown and put it in a dict with the key "error"
    def _format_error_output(self, error_output: Dict[str, str]) -> Dict[str, Any]:
        return {
            "success": False,
            "message": error_output["error"],
            "action": "error",


from real_agents.web_agent.web_browsing.react.prompt import (
    RETRY_PROMPT,
    SYSTEM_PROMPT,
    USER_PROMPT,
)

class ReActWebotChain(WebotChain):
    """Basic prompt based web bot that interact with websites."""

    max_retry_times: int = 3

    @property
    def input_keys(self) -> List[str]:
        """Return the singular input key.

        :meta private:
        """
        return ["plan", "user_query", "previous_actions", "previous_thoughts", "page_info"]

    @property
    def output_keys(self) -> List[str]:
        """Return the singular output key.

        :meta private:
        """
        # success: boolean, message: string, action: string, thought: string
        # success is a boolean indicating whether the action was successful
        # message is the error message that will be displayed to the user if success == false
        # example: {'success': True, 'message' = 'success', 'thought': "I should first set the value in the search field to '...'", 'action': 'setValue(93, "...")', 'parsedAction': {'name': 'setValue', 'args': {'elementId': 93, 'value': '...'}}}
        return ["success", "message", "action", "thought", "parsedAction"]

    #example case: _format_error_output({"error": "This model's maximum context length is 8192 tokens. However, your messages resulted in 8243 tokens. Please reduce the length of the messages."})
    #you need to input like the example, i.e. stringfy the error thrown and put it in a dict with the key "error"
    def _format_error_output(self, error_output: Dict[str, str]) -> Dict[str, Any]:
        return {
            "success": False,
            "message": error_output["error"],
            "action": "error",
            "thought": error_output["error"],
            "parsedAction": {"name": "error", "args": {}},
        }

    def parse_response(self, text):
        if "finish" in text:
            thought_match = re.search("<Thought>(.*?)</Thought>", text)
            if thought_match is None:
                return self._format_error_output({"error": "Invalid response: Thought not found in the model response."})
            thought = thought_match.group(1)
            action_string = "finish"
            parsed_action = "finish"