diff --git a/agents/agents.py b/agents/agents.py index c83dbc3..0dee846 100644 --- a/agents/agents.py +++ b/agents/agents.py @@ -92,6 +92,7 @@ class ActionStep: final_answer: Any = None error: AgentError | None = None step_duration: float | None = None + llm_output: str | None = None @dataclass class PlanningStep: @@ -440,23 +441,20 @@ class ReactAgent(BaseAgent): else: self.logs.append(TaskStep(task=task)) - with console.status( - "Agent is running...", spinner="aesthetic" - ): - if oneshot: - step_start_time = time.time() - step_log = ActionStep(start_time=step_start_time) - step_log.step_end_time = time.time() - step_log.step_duration = step_log.step_end_time - step_start_time + if oneshot: + step_start_time = time.time() + step_log = ActionStep(start_time=step_start_time) + step_log.step_end_time = time.time() + step_log.step_duration = step_log.step_end_time - step_start_time - # Run the agent's step - result = self.step(step_log) - return result + # Run the agent's step + result = self.step(step_log) + return result - if stream: - return self.stream_run(task) - else: - return self.direct_run(task) + if stream: + return self.stream_run(task) + else: + return self.direct_run(task) def stream_run(self, task: str): """ @@ -468,6 +466,9 @@ class ReactAgent(BaseAgent): step_start_time = time.time() step_log = ActionStep(iteration=iteration, start_time=step_start_time) try: + if self.planning_interval is not None and iteration % self.planning_interval == 0: + self.planning_step(task, is_first_step=(iteration == 0), iteration=iteration) + console.rule("[bold]New step") self.step(step_log) if step_log.final_answer is not None: final_answer = step_log.final_answer @@ -484,7 +485,6 @@ class ReactAgent(BaseAgent): if final_answer is None and iteration == self.max_iterations: error_message = "Reached max iterations." - console.print(f"[bold red]{error_message}") final_step_log = ActionStep(error=AgentMaxIterationsError(error_message)) self.logs.append(final_step_log) final_answer = self.provide_final_answer(task) @@ -509,6 +509,7 @@ class ReactAgent(BaseAgent): try: if self.planning_interval is not None and iteration % self.planning_interval == 0: self.planning_step(task, is_first_step=(iteration == 0), iteration=iteration) + console.rule("[bold]New step") self.step(step_log) if step_log.final_answer is not None: final_answer = step_log.final_answer @@ -527,7 +528,6 @@ class ReactAgent(BaseAgent): error_message = "Reached max iterations." final_step_log = ActionStep(error=AgentMaxIterationsError(error_message)) self.logs.append(final_step_log) - console.print(f"[bold red]{error_message}") final_answer = self.provide_final_answer(task) final_step_log.final_answer = final_answer final_step_log.step_duration = 0 @@ -677,7 +677,6 @@ class JsonAgent(ReactAgent): agent_memory = self.write_inner_memory_from_logs() self.prompt = agent_memory - console.rule("New step") # Add new step in logs log_entry.agent_memory = agent_memory.copy() @@ -692,12 +691,14 @@ class JsonAgent(ReactAgent): llm_output = self.llm_engine( self.prompt, stop_sequences=["", "Observation:"], **additional_args ) + log_entry.llm_output = llm_output except Exception as e: raise AgentGenerationError(f"Error in generating llm output: {e}.") - console.rule("Output message of the LLM") - console.print(llm_output) - log_entry.llm_output = llm_output + if self.verbose: + console.rule("[italic]Output message of the LLM:") + console.print(llm_output) + # Parse rationale, action = self.extract_action(llm_output=llm_output, split_token="Action:") @@ -796,7 +797,6 @@ class CodeAgent(ReactAgent): agent_memory = self.write_inner_memory_from_logs() self.prompt = agent_memory.copy() - console.rule("New step") # Add new step in logs log_entry.agent_memory = agent_memory.copy() @@ -811,13 +811,13 @@ class CodeAgent(ReactAgent): llm_output = self.llm_engine( self.prompt, stop_sequences=["", "Observation:"], **additional_args ) + log_entry.llm_output = llm_output except Exception as e: raise AgentGenerationError(f"Error in generating llm output: {e}.") if self.verbose: console.rule("[italic]Output message of the LLM:") console.print(Syntax(llm_output, lexer='markdown', background_color='default')) - log_entry.llm_output = llm_output # Parse try: diff --git a/agents/default_tools.py b/agents/default_tools.py index 5a267b9..d294a3e 100644 --- a/agents/default_tools.py +++ b/agents/default_tools.py @@ -185,3 +185,13 @@ class FinalAnswerTool(Tool): def forward(self, answer): return answer + +class UserInputTool(Tool): + name = "user_input" + description = "Asks for user's input on a specific question" + inputs = {"question": {"type": "string", "description": "The question to ask the user"}} + output_type = "string" + + def forward(self, question): + user_input = input(f"{question} => ") + return user_input diff --git a/examples/orchestrator.py b/examples/orchestrator.py new file mode 100644 index 0000000..b5b5afb --- /dev/null +++ b/examples/orchestrator.py @@ -0,0 +1,73 @@ +import re +import requests +from markdownify import markdownify as md +from requests.exceptions import RequestException + +from agents import ( + tool, + CodeAgent, + JsonAgent, + HfApiEngine, + ManagedAgent, +) +from agents.default_tools import UserInputTool +from agents.search import DuckDuckGoSearchTool +from agents.utils import console + +model = "Qwen/Qwen2.5-72B-Instruct" + +@tool +def visit_webpage(url: str) -> str: + """Visits a webpage at the given URL and returns its content as a markdown string. + + Args: + url: The URL of the webpage to visit. + + Returns: + The content of the webpage converted to Markdown, or an error message if the request fails. + """ + try: + # Send a GET request to the URL + response = requests.get(url) + response.raise_for_status() # Raise an exception for bad status codes + + # Convert the HTML content to Markdown + markdown_content = md(response.text).strip() + + # Remove multiple line breaks + markdown_content = re.sub(r"\n{3,}", "\n\n", markdown_content) + + return markdown_content + + except RequestException as e: + return f"Error fetching the webpage: {str(e)}" + except Exception as e: + return f"An unexpected error occurred: {str(e)}" + + +llm_engine = HfApiEngine(model) + +web_agent = JsonAgent( + tools=[DuckDuckGoSearchTool(), visit_webpage], + llm_engine=llm_engine, + max_iterations=10, +) + +managed_web_agent = ManagedAgent( + agent=web_agent, + name="search", + description="Runs web searches for you. Give it your query as an argument.", +) + +manager_agent = CodeAgent( + tools=[UserInputTool()], + llm_engine=llm_engine, + managed_agents=[managed_web_agent], + additional_authorized_imports=["time", "datetime"], +) + +with console.status( + "Agent is running...", spinner="aesthetic" +): + manager_agent.run("""How many years ago was Stripe founded? + You should ask for user input on wether the answer is correct before returning your final answer.""") diff --git a/poetry.lock b/poetry.lock index 543af28..0992e37 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,26 @@ # This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +[[package]] +name = "beautifulsoup4" +version = "4.12.3" +description = "Screen-scraping library" +optional = false +python-versions = ">=3.6.0" +files = [ + {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, + {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, +] + +[package.dependencies] +soupsieve = ">1.2" + +[package.extras] +cchardet = ["cchardet"] +chardet = ["chardet"] +charset-normalizer = ["charset-normalizer"] +html5lib = ["html5lib"] +lxml = ["lxml"] + [[package]] name = "certifi" version = "2024.8.30" @@ -338,6 +359,21 @@ profiling = ["gprof2dot"] rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] +[[package]] +name = "markdownify" +version = "0.14.1" +description = "Convert HTML to markdown." +optional = false +python-versions = "*" +files = [ + {file = "markdownify-0.14.1-py3-none-any.whl", hash = "sha256:4c46a6c0c12c6005ddcd49b45a5a890398b002ef51380cd319db62df5e09bc2a"}, + {file = "markdownify-0.14.1.tar.gz", hash = "sha256:a62a7a216947ed0b8dafb95b99b2ef4a0edd1e18d5653c656f68f03db2bfb2f1"}, +] + +[package.dependencies] +beautifulsoup4 = ">=4.9,<5" +six = ">=1.15,<2" + [[package]] name = "markupsafe" version = "3.0.2" @@ -1096,6 +1132,17 @@ files = [ {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] +[[package]] +name = "soupsieve" +version = "2.6" +description = "A modern CSS selector implementation for Beautiful Soup." +optional = false +python-versions = ">=3.8" +files = [ + {file = "soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9"}, + {file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"}, +] + [[package]] name = "tokenizers" version = "0.21.0" @@ -1301,4 +1348,4 @@ zstd = ["zstandard (>=0.18.0)"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.13" -content-hash = "6c3841968936d66bf70e11c6c8e0a16fec6c2f4d88d79cd8ac5a412225e7cf56" +content-hash = "3a0896faf882952a0d780efcc862017989612fcb421a6ee01e4eec0ba6c0f638" diff --git a/pyproject.toml b/pyproject.toml index 7363250..5c57e81 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,6 +67,7 @@ pandas = "^2.2.3" jinja2 = "^3.1.4" pillow = "^11.0.0" llama-cpp-python = "^0.3.4" +markdownify = "^0.14.1" [build-system]