From c5c3444bcee6190664de6922be2f2ed71ce9b6e2 Mon Sep 17 00:00:00 2001 From: peterychang <49209570+peterychang@users.noreply.github.com> Date: Fri, 6 Dec 2024 17:53:59 -0500 Subject: [PATCH] Add user proxy docs. Make user proxy's default impl cancellable (#4459) * Add user proxy docs. Make user proxy's default impl cancellable * remove unnecessary import * revert accidental change * address PR comments * uv sync * Fix bugs * poe format * fixing mypy issues * poe format * ignore pyright errors for ainput * fix example code * remove unused import * fix accidental reversion, example code * formatting * fix typing * Update python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/agents.ipynb Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/agents.ipynb --------- Co-authored-by: Jack Gerrits Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Eric Zhu --- .../agents/_user_proxy_agent.py | 69 ++++++++++++++++++- .../tutorial/agents.ipynb | 35 +++++++++- python/packages/autogen-core/pyproject.toml | 1 + python/uv.lock | 11 +++ 4 files changed, 114 insertions(+), 2 deletions(-) diff --git a/python/packages/autogen-agentchat/src/autogen_agentchat/agents/_user_proxy_agent.py b/python/packages/autogen-agentchat/src/autogen_agentchat/agents/_user_proxy_agent.py index 6e92825af..35aa5fa88 100644 --- a/python/packages/autogen-agentchat/src/autogen_agentchat/agents/_user_proxy_agent.py +++ b/python/packages/autogen-agentchat/src/autogen_agentchat/agents/_user_proxy_agent.py @@ -2,6 +2,7 @@ import asyncio from inspect import iscoroutinefunction from typing import Awaitable, Callable, List, Optional, Sequence, Union, cast +from aioconsole import ainput # type: ignore from autogen_core import CancellationToken from ..base import Response @@ -14,6 +15,15 @@ AsyncInputFunc = Callable[[str, Optional[CancellationToken]], Awaitable[str]] InputFuncType = Union[SyncInputFunc, AsyncInputFunc] +# TODO: ainput doesn't seem to play nicely with jupyter. +# No input window appears in this case. +async def cancellable_input(prompt: str, cancellation_token: Optional[CancellationToken]) -> str: + task = asyncio.Task[str](asyncio.create_task(ainput(prompt))) # type: ignore + if cancellation_token is not None: + cancellation_token.link_future(task) + return await task + + class UserProxyAgent(BaseChatAgent): """An agent that can represent a human user through an input function. @@ -40,6 +50,63 @@ class UserProxyAgent(BaseChatAgent): See `Pause for User Input `_ for more information. + Example: + Simple usage case:: + + import asyncio + from autogen_core import CancellationToken + from autogen_agentchat.agents import UserProxyAgent + from autogen_agentchat.messages import TextMessage + + + async def simple_user_agent(): + agent = UserProxyAgent("user_proxy") + response = await asyncio.create_task( + agent.on_messages( + [TextMessage(content="What is your name? ", source="user")], + cancellation_token=CancellationToken(), + ) + ) + print(f"Your name is {response.chat_message.content}") + + Example: + Cancellable usage case:: + + import asyncio + from typing import Any + from autogen_core import CancellationToken + from autogen_agentchat.agents import UserProxyAgent + from autogen_agentchat.messages import TextMessage + + + token = CancellationToken() + agent = UserProxyAgent("user_proxy") + + + async def timeout(delay: float): + await asyncio.sleep(delay) + + + def cancellation_callback(task: asyncio.Task[Any]): + token.cancel() + + + async def cancellable_user_agent(): + try: + timeout_task = asyncio.create_task(timeout(3)) + timeout_task.add_done_callback(cancellation_callback) + agent_task = asyncio.create_task( + agent.on_messages( + [TextMessage(content="What is your name? ", source="user")], + cancellation_token=CancellationToken(), + ) + ) + response = await agent_task + print(f"Your name is {response.chat_message.content}") + except Exception as e: + print(f"Exception: {e}") + except BaseException as e: + print(f"BaseException: {e}") """ def __init__( @@ -51,7 +118,7 @@ class UserProxyAgent(BaseChatAgent): ) -> None: """Initialize the UserProxyAgent.""" super().__init__(name=name, description=description) - self.input_func = input_func or input + self.input_func = input_func or cancellable_input self._is_async = iscoroutinefunction(self.input_func) @property diff --git a/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/agents.ipynb b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/agents.ipynb index e43b83eea..9fcf6933f 100644 --- a/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/agents.ipynb +++ b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/agents.ipynb @@ -26,7 +26,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -101,6 +101,39 @@ "as well as a list of inner messages in the {py:attr}`~autogen_agentchat.base.Response.inner_messages` attribute,\n", "which stores the agent's \"thought process\" that led to the final response.\n", "\n", + "## User Proxy Agent\n", + "\n", + "{py:class}`~autogen_agentchat.agents.UserProxyAgent` is a built-in agent that\n", + "provides one way for a user to intervene in the process. This agent will put the team in a temporary blocking state, and thus any exceptions or runtime failures while in the blocked state will result in a deadlock. It is strongly advised that this agent be coupled with a timeout mechanic and that all errors and exceptions emanating from it are handled." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from autogen_agentchat.agents import UserProxyAgent\n", + "\n", + "\n", + "async def user_proxy_run() -> None:\n", + " user_proxy_agent = UserProxyAgent(\"user_proxy\")\n", + " response = await user_proxy_agent.on_messages(\n", + " [TextMessage(content=\"What is your name? \", source=\"user\")], cancellation_token=CancellationToken()\n", + " )\n", + " print(f\"Your name is {response.chat_message.content}\")\n", + "\n", + "\n", + "# Use asyncio.run(user_proxy_run()) when running in a script.\n", + "await user_proxy_run()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The User Proxy agent is ideally used for on-demand human-in-the-loop interactions for scenarios such as Just In Time approvals, human feedback, alerts, etc. For slower user interactions, consider terminating the session using a termination condition and start another one from run or run_stream with another message.\n", + "\n", "### Stream Messages\n", "\n", "We can also stream each message as it is generated by the agent by using the\n", diff --git a/python/packages/autogen-core/pyproject.toml b/python/packages/autogen-core/pyproject.toml index 1af254fca..0f9e2bc67 100644 --- a/python/packages/autogen-core/pyproject.toml +++ b/python/packages/autogen-core/pyproject.toml @@ -17,6 +17,7 @@ classifiers = [ dependencies = [ "openai>=1.3", "pillow", + "aioconsole", "aiohttp", "typing-extensions", "pydantic<3.0.0,>=2.0.0", diff --git a/python/uv.lock b/python/uv.lock index b841510f4..eeed86fa8 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -111,6 +111,15 @@ dev = [ { name = "types-tabulate" }, ] +[[package]] +name = "aioconsole" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/c9/c57e979eea211b10a63783882a826f257713fa7c0d6c9a6eac851e674fb4/aioconsole-0.8.1.tar.gz", hash = "sha256:0535ce743ba468fb21a1ba43c9563032c779534d4ecd923a46dbd350ad91d234", size = 61085 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/ea/23e756ec1fea0c685149304dda954b3b3932d6d06afbf42a66a2e6dc2184/aioconsole-0.8.1-py3-none-any.whl", hash = "sha256:e1023685cde35dde909fbf00631ffb2ed1c67fe0b7058ebb0892afbde5f213e5", size = 43324 }, +] + [[package]] name = "aiofiles" version = "24.1.0" @@ -356,6 +365,7 @@ name = "autogen-core" version = "0.4.0.dev9" source = { editable = "packages/autogen-core" } dependencies = [ + { name = "aioconsole" }, { name = "aiohttp" }, { name = "asyncio-atexit" }, { name = "jsonref" }, @@ -419,6 +429,7 @@ dev = [ [package.metadata] requires-dist = [ + { name = "aioconsole" }, { name = "aiohttp" }, { name = "asyncio-atexit" }, { name = "grpcio", marker = "extra == 'grpc'", specifier = "~=1.62.0" },