Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions samples/python/agent-framework/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,84 @@ agent-framework/
├── date_time.py # Current date/time tool
└── weather_lookup.py # OpenWeatherMap weather tools
```

---

## WorkIQ MCP Integration

This agent is extended with **WorkIQ** (Microsoft 365 / Agent 365) tools served over the **Model Context Protocol (MCP)**. The agent routes requests as follows:

| Question type | Tools used |
|---|---|
| **Weather in the United States** | Local tools — `get_current_weather`, `get_weather_forecast`, `get_date` |
| **Everything else** (M365, Teams chat, mail, files, calendar, non-US weather, general Q&A) | **WorkIQ MCP tools** |

The WorkIQ tools are discovered at runtime and merged with the local weather tools via
`McpToolRegistrationService.add_tool_servers_to_agent(...)`. If WorkIQ is not configured (or fails to
load) and `SKIP_TOOLING_ON_ERRORS=true`, the agent still answers using the local weather tools only.

### Prerequisites

| Tool | Purpose |
|---|---|
| [Agent 365 CLI (`a365`)](https://learn.microsoft.com/microsoft-365/agents-sdk/) | Manage MCP servers, tokens, and permissions |
| Microsoft 365 tenant with WorkIQ / Agent 365 enabled | Provides the MCP servers (Teams, Mail, etc.) |

The integration packages are already listed in `requirements.txt`:

```text
microsoft-agents-a365-tooling
microsoft-agents-a365-tooling-extensions-agentframework
microsoft-agents-a365-runtime
```

### Step A — Register the MCP servers

List the MCP servers available to your tenant, then add the one(s) you want. This writes a
`ToolingManifest.json` file — **do not hand-edit it**, always use the CLI.

```bash
a365 develop list-available
a365 develop add-mcp-servers "mcp_TeamsTools"
```

### Step B — Grant permissions

```bash
a365 setup permissions mcp
```

Skipping this results in `403` errors at runtime when the agent calls a WorkIQ tool.

### Step C — Configure authentication

Open your `.env` and set the WorkIQ values:

**Local development** — get a short-lived token and paste it in:

```bash
a365 develop get-token
```

```bash
BEARER_TOKEN=<token-from-a365-develop-get-token>
USE_AGENTIC_AUTH=false
SKIP_TOOLING_ON_ERRORS=true
PYTHON_ENVIRONMENT=Development
```

**Production / Teams** — use on-behalf-of (OBO) token exchange instead of a static token:

```bash
BEARER_TOKEN=
USE_AGENTIC_AUTH=true
WORKIQ_AUTH_HANDLER=<auth-handler-name-registered-in-your-AgentApplication>
SKIP_TOOLING_ON_ERRORS=true
```

### Step D — Run and test

Start the agent as usual (`python -m src.main`) and try both routes:

- *"What's the weather in Seattle?"* → answered by the local weather tools.
- *"Summarize my recent Teams chats"* → answered by the WorkIQ MCP tools.
14 changes: 14 additions & 0 deletions samples/python/agent-framework/env.TEMPLATE
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,17 @@ AZURE_OPENAI_API_KEY=
AZURE_OPENAI_MODEL=gpt-4o

OPEN_WEATHER_API_KEY=

# --- WorkIQ MCP integration ---
# Local dev: paste a token from `a365 develop get-token` (resource: mcp).
BEARER_TOKEN=
# Set to true for production / Teams. Uses OBO token exchange instead of BEARER_TOKEN.
USE_AGENTIC_AUTH=false
# Auth handler name registered in your AgentApplication (used when USE_AGENTIC_AUTH=true).
WORKIQ_AUTH_HANDLER=
# Keep answering (weather-only) if the WorkIQ tools fail to load.
SKIP_TOOLING_ON_ERRORS=true
# Tells the A365 SDK it is running locally.
PYTHON_ENVIRONMENT=Development
# Leave empty to use the production MCP platform endpoint.
MCP_PLATFORM_ENDPOINT=
3 changes: 3 additions & 0 deletions samples/python/agent-framework/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@ microsoft-agents-activity
microsoft-agents-hosting-core
microsoft-agents-hosting-aiohttp
microsoft-agents-authentication-msal
microsoft-agents-a365-tooling
microsoft-agents-a365-tooling-extensions-agentframework
microsoft-agents-a365-runtime
134 changes: 115 additions & 19 deletions samples/python/agent-framework/src/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@

logger = logging.getLogger(__name__)

# WorkIQ MCP tooling (Agent 365). Imported defensively so the agent still runs
# (weather-only) if the package or its dependencies are not installed.
try:
from microsoft_agents_a365.tooling.extensions.agentframework.services.mcp_tool_registration_service import (
McpToolRegistrationService,
)
except Exception as mcp_import_error: # pragma: no cover - optional dependency
McpToolRegistrationService = None
logger.warning("WorkIQ MCP tooling unavailable: %s", mcp_import_error)

load_dotenv()
agents_sdk_config = load_configuration_from_env(environ)

Expand All @@ -37,33 +47,117 @@
)

AGENT_INSTRUCTIONS = """
You are a friendly feline assistant that helps people find the current weather or a weather forecast for a given place.
You will always speak like a cat.
Location is a city name, 2 letter US state codes should be resolved to the full name of the United States State.
You may ask follow up questions until you have enough information to answer the customers question, but once you have the current weather or a forecast, make sure to format it nicely in text.
You are a friendly feline assistant. You always speak like a cat (use "meow", playful cat puns, and emojis when they fit).

You can help with two kinds of requests, and you must always pick the right tool for each:

For current weather, use the get_current_weather tool. You should include the current temperature, low and high temperatures, wind speed, humidity, and a short description of the weather.
For forecasts, use the get_weather_forecast tool. You should report on the next 5 days, including the current day, and include the date, high and low temperatures, and a short description of the weather.
You should use the get_date tool to get the current date and time.
1. Weather in the United States -- use your local weather tools:
- Use get_current_weather for current conditions. Include the current temperature, low and high temperatures, wind speed, humidity, and a short description of the weather.
- Use get_weather_forecast for forecasts. Report the next 5 days, including the current day, with the date, high and low temperatures, and a short description.
- Use get_date to get the current date and time.
- Location is a city name; resolve 2-letter US state codes to the full name of the United States state.

When responding, make sure to format the information in a way that is easy to read and understand, markdown is good, and always speak like a cat. Use emojis if it fits the response!
2. Anything that is NOT United States weather -- use the WorkIQ tools to answer. This includes Microsoft 365 and Microsoft Teams tasks such as reading or posting chat messages, listing chats, channels, and teams, and other workplace questions.

Routing rule: US weather questions go to the weather tools; every other question goes to the WorkIQ tools. You may ask brief follow-up questions when you need more detail. Always format answers nicely in markdown, keep them easy to read, and always speak like a cat. Use emojis if it fits the response!
"""

# Shared chat client and the local (weather) tools. These are reused both for the
# base weather agent and when WorkIQ MCP tools are attached on top of them.
CHAT_CLIENT = OpenAIChatClient(
azure_endpoint=environ.get("AZURE_OPENAI_ENDPOINT", ""),
api_key=environ.get("AZURE_OPENAI_API_KEY", ""),
model=environ.get("AZURE_OPENAI_MODEL", "gpt-4o"),
)

WEATHER_TOOLS = [get_date, get_current_weather, get_weather_forecast]

WEATHER_AGENT = Agent(
client=OpenAIChatClient(
azure_endpoint=environ.get("AZURE_OPENAI_ENDPOINT", ""),
api_key=environ.get("AZURE_OPENAI_API_KEY", ""),
model=environ.get("AZURE_OPENAI_MODEL", "gpt-4o"),
),
client=CHAT_CLIENT,
name="Purrfect Weather Agent",
instructions=AGENT_INSTRUCTIONS,
tools=[get_date, get_current_weather, get_weather_forecast],
tools=WEATHER_TOOLS,
)

# WorkIQ MCP integration state. The augmented agent (weather tools + WorkIQ MCP
# tools) is built lazily on the first message and then reused for the lifetime of
# the process. If WorkIQ is not configured or fails to load, the agent falls back
# to weather-only mode.
TOOL_SERVICE = McpToolRegistrationService() if McpToolRegistrationService else None
WORKIQ_AUTH_HANDLER = environ.get("WORKIQ_AUTH_HANDLER", "")
_workiq_agent = None
_workiq_setup_done = False


async def get_agent(context: TurnContext):
"""Return the agent to use for this turn.

Attempts once to attach the WorkIQ MCP tools to the base weather agent. On
success the augmented agent is cached and reused. When WorkIQ is unavailable
or not configured, the weather-only agent is returned instead.
"""
global _workiq_agent, _workiq_setup_done

# Already built the WorkIQ-enabled agent -> reuse it (built once, cached).
if _workiq_agent is not None:
return _workiq_agent
# Setup already attempted (and failed), or the package isn't installed -> weather-only.
if _workiq_setup_done or TOOL_SERVICE is None:
return WEATHER_AGENT

# Only try setup once; later messages take the branches above.
_workiq_setup_done = True

use_agentic_auth = environ.get("USE_AGENTIC_AUTH", "false").lower() == "true"
bearer_token = environ.get("BEARER_TOKEN", "")

# No auth configured -> nothing to call WorkIQ with, stay weather-only.
if not use_agentic_auth and not bearer_token and not WORKIQ_AUTH_HANDLER:
logger.info(
"WorkIQ not configured (no BEARER_TOKEN, USE_AGENTIC_AUTH, or "
"WORKIQ_AUTH_HANDLER) - running in weather-only mode."
)
return WEATHER_AGENT

try:
if use_agentic_auth:
# Production / Teams: the SDK exchanges an OBO token via the auth handler.
agent = await TOOL_SERVICE.add_tool_servers_to_agent(
chat_client=CHAT_CLIENT,
agent_instructions=AGENT_INSTRUCTIONS,
initial_tools=WEATHER_TOOLS,
auth=AUTHORIZATION,
auth_handler_name=WORKIQ_AUTH_HANDLER,
turn_context=context,
)
else:
# Local dev: use the bearer token from `a365 develop get-token`.
agent = await TOOL_SERVICE.add_tool_servers_to_agent(
chat_client=CHAT_CLIENT,
agent_instructions=AGENT_INSTRUCTIONS,
initial_tools=WEATHER_TOOLS,
auth=AUTHORIZATION,
auth_handler_name=WORKIQ_AUTH_HANDLER,
turn_context=context,
auth_token=bearer_token,
)
_workiq_agent = agent or WEATHER_AGENT
logger.info("WorkIQ MCP tools attached to the agent.")
except Exception as e:
# If setup fails, keep serving weather (default) instead of crashing.
if environ.get("SKIP_TOOLING_ON_ERRORS", "true").lower() == "true":
logger.error("WorkIQ MCP setup failed - running weather-only: %s", e)
_workiq_agent = WEATHER_AGENT
else:
raise

return _workiq_agent

WELCOME_MESSAGE = (
"Hello! I'm your friendly weather cat assistant. 🐱 "
"I can help you find the current weather or a weather forecast for any city. "
"Just tell me the city name and, if you're in the US, the 2-letter state code. Meow!"
"Hello! I'm your friendly purr-ductivity cat assistant. 🐱 "
"I can fetch the current weather or forecast for any US city, "
"and I can help with your Microsoft 365 work too — like Teams chats, mail, and files. "
"Ask me a weather question (city + 2-letter state) or anything about your workday. Meow!"
)


Expand All @@ -85,12 +179,14 @@ async def on_message(context: TurnContext, state: TurnState):

session_data = None
try:
agent = await get_agent(context)

session_data = state.get_value("ConversationState.agentSession", lambda: None)

if session_data is None:
session_data = WEATHER_AGENT.create_session()
session_data = agent.create_session()

async for chunk in WEATHER_AGENT.run(user_text, session=session_data, stream=True):
async for chunk in agent.run(user_text, session=session_data, stream=True):
if chunk.text:
context.streaming_response.queue_text_chunk(chunk.text)

Expand Down
Loading