Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create weatherapi toolbox from url and integrate with the team #816

Merged
merged 10 commits into from
Jul 5, 2024
2 changes: 2 additions & 0 deletions captn/captn_agents/backend/teams/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from ._gbb_initial_team import GBBInitialTeam
from ._google_ads_team import GoogleAdsTeam
from ._team import Team
from ._weather_team import WeatherTeam
from ._weekly_analysis_team import (
REACT_APP_API_URL,
WeeklyAnalysisTeam,
Expand All @@ -14,6 +15,7 @@
"CampaignCreationTeam",
"GBBInitialTeam",
"WeeklyAnalysisTeam",
"WeatherTeam",
"GoogleAdsTeam",
"REACT_APP_API_URL",
"Team",
Expand Down
4 changes: 3 additions & 1 deletion captn/captn_agents/backend/teams/_gbb_initial_team.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from ._shared_prompts import REPLY_TO_CLIENT_COMMAND
from ._team import Team

__all__ = ("GBBInitialTeam",)


@Team.register_team("gbb_initial_team")
class GBBInitialTeam(BriefCreationTeam):
Expand Down Expand Up @@ -65,7 +67,7 @@ def _guidelines(self) -> str:
If you fail to choose the appropriate team, you will be penalized!
3. Here is a list of teams you can choose from after you determine which one is the most appropriate for the task:
{self.construct_team_names_and_descriptions_message(use_only_team_names={"campaign_creation_team"})}
{self.construct_team_names_and_descriptions_message(use_only_team_names={"weather_team"})}
Guidelines SUMMARY:
- Write a detailed step-by-step plan
Expand Down
155 changes: 155 additions & 0 deletions captn/captn_agents/backend/teams/_weather_team.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
from typing import Any, Callable, Dict, List, Optional, Tuple

from fastagency.openapi.client import Client

from ..config import Config
from ..toolboxes import Toolbox
from ..tools._weather_team_tools import (
create_weather_team_client,
create_weather_team_toolbox,
)
from ._shared_prompts import REPLY_TO_CLIENT_COMMAND
from ._team import Team


@Team.register_team("weather_team")
class WeatherTeam(Team):
_default_roles = [
{
"Name": "Weather_forecaster",
"Description": """You are a weather forecaster.
Never introduce yourself when writing messages. E.g. do not write 'As a ...'""",
},
{
"Name": "News_reporter",
"Description": """You are a news reporter.
You are also SOLELY responsible for communicating with the client.
Based on the initial task, a number of proposed solutions will be suggested by the team. You must ask the team to write a detailed plan
including steps and expected outcomes.
Once the initial task given to the team is completed by implementing proposed solutions, you must write down the
accomplished work and execute the 'reply_to_client' command. That message will be forwarded to the client so make
sure it is understandable by non-experts.
Never introduce yourself when writing messages. E.g. do not write 'As an account manager'
""",
},
]

_functions: Optional[List[Dict[str, Any]]] = []

def __init__(
self,
*,
task: str,
user_id: int,
conv_id: int,
work_dir: str = "weather_team",
max_round: int = 80,
seed: int = 42,
temperature: float = 0.2,
config_list: Optional[List[Dict[str, str]]] = None,
create_toolbox_func: Callable[
[int, int], Toolbox
] = create_weather_team_toolbox,
create_client_func: Callable[[str], Client] = create_weather_team_client,
openapi_url: str = "https://weather.tools.fastagency.ai/openapi.json",
):
recommended_modifications_and_answer_list: List[
Tuple[Dict[str, Any], Optional[str]]
] = []
function_map: Dict[str, Callable[[Any], Any]] = {}

roles: List[Dict[str, str]] = self._default_roles

super().__init__(
user_id=user_id,
conv_id=conv_id,
roles=roles,
task=task,
function_map=function_map,
work_dir=work_dir,
max_round=max_round,
seed=seed,
temperature=temperature,
recommended_modifications_and_answer_list=recommended_modifications_and_answer_list,
use_user_proxy=True,
)

if config_list is None:
config = Config()
config_list = config.config_list_gpt_4o

self.llm_config = self._get_llm_config(
seed=seed, temperature=temperature, config_list=config_list
)
self.create_toolbox_func = create_toolbox_func
self.create_client_func = create_client_func
self.openapi_url = openapi_url

self._create_members()

self._add_client()
self._add_tools()

self._create_initial_message()

def _add_client(self) -> None:
self.client = self.create_client_func(self.openapi_url)

self.client.register_for_execution(self.user_proxy)
for agent in self.members:
# Add only for Weather_forecaster
if agent.name == self._default_roles[0]["Name"].lower():
if agent.llm_config["tools"] is None:
agent.llm_config.pop("tools")
self.client.register_for_llm(agent)

def _add_tools(self) -> None:
self.toolbox = self.create_toolbox_func(
self.user_id,
self.conv_id,
)
# Add only for News_reporter
for agent in self.members:
if (
agent != self.user_proxy
and agent.name != self._default_roles[0]["Name"].lower()
):
self.toolbox.add_to_agent(agent, self.user_proxy)

@property
def _task(self) -> str:
return f"""You are a team in charge of weather forecasting.
Here is the current customers brief/information we have gathered for you as a starting point:
{self.task}
"""

@property
def _guidelines(self) -> str:
return """### Guidelines
1. Do NOT repeat the content of the previous messages nor repeat your role.
"""

@property
def _commands(self) -> str:
return f"""## Commands
Only News_reporter has access to the following command:
1. {REPLY_TO_CLIENT_COMMAND}
"smart_suggestions": {{
'suggestions': ['Give me suggestions what I can do based on the current weather forecast.'],
'type': 'oneOf'
}}
2. Only Weather_forecaster has access to weather API to get the weather forecast for the city.
"""

@classmethod
def get_capabilities(cls) -> str:
return "Weather forecasting for any city."

@classmethod
def get_brief_template(cls) -> str:
return (
"We only need the name of the city for which you want the weather forecast."
)
38 changes: 38 additions & 0 deletions captn/captn_agents/backend/tools/_weather_team_tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import httpx
from fastagency.openapi.client import Client

from ..toolboxes import Toolbox
from ._functions import REPLY_TO_CLIENT_DESCRIPTION, BaseContext, reply_to_client

__all__ = (
"create_weather_team_client",
"create_weather_team_toolbox",
)


def create_weather_team_client(openapi_url: str) -> Client:
with httpx.Client() as httpx_client:
response = httpx_client.get(openapi_url)
response.raise_for_status()
openapi_spec = response.text

client = Client.create(openapi_spec)

return client


def create_weather_team_toolbox(
user_id: int,
conv_id: int,
) -> Toolbox:
toolbox = Toolbox()

context = BaseContext(
user_id=user_id,
conv_id=conv_id,
)
toolbox.set_context(context)

toolbox.add_function(REPLY_TO_CLIENT_DESCRIPTION)(reply_to_client)

return toolbox
6 changes: 4 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,13 @@ testing = [
]

benchmarking = [
"typer==0.12.3",
"typer==0.9.4", # downgraded because fastagency
"filelock==3.15.4",
"tabulate==0.9.0",
]

agents = [
"fastapi==0.111.0",
"fastapi>=0.110.2,<0.111.0", # downgraded because fastagency
"APScheduler==3.10.4",
"prisma==0.13.1",
"google-ads==24.1.0",
Expand All @@ -111,6 +111,8 @@ agents = [
"opentelemetry-exporter-otlp==1.25.0",
"openpyxl==3.1.5",
"aiofiles==24.1.0",
"fastagency@git+https://github.com/airtai/fastagency.git@3a2346c",
"python-multipart==0.0.9", # remove after fastagency is updated
]

dev = [
Expand Down
18 changes: 12 additions & 6 deletions tests/ci/captn/captn_agents/backend/teams/helpers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Type
from typing import Dict, Type

from autogen.agentchat import UserProxyAgent

Expand All @@ -7,23 +7,28 @@

def helper_test_init(
team: Team,
number_of_team_members: int,
number_of_functions: int,
number_of_registered_executions: int,
agent_number_of_functions_dict: Dict[str, int],
team_class: Type[Team],
) -> None:
try:
assert isinstance(team, team_class)

assert len(team.members) == number_of_team_members
assert len(team.members) == len(agent_number_of_functions_dict)

for agent in team.members:
# execution of the tools
number_of_functions_in_function_map = len(agent.function_map)
if isinstance(agent, UserProxyAgent):
assert number_of_functions_in_function_map == number_of_functions
print(agent.function_map)
assert (
number_of_functions_in_function_map
== number_of_registered_executions
)
else:
assert number_of_functions_in_function_map == 0

number_of_functions = agent_number_of_functions_dict[agent.name]
# specification of the tools
llm_config = agent.llm_config
if not isinstance(agent, UserProxyAgent):
Expand All @@ -36,7 +41,8 @@ def helper_test_init(
tool["function"]["name"] for tool in llm_config["tools"]
]

assert set(team.user_proxy.function_map.keys()) == set(function_names)
# Check if the agent's function names are in the user_proxy's function map
assert set(function_names) <= set(team.user_proxy.function_map.keys())
else:
assert llm_config is False

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,16 @@ def test_init(self) -> None:
task="do your magic",
)

agent_number_of_functions_dict = {
"digitial_marketing_strategist": 4,
"account_manager": 4,
"user_proxy": 0,
}

helper_test_init(
team=brief_creation_team,
number_of_team_members=3,
number_of_functions=4,
number_of_registered_executions=4,
agent_number_of_functions_dict=agent_number_of_functions_dict,
team_class=BriefCreationTeam,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,16 @@ def test_init(self) -> None:
task="do your magic",
)

agent_number_of_functions_dict = {
"copywriter": 7,
"account_manager": 7,
"user_proxy": 0,
}

helper_test_init(
team=campaign_creation_team,
number_of_team_members=3,
number_of_functions=7,
number_of_registered_executions=7,
agent_number_of_functions_dict=agent_number_of_functions_dict,
team_class=CampaignCreationTeam,
)

Expand Down
14 changes: 10 additions & 4 deletions tests/ci/captn/captn_agents/backend/teams/test_gbb_initial_team.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,21 @@ def setup(self) -> Iterator[None]:
yield

def test_init(self) -> None:
brief_creation_team = GBBInitialTeam(
gbb_initial_team = GBBInitialTeam(
user_id=123,
conv_id=456,
task="do your magic",
)

agent_number_of_functions_dict = {
"digitial_marketing_strategist": 3,
"account_manager": 3,
"user_proxy": 0,
}

helper_test_init(
team=brief_creation_team,
number_of_team_members=3,
number_of_functions=3,
team=gbb_initial_team,
number_of_registered_executions=3,
agent_number_of_functions_dict=agent_number_of_functions_dict,
team_class=GBBInitialTeam,
)
12 changes: 10 additions & 2 deletions tests/ci/captn/captn_agents/backend/teams/test_google_ads_team.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,17 @@ def test_init(self) -> None:
task="do your magic",
)

agent_number_of_functions_dict = {
"google_ads_specialist": 21,
"copywriter": 21,
"digital_strategist": 21,
"account_manager": 21,
"user_proxy": 0,
}

helper_test_init(
team=google_ads_team,
number_of_team_members=5,
number_of_functions=21,
number_of_registered_executions=21,
agent_number_of_functions_dict=agent_number_of_functions_dict,
team_class=GoogleAdsTeam,
)
Loading