Skip to content

Commit 2997c27

Browse files
authored
Add Human Input Mode in AGS (#4210)
* add react-flow * update tutorial, some ui improvement for agent thread layout * v1 for interactive viz for agent state * add Ui visualization of message based transitions * minor updates * fix node edges, support visualization of self loops * improve edges and layout, add support for selector group chat prompt * minor ui tweaks * ui and layout updates * ugrade dependencies to fix dependabot scan errors * persist sidebar, enable contentbar title mechanism #4191 * add support for user proxy agent, support human in put mode. #4011 * add UI support for human input mode via a userproxy agent #4011 * version update * fix db initialization bug * support for human input mode in UI, fix backend api route minor bugs * update pyproject toml and uv lock * readme update, support full screen mode for agent visualiation * update uv.lock
1 parent d55e68f commit 2997c27

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+3151
-1011
lines changed

python/packages/autogen-studio/README.md

+11-3
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
[![PyPI version](https://badge.fury.io/py/autogenstudio.svg)](https://badge.fury.io/py/autogenstudio)
44
[![Downloads](https://static.pepy.tech/badge/autogenstudio/week)](https://pepy.tech/project/autogenstudio)
55

6-
![ARA](./docs/ara_stockprices.png)
6+
![ARA](./docs/ags_screen.png)
77

88
AutoGen Studio is an AutoGen-powered AI app (user interface) to help you rapidly prototype AI agents, enhance them with skills, compose them into workflows and interact with them to accomplish tasks. It is built on top of the [AutoGen](https://microsoft.github.io/autogen) framework, which is a toolkit for building AI agents.
99

10-
Code for AutoGen Studio is on GitHub at [microsoft/autogen](https://github.com/microsoft/autogen/tree/main/samples/apps/autogen-studio)
10+
Code for AutoGen Studio is on GitHub at [microsoft/autogen](https://github.com/microsoft/autogen/tree/main/python/packages/autogen-studio)
1111

1212
> **Note**: AutoGen Studio is meant to help you rapidly prototype multi-agent workflows and demonstrate an example of end user interfaces built with AutoGen. It is not meant to be a production-ready app.
1313
@@ -16,6 +16,7 @@ Code for AutoGen Studio is on GitHub at [microsoft/autogen](https://github.com/m
1616
1717
**Updates**
1818

19+
> Nov 14: AutoGen Studio is being rewritten to use the updated AutoGen 0.4.0 api AgentChat api.
1920
> April 17: AutoGen Studio database layer is now rewritten to use [SQLModel](https://sqlmodel.tiangolo.com/) (Pydantic + SQLAlchemy). This provides entity linking (skills, models, agents and workflows are linked via association tables) and supports multiple [database backend dialects](https://docs.sqlalchemy.org/en/20/dialects/) supported in SQLAlchemy (SQLite, PostgreSQL, MySQL, Oracle, Microsoft SQL Server). The backend database can be specified a `--database-uri` argument when running the application. For example, `autogenstudio ui --database-uri sqlite:///database.sqlite` for SQLite and `autogenstudio ui --database-uri postgresql+psycopg://user:password@localhost/dbname` for PostgreSQL.
2021
2122
> March 12: Default directory for AutoGen Studio is now /home/<user>/.autogenstudio. You can also specify this directory using the `--appdir` argument when running the application. For example, `autogenstudio ui --appdir /path/to/folder`. This will store the database and other files in the specified directory e.g. `/path/to/folder/database.sqlite`. `.env` files in that directory will be used to set environment variables for the app.
@@ -49,7 +50,7 @@ There are two ways to install AutoGen Studio - from PyPi or from source. We **re
4950
pip install -e .
5051
```
5152
52-
- Navigate to the `samples/apps/autogen-studio/frontend` directory, install dependencies, and build the UI:
53+
- Navigate to the `python/packages/autogen-studio/frontend` directory, install dependencies, and build the UI:
5354
5455
```bash
5556
npm install -g gatsby-cli
@@ -88,16 +89,23 @@ AutoGen Studio also takes several parameters to customize the application:
8889
Now that you have AutoGen Studio installed and running, you are ready to explore its capabilities, including defining and modifying agent workflows, interacting with agents and sessions, and expanding agent skills.
8990

9091
#### If running from source
92+
9193
When running from source, you need to separately bring up the frontend server.
94+
9295
1. Open a separate terminal and change directory to the frontend
96+
9397
```bash
9498
cd frontend
9599
```
100+
96101
3. Create a `.env.development` file.
102+
97103
```bash
98104
cp .env.default .env.development
99105
```
106+
100107
3. Launch frontend server
108+
101109
```bash
102110
npm run start
103111
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .agents.userproxy import UserProxyAgent

python/packages/autogen-studio/autogenstudio/components/agents/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from typing import Callable, List, Optional, Sequence, Union, Awaitable
2+
from inspect import iscoroutinefunction
3+
4+
from autogen_agentchat.agents import BaseChatAgent
5+
from autogen_agentchat.base import Response
6+
from autogen_agentchat.messages import ChatMessage, TextMessage
7+
from autogen_core.base import CancellationToken
8+
import asyncio
9+
10+
11+
class UserProxyAgent(BaseChatAgent):
12+
"""An agent that can represent a human user in a chat."""
13+
14+
def __init__(
15+
self,
16+
name: str,
17+
description: Optional[str] = "a",
18+
input_func: Optional[Union[Callable[..., str],
19+
Callable[..., Awaitable[str]]]] = None
20+
) -> None:
21+
super().__init__(name, description=description)
22+
self.input_func = input_func or input
23+
self._is_async = iscoroutinefunction(
24+
input_func) if input_func else False
25+
26+
@property
27+
def produced_message_types(self) -> List[type[ChatMessage]]:
28+
return [TextMessage]
29+
30+
async def _get_input(self, prompt: str) -> str:
31+
"""Handle both sync and async input functions"""
32+
if self._is_async:
33+
return await self.input_func(prompt)
34+
else:
35+
return await asyncio.get_event_loop().run_in_executor(None, self.input_func, prompt)
36+
37+
async def on_messages(self, messages: Sequence[ChatMessage], cancellation_token: CancellationToken) -> Response:
38+
39+
try:
40+
user_input = await self._get_input("Enter your response: ")
41+
return Response(chat_message=TextMessage(content=user_input, source=self.name))
42+
except Exception as e:
43+
# Consider logging the error here
44+
raise RuntimeError(f"Failed to get user input: {str(e)}") from e
45+
46+
async def on_reset(self, cancellation_token: CancellationToken) -> None:
47+
pass
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
from .db_manager import DatabaseManager
2-
from .component_factory import ComponentFactory
2+
from .component_factory import ComponentFactory, Component
33
from .config_manager import ConfigurationManager

python/packages/autogen-studio/autogenstudio/database/component_factory.py

+47-25
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import os
22
from pathlib import Path
3-
from typing import List, Literal, Union, Optional, Dict, Any, Type
3+
from typing import Callable, List, Literal, Union, Optional, Dict, Any, Type
44
from datetime import datetime
55
import json
66
from autogen_agentchat.task import MaxMessageTermination, TextMentionTermination, StopMessageTermination
@@ -13,6 +13,7 @@
1313
TeamTypes, AgentTypes, ModelTypes, ToolTypes,
1414
ComponentType, ComponentConfig, ComponentConfigInput, TerminationConfig, TerminationTypes, Response
1515
)
16+
from ..components import UserProxyAgent
1617
from autogen_agentchat.agents import AssistantAgent
1718
from autogen_agentchat.teams import RoundRobinGroupChat, SelectorGroupChat
1819
from autogen_ext.models import OpenAIChatCompletionClient
@@ -38,6 +39,17 @@
3839
Component = Union[RoundRobinGroupChat, SelectorGroupChat,
3940
AssistantAgent, OpenAIChatCompletionClient, FunctionTool]
4041

42+
DEFAULT_SELECTOR_PROMPT = """You are in a role play game. The following roles are available:
43+
{roles}.
44+
Read the following conversation. Then select the next role from {participants} to play. Only return the role.
45+
46+
{history}
47+
48+
Read the above conversation. Then select the next role from {participants} to play. Only return the role.
49+
"""
50+
51+
CONFIG_RETURN_TYPES = Literal['object', 'dict', 'config']
52+
4153

4254
class ComponentFactory:
4355
"""Creates and manages agent components with versioned configuration loading"""
@@ -55,19 +67,22 @@ def __init__(self):
5567
self._tool_cache: Dict[str, FunctionTool] = {}
5668
self._last_cache_clear = datetime.now()
5769

58-
async def load(self, component: ComponentConfigInput, return_type: ReturnType = 'object') -> Union[Component, dict, ComponentConfig]:
70+
async def load(
71+
self,
72+
component: ComponentConfigInput,
73+
input_func: Optional[Callable] = None,
74+
return_type: ReturnType = 'object'
75+
) -> Union[Component, dict, ComponentConfig]:
5976
"""
6077
Universal loader for any component type
6178
6279
Args:
6380
component: Component configuration (file path, dict, or ComponentConfig)
81+
input_func: Optional callable for user input handling
6482
return_type: Type of return value ('object', 'dict', or 'config')
6583
6684
Returns:
6785
Component instance, config dict, or ComponentConfig based on return_type
68-
69-
Raises:
70-
ValueError: If component type is unknown or version unsupported
7186
"""
7287
try:
7388
# Load and validate config
@@ -95,8 +110,8 @@ async def load(self, component: ComponentConfigInput, return_type: ReturnType =
95110

96111
# Otherwise create and return component instance
97112
handlers = {
98-
ComponentType.TEAM: self.load_team,
99-
ComponentType.AGENT: self.load_agent,
113+
ComponentType.TEAM: lambda c: self.load_team(c, input_func),
114+
ComponentType.AGENT: lambda c: self.load_agent(c, input_func),
100115
ComponentType.MODEL: self.load_model,
101116
ComponentType.TOOL: self.load_tool,
102117
ComponentType.TERMINATION: self.load_termination
@@ -113,7 +128,7 @@ async def load(self, component: ComponentConfigInput, return_type: ReturnType =
113128
logger.error(f"Failed to load component: {str(e)}")
114129
raise
115130

116-
async def load_directory(self, directory: Union[str, Path], check_exists: bool = False, return_type: ReturnType = 'object') -> List[Union[Component, dict, ComponentConfig]]:
131+
async def load_directory(self, directory: Union[str, Path], return_type: ReturnType = 'object') -> List[Union[Component, dict, ComponentConfig]]:
117132
"""
118133
Import all component configurations from a directory.
119134
"""
@@ -124,7 +139,7 @@ async def load_directory(self, directory: Union[str, Path], check_exists: bool =
124139
for path in list(directory.glob("*")):
125140
if path.suffix.lower().endswith(('.json', '.yaml', '.yml')):
126141
try:
127-
component = await self.load(path, return_type)
142+
component = await self.load(path, return_type=return_type)
128143
components.append(component)
129144
except Exception as e:
130145
logger.info(
@@ -176,22 +191,17 @@ async def load_termination(self, config: TerminationConfig) -> TerminationCompon
176191
raise ValueError(
177192
f"Termination condition creation failed: {str(e)}")
178193

179-
async def load_team(self, config: TeamConfig) -> TeamComponent:
194+
async def load_team(
195+
self,
196+
config: TeamConfig,
197+
input_func: Optional[Callable] = None
198+
) -> TeamComponent:
180199
"""Create team instance from configuration."""
181-
182-
default_selector_prompt = """You are in a role play game. The following roles are available:
183-
{roles}.
184-
Read the following conversation. Then select the next role from {participants} to play. Only return the role.
185-
186-
{history}
187-
188-
Read the above conversation. Then select the next role from {participants} to play. Only return the role.
189-
"""
190200
try:
191-
# Load participants (agents)
201+
# Load participants (agents) with input_func
192202
participants = []
193203
for participant in config.participants:
194-
agent = await self.load(participant)
204+
agent = await self.load(participant, input_func=input_func)
195205
participants.append(agent)
196206

197207
# Load model client if specified
@@ -202,7 +212,6 @@ async def load_team(self, config: TeamConfig) -> TeamComponent:
202212
# Load termination condition if specified
203213
termination = None
204214
if config.termination_condition:
205-
# Now we can use the universal load() method since termination is a proper component
206215
termination = await self.load(config.termination_condition)
207216

208217
# Create team based on type
@@ -215,7 +224,7 @@ async def load_team(self, config: TeamConfig) -> TeamComponent:
215224
if not model_client:
216225
raise ValueError(
217226
"SelectorGroupChat requires a model_client")
218-
selector_prompt = config.selector_prompt if config.selector_prompt else default_selector_prompt
227+
selector_prompt = config.selector_prompt if config.selector_prompt else DEFAULT_SELECTOR_PROMPT
219228
return SelectorGroupChat(
220229
participants=participants,
221230
model_client=model_client,
@@ -229,24 +238,37 @@ async def load_team(self, config: TeamConfig) -> TeamComponent:
229238
logger.error(f"Failed to create team {config.name}: {str(e)}")
230239
raise ValueError(f"Team creation failed: {str(e)}")
231240

232-
async def load_agent(self, config: AgentConfig) -> AgentComponent:
241+
async def load_agent(
242+
self,
243+
config: AgentConfig,
244+
input_func: Optional[Callable] = None
245+
) -> AgentComponent:
233246
"""Create agent instance from configuration."""
234247
try:
235248
# Load model client if specified
236249
model_client = None
237250
if config.model_client:
238251
model_client = await self.load(config.model_client)
252+
239253
system_message = config.system_message if config.system_message else "You are a helpful assistant"
254+
240255
# Load tools if specified
241256
tools = []
242257
if config.tools:
243258
for tool_config in config.tools:
244259
tool = await self.load(tool_config)
245260
tools.append(tool)
246261

247-
if config.agent_type == AgentTypes.ASSISTANT:
262+
if config.agent_type == AgentTypes.USERPROXY:
263+
return UserProxyAgent(
264+
name=config.name,
265+
description=config.description or "A human user",
266+
input_func=input_func # Pass through to UserProxyAgent
267+
)
268+
elif config.agent_type == AgentTypes.ASSISTANT:
248269
return AssistantAgent(
249270
name=config.name,
271+
description=config.description or "A helpful assistant",
250272
model_client=model_client,
251273
tools=tools,
252274
system_message=system_message

python/packages/autogen-studio/autogenstudio/database/db_manager.py

+23-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from pathlib import Path
12
import threading
23
from datetime import datetime
34
from typing import Optional
@@ -19,15 +20,34 @@ class DatabaseManager:
1920

2021
_init_lock = threading.Lock()
2122

22-
def __init__(self, engine_uri: str, auto_upgrade: bool = True):
23+
def __init__(
24+
self,
25+
engine_uri: str,
26+
base_dir: Optional[Path | str] = None,
27+
auto_upgrade: bool = True
28+
):
29+
"""
30+
Initialize DatabaseManager with optional custom base directory.
31+
32+
Args:
33+
engine_uri: Database connection URI
34+
base_dir: Custom base directory for Alembic files. If None, uses current working directory
35+
auto_upgrade: Whether to automatically upgrade schema when differences found
36+
"""
37+
# Convert string path to Path object if necessary
38+
if isinstance(base_dir, str):
39+
base_dir = Path(base_dir)
40+
2341
connection_args = {
24-
"check_same_thread": True} if "sqlite" in engine_uri else {}
42+
"check_same_thread": True
43+
} if "sqlite" in engine_uri else {}
44+
2545
self.engine = create_engine(engine_uri, connect_args=connection_args)
2646
self.schema_manager = SchemaManager(
2747
engine=self.engine,
48+
base_dir=base_dir,
2849
auto_upgrade=auto_upgrade,
2950
)
30-
3151
# Check and upgrade on startup
3252
upgraded, status = self.schema_manager.check_and_upgrade()
3353
if upgraded:

0 commit comments

Comments
 (0)