From 1a34bc8ce92832f668e4a95f804d745155e50171 Mon Sep 17 00:00:00 2001 From: Axel Suarez Martinez Date: Tue, 25 Mar 2025 01:01:24 -0700 Subject: [PATCH 1/2] WIP Weather openAI --- .../agents/builder/message_factory.py | 3 +- test_samples/weather-agent-open-ai/app.py | 71 ++++++++++++++++ test_samples/weather-agent-open-ai/config.py | 18 ++++ .../tools/date_time_tool.py | 0 .../tools/get_weather_tool.py | 15 ++++ .../weather-agent-open-ai/weather_agent.py | 82 +++++++++++++++++++ 6 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 test_samples/weather-agent-open-ai/app.py create mode 100644 test_samples/weather-agent-open-ai/config.py create mode 100644 test_samples/weather-agent-open-ai/tools/date_time_tool.py create mode 100644 test_samples/weather-agent-open-ai/tools/get_weather_tool.py create mode 100644 test_samples/weather-agent-open-ai/weather_agent.py diff --git a/libraries/Builder/microsoft-agents-builder/microsoft/agents/builder/message_factory.py b/libraries/Builder/microsoft-agents-builder/microsoft/agents/builder/message_factory.py index 423a8dcf..bfafa491 100644 --- a/libraries/Builder/microsoft-agents-builder/microsoft/agents/builder/message_factory.py +++ b/libraries/Builder/microsoft-agents-builder/microsoft/agents/builder/message_factory.py @@ -25,12 +25,13 @@ def attachment_activity( type=ActivityTypes.message, attachment_layout=attachment_layout, attachments=attachments, - input_hint=input_hint, ) if text: message.text = text if speak: message.speak = speak + if input_hint: + message.input_hint = input_hint return message diff --git a/test_samples/weather-agent-open-ai/app.py b/test_samples/weather-agent-open-ai/app.py new file mode 100644 index 00000000..cb243d0a --- /dev/null +++ b/test_samples/weather-agent-open-ai/app.py @@ -0,0 +1,71 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os +from agents import set_tracing_export_api_key +from dotenv import load_dotenv +from aiohttp.web import Application, Request, Response, run_app + +from microsoft.agents.builder import RestChannelServiceClientFactory +from microsoft.agents.hosting.aiohttp import CloudAdapter, jwt_authorization_middleware +from microsoft.agents.authorization import ( + Connections, + AccessTokenProviderBase, + ClaimsIdentity, +) +from microsoft.agents.authentication.msal import MsalAuth +from openai import AsyncAzureOpenAI + +from weather_agent import WeatherAgent +from config import DefaultConfig + +load_dotenv() + +CONFIG = DefaultConfig() +AUTH_PROVIDER = MsalAuth(DefaultConfig()) + + +class DefaultConnection(Connections): + def get_default_connection(self) -> AccessTokenProviderBase: + pass + + def get_token_provider( + self, claims_identity: ClaimsIdentity, service_url: str + ) -> AccessTokenProviderBase: + return AUTH_PROVIDER + + def get_connection(self, connection_name: str) -> AccessTokenProviderBase: + pass + + +CHANNEL_CLIENT_FACTORY = RestChannelServiceClientFactory(CONFIG, DefaultConnection()) + +# Create adapter. +ADAPTER = CloudAdapter(CHANNEL_CLIENT_FACTORY) + +# gets the API Key from environment variable AZURE_OPENAI_API_KEY +CLIENT = AsyncAzureOpenAI( + api_version=CONFIG.AZURE_OPENAI_API_VERSION, + azure_endpoint=CONFIG.AZURE_OPENAI_ENDPOINT, +) + +# Create the Agent +AGENT = WeatherAgent(client=CLIENT) + + +# Listen for incoming requests on /api/messages +async def messages(req: Request) -> Response: + adapter: CloudAdapter = req.app["adapter"] + return await adapter.process(req, AGENT) + + +APP = Application(middlewares=[jwt_authorization_middleware]) +APP.router.add_post("/api/messages", messages) +APP["agent_configuration"] = CONFIG +APP["adapter"] = ADAPTER + +if __name__ == "__main__": + try: + run_app(APP, host="localhost", port=CONFIG.PORT) + except Exception as error: + raise error diff --git a/test_samples/weather-agent-open-ai/config.py b/test_samples/weather-agent-open-ai/config.py new file mode 100644 index 00000000..db0afcd9 --- /dev/null +++ b/test_samples/weather-agent-open-ai/config.py @@ -0,0 +1,18 @@ +from os import environ +from microsoft.agents.authentication.msal import AuthTypes, MsalAuthConfiguration + + +class DefaultConfig(MsalAuthConfiguration): + """Agent Configuration""" + + def __init__(self) -> None: + self.AUTH_TYPE = AuthTypes.client_secret + self.TENANT_ID = "" or environ.get("TENANT_ID") + self.CLIENT_ID = "" or environ.get("CLIENT_ID") + self.CLIENT_SECRET = "" or environ.get("CLIENT_SECRET") + self.AZURE_OPENAI_API_KEY = "" or environ.get("AZURE_OPENAI_API_KEY") + self.AZURE_OPENAI_ENDPOINT = "" or environ.get("AZURE_OPENAI_ENDPOINT") + self.AZURE_OPENAI_API_VERSION = "" or environ.get( + "AZURE_OPENAI_API_VERSION", "2024-06-01" + ) + self.PORT = 3978 diff --git a/test_samples/weather-agent-open-ai/tools/date_time_tool.py b/test_samples/weather-agent-open-ai/tools/date_time_tool.py new file mode 100644 index 00000000..e69de29b diff --git a/test_samples/weather-agent-open-ai/tools/get_weather_tool.py b/test_samples/weather-agent-open-ai/tools/get_weather_tool.py new file mode 100644 index 00000000..07955580 --- /dev/null +++ b/test_samples/weather-agent-open-ai/tools/get_weather_tool.py @@ -0,0 +1,15 @@ +from pydantic import BaseModel + +from agents import function_tool + + +class Weather(BaseModel): + city: str + temperature_range: str + conditions: str + + +@function_tool +def get_weather(city: str) -> Weather: + print("[debug] get_weather called") + return Weather(city=city, temperature_range="14-20C", conditions="Sunny with wind.") diff --git a/test_samples/weather-agent-open-ai/weather_agent.py b/test_samples/weather-agent-open-ai/weather_agent.py new file mode 100644 index 00000000..288de2ef --- /dev/null +++ b/test_samples/weather-agent-open-ai/weather_agent.py @@ -0,0 +1,82 @@ +from microsoft.agents.builder import ActivityHandler, MessageFactory, TurnContext +from microsoft.agents.core.models import ChannelAccount, Attachment + +from agents import ( + Agent as OpenAIAgent, + Model, + ModelProvider, + OpenAIChatCompletionsModel, + RunConfig, + Runner, + trace, +) +from openai import AsyncAzureOpenAI +from pydantic import BaseModel, Field + +from tools.get_weather_tool import get_weather + + +class WeatherForecastAgentResponse(BaseModel): + contentType: str = Field(pattern=r"^(Text|AdaptiveCard)$") + content: dict + + +class WeatherAgent(ActivityHandler): + def __init__(self, client: AsyncAzureOpenAI): + self.agent = OpenAIAgent( + name="WeatherAgent", + instructions="""" + You are a friendly assistant that helps people find a weather forecast for a given place. + You may ask follow up questions until you have enough information to answer the customers question, + but once you have a forecast forecast, make sure to format it nicely using an adaptive card. + + Always respond in JSON format with the following JSON schema, and do not use markdown in the response: + + { + "contentType": "'Text' if you don't have a forecast or 'AdaptiveCard' if you do", + "content": "{The content of the response, may be plain text, or JSON based adaptive card}" + } + """, + tools=[get_weather], + ) + + class CustomModelProvider(ModelProvider): + def get_model(self, model_name: str | None) -> Model: + return OpenAIChatCompletionsModel( + model=model_name or "gpt-4o", openai_client=client + ) + + self.custom_model_provider = CustomModelProvider() + + async def on_members_added_activity( + self, members_added: list[ChannelAccount], turn_context: TurnContext + ): + for member in members_added: + if member.id != turn_context.activity.recipient.id: + await turn_context.send_activity("Hello and welcome!") + + async def on_message_activity(self, turn_context: TurnContext): + with trace("Get Weather", group_id=turn_context.activity.conversation.id): + response = await Runner.run( + self.agent, + turn_context.activity.text, + run_config=RunConfig( + model_provider=self.custom_model_provider, + group_id=turn_context.activity.conversation.id, + ), + ) + + llm_response = WeatherForecastAgentResponse.model_validate_json( + response.final_output + ) + if llm_response.contentType == "AdaptiveCard": + activity = MessageFactory.attachment( + Attachment( + content_type="application/vnd.microsoft.card.adaptive", + content=llm_response.content, + ) + ) + elif llm_response.contentType == "Text": + activity = MessageFactory.text(llm_response.content) + + return await turn_context.send_activity(activity) From 5e23146896a4560f978e57453d6da05ea0904786 Mon Sep 17 00:00:00 2001 From: Axel Suarez Date: Tue, 25 Mar 2025 14:44:20 -0700 Subject: [PATCH 2/2] Single-turn weather openai working --- .../weather-agent-open-ai/requirements.txt | 2 ++ .../tools/date_time_tool.py | 10 ++++++ .../tools/get_weather_tool.py | 14 ++++++-- .../weather-agent-open-ai/weather_agent.py | 36 +++++++++---------- 4 files changed, 41 insertions(+), 21 deletions(-) create mode 100644 test_samples/weather-agent-open-ai/requirements.txt diff --git a/test_samples/weather-agent-open-ai/requirements.txt b/test_samples/weather-agent-open-ai/requirements.txt new file mode 100644 index 00000000..11a0a416 --- /dev/null +++ b/test_samples/weather-agent-open-ai/requirements.txt @@ -0,0 +1,2 @@ +openai +openai-agents \ No newline at end of file diff --git a/test_samples/weather-agent-open-ai/tools/date_time_tool.py b/test_samples/weather-agent-open-ai/tools/date_time_tool.py index e69de29b..22986b30 100644 --- a/test_samples/weather-agent-open-ai/tools/date_time_tool.py +++ b/test_samples/weather-agent-open-ai/tools/date_time_tool.py @@ -0,0 +1,10 @@ +from agents import function_tool +from datetime import datetime + + +@function_tool +def get_date() -> str: + """ + A function tool that returns the current date and time. + """ + return datetime.now().isoformat() diff --git a/test_samples/weather-agent-open-ai/tools/get_weather_tool.py b/test_samples/weather-agent-open-ai/tools/get_weather_tool.py index 07955580..1caff893 100644 --- a/test_samples/weather-agent-open-ai/tools/get_weather_tool.py +++ b/test_samples/weather-agent-open-ai/tools/get_weather_tool.py @@ -1,3 +1,4 @@ +import random from pydantic import BaseModel from agents import function_tool @@ -5,11 +6,18 @@ class Weather(BaseModel): city: str - temperature_range: str + temperature: str conditions: str + date: str @function_tool -def get_weather(city: str) -> Weather: +def get_weather(city: str, date: str) -> Weather: print("[debug] get_weather called") - return Weather(city=city, temperature_range="14-20C", conditions="Sunny with wind.") + temperature = random.randint(8, 21) + return Weather( + city=city, + temperature=f"{temperature}C", + conditions="Sunny with wind.", + date=date, + ) diff --git a/test_samples/weather-agent-open-ai/weather_agent.py b/test_samples/weather-agent-open-ai/weather_agent.py index 288de2ef..9c20156e 100644 --- a/test_samples/weather-agent-open-ai/weather_agent.py +++ b/test_samples/weather-agent-open-ai/weather_agent.py @@ -1,3 +1,6 @@ +from __future__ import annotations + +from typing import Union from microsoft.agents.builder import ActivityHandler, MessageFactory, TurnContext from microsoft.agents.core.models import ChannelAccount, Attachment @@ -8,17 +11,17 @@ OpenAIChatCompletionsModel, RunConfig, Runner, - trace, ) from openai import AsyncAzureOpenAI from pydantic import BaseModel, Field from tools.get_weather_tool import get_weather +from tools.date_time_tool import get_date class WeatherForecastAgentResponse(BaseModel): contentType: str = Field(pattern=r"^(Text|AdaptiveCard)$") - content: dict + content: Union[dict, str] class WeatherAgent(ActivityHandler): @@ -26,18 +29,16 @@ def __init__(self, client: AsyncAzureOpenAI): self.agent = OpenAIAgent( name="WeatherAgent", instructions="""" - You are a friendly assistant that helps people find a weather forecast for a given place. - You may ask follow up questions until you have enough information to answer the customers question, - but once you have a forecast forecast, make sure to format it nicely using an adaptive card. - - Always respond in JSON format with the following JSON schema, and do not use markdown in the response: - + You are a friendly assistant that helps people find a weather forecast for a given time and place. + Do not reply with MD format nor plain text. You can ONLY respond in JSON format with the following JSON schema { "contentType": "'Text' if you don't have a forecast or 'AdaptiveCard' if you do", "content": "{The content of the response, may be plain text, or JSON based adaptive card}" } + You may ask follow up questions until you have enough information to answer the customers question, + but once you have a forecast forecast, make sure to format it nicely using an adaptive card. """, - tools=[get_weather], + tools=[get_weather, get_date], ) class CustomModelProvider(ModelProvider): @@ -56,15 +57,14 @@ async def on_members_added_activity( await turn_context.send_activity("Hello and welcome!") async def on_message_activity(self, turn_context: TurnContext): - with trace("Get Weather", group_id=turn_context.activity.conversation.id): - response = await Runner.run( - self.agent, - turn_context.activity.text, - run_config=RunConfig( - model_provider=self.custom_model_provider, - group_id=turn_context.activity.conversation.id, - ), - ) + response = await Runner.run( + self.agent, + turn_context.activity.text, + run_config=RunConfig( + model_provider=self.custom_model_provider, + tracing_disabled=True, + ), + ) llm_response = WeatherForecastAgentResponse.model_validate_json( response.final_output