Skip to content

feat: add M365 AgentsSDK A2A patterns Python sample#474

Open
sgshaji wants to merge 1 commit intomicrosoft:mainfrom
sgshaji:samples/python/m365-agents-sdk-a2a-patterns
Open

feat: add M365 AgentsSDK A2A patterns Python sample#474
sgshaji wants to merge 1 commit intomicrosoft:mainfrom
sgshaji:samples/python/m365-agents-sdk-a2a-patterns

Conversation

@sgshaji
Copy link

@sgshaji sgshaji commented Mar 8, 2026

Summary

This PR adds a new Python sample — M365 AgentsSDK A2A Patterns — demonstrating how to build intelligent agents using the M365 Agents SDK with Agent-to-Agent (A2A) protocol integration.


What's Included

Track 1 — Intelligent Agent Orchestration with Semantic Kernel

  • AzureChatCompletion wired into a ChatCompletionAgent inside the M365 Agents SDK hosting model
  • Domain functions exposed as @kernel_function tools
  • Graceful fallback to regex-based routing when Azure OpenAI is unavailable

Track 2 — Agent-to-Agent Communication via A2A Protocol

  • Full A2A client lifecycle: discovery, message/send, message/stream, push notification registration
  • Webhook receiver for async push callbacks
  • Paired with a Google ADK agent (A2A producer) backed by BigQuery + SerpAPI

Track 3 — A2A Transmission Patterns (Ping / Stream / Push)

  • Ping (message/send): blocking request/response
  • Stream (message/stream): Server-Sent Events for real-time results
  • Push (message/send + webhook): fire-and-forget with async callback
  • Test CLIs to exercise each pattern independently (with and without an LLM)

Sample Structure

samples/python/m365-agents-sdk-a2a-patterns/
├── a2a-client-agent/ # M365 Agents SDK + Semantic Kernel
├── adk-agent/ # Google ADK + Gemini 2.0 Flash (A2A producer)
└── docs/ # Architecture diagrams and A2A pattern deep-dives


Notes

  • Uses microsoft_agents import structure (underscore) per current SDK convention
  • No build step required; Python dependencies via pip install -r requirements.txt
  • Does not affect the existing cd-samples CI workflow

@github-actions github-actions bot added Samples Changes to Samples From Fork labels Mar 8, 2026
@sgshaji
Copy link
Author

sgshaji commented Mar 8, 2026

@microsoft-github-policy-service agree company="Microsoft"

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new end-to-end Python sample under samples/python/m365-agents-sdk-a2a-patterns/ demonstrating M365 Agents SDK agent orchestration with Semantic Kernel, plus A2A protocol integrations (ping/send, SSE streaming, push/webhook) against a Google ADK “producer” agent.

Changes:

  • Introduces a Google ADK A2A producer agent (BigQuery + SerpAPI + optional Selenium scraping) with scripts, eval datasets, and containerization.
  • Adds an M365 Agents SDK “consumer” agent that uses Semantic Kernel for tool-driven orchestration and provides CLIs to exercise ping/stream/push patterns.
  • Adds documentation and sample scaffolding (docs, env templates, ignore rules) and links the sample from samples/python/README.md.

Reviewed changes

Copilot reviewed 51 out of 51 changed files in this pull request and generated 19 comments.

Show a summary per file
File Description
samples/python/m365-agents-sdk-a2a-patterns/env.example Environment variable template for the sample.
samples/python/m365-agents-sdk-a2a-patterns/docs/ARCHITECTURE.md Architecture write-up and component diagrams.
samples/python/m365-agents-sdk-a2a-patterns/docs/A2A_PATTERNS.md Protocol/pattern deep dive with JSON-RPC and sequence diagrams.
samples/python/m365-agents-sdk-a2a-patterns/adk-agent/tests/unit/test_tools.py Adds unit test(s) for ADK tool layer.
samples/python/m365-agents-sdk-a2a-patterns/adk-agent/run_a2a.py ADK A2A server entrypoint (Starlette + A2A handler + push).
samples/python/m365-agents-sdk-a2a-patterns/adk-agent/pyproject.toml ADK agent dependencies and dev tooling config.
samples/python/m365-agents-sdk-a2a-patterns/adk-agent/eval/test_eval.py ADK evaluation runner using ADK evaluator.
samples/python/m365-agents-sdk-a2a-patterns/adk-agent/eval/data/test_config.json Evaluation scoring configuration.
samples/python/m365-agents-sdk-a2a-patterns/adk-agent/eval/data/eval_data1.evalset.json Evaluation dataset for multi-turn/tool-trajectory behavior.
samples/python/m365-agents-sdk-a2a-patterns/adk-agent/eval/init.py Eval package initializer.
samples/python/m365-agents-sdk-a2a-patterns/adk-agent/deployment/test_deployment.py Script to test deployed agent on Vertex AI Agent Engine.
samples/python/m365-agents-sdk-a2a-patterns/adk-agent/deployment/test.sh Local unit test runner script.
samples/python/m365-agents-sdk-a2a-patterns/adk-agent/deployment/run.sh Local setup script (install + BigQuery populate).
samples/python/m365-agents-sdk-a2a-patterns/adk-agent/deployment/eval.sh Shell wrapper to run ADK eval.
samples/python/m365-agents-sdk-a2a-patterns/adk-agent/deployment/deploy.py Vertex AI Agent Engine deployment helper.
samples/python/m365-agents-sdk-a2a-patterns/adk-agent/deployment/bq_populate_data.py Helper to create/populate BigQuery dataset/table with sample rows.
samples/python/m365-agents-sdk-a2a-patterns/adk-agent/deployment/bq_data_setup.sql SQL alternative for BigQuery table setup.
samples/python/m365-agents-sdk-a2a-patterns/adk-agent/deployment/init.py Deployment package initializer.
samples/python/m365-agents-sdk-a2a-patterns/adk-agent/brand_search_optimization/tools/serp_connector.py SerpAPI-based competitor product retrieval.
samples/python/m365-agents-sdk-a2a-patterns/adk-agent/brand_search_optimization/tools/bq_connector.py BigQuery tools for categories + product lookup.
samples/python/m365-agents-sdk-a2a-patterns/adk-agent/brand_search_optimization/sub_agents/search_results/prompt.py Prompt for web/search-results sub-agent.
samples/python/m365-agents-sdk-a2a-patterns/adk-agent/brand_search_optimization/sub_agents/search_results/agent.py Search-results agent tools (SerpAPI + scraping fallback) and Selenium helpers.
samples/python/m365-agents-sdk-a2a-patterns/adk-agent/brand_search_optimization/sub_agents/keyword_finding/prompt.py Prompt for keyword-finding sub-agent.
samples/python/m365-agents-sdk-a2a-patterns/adk-agent/brand_search_optimization/sub_agents/keyword_finding/agent.py Keyword-finding agent wiring.
samples/python/m365-agents-sdk-a2a-patterns/adk-agent/brand_search_optimization/sub_agents/comparison/prompt.py Prompts for comparison generator/critic/root.
samples/python/m365-agents-sdk-a2a-patterns/adk-agent/brand_search_optimization/sub_agents/comparison/agent.py Comparison sub-agent wiring.
samples/python/m365-agents-sdk-a2a-patterns/adk-agent/brand_search_optimization/sub_agents/init.py Sub-agents package initializer.
samples/python/m365-agents-sdk-a2a-patterns/adk-agent/brand_search_optimization/shared_libraries/constants.py Shared configuration/env constants for ADK agent.
samples/python/m365-agents-sdk-a2a-patterns/adk-agent/brand_search_optimization/prompt.py Root agent orchestration prompt.
samples/python/m365-agents-sdk-a2a-patterns/adk-agent/brand_search_optimization/agent.py Root ADK agent wiring and tool exposure.
samples/python/m365-agents-sdk-a2a-patterns/adk-agent/brand_search_optimization/init.py Package initializer.
samples/python/m365-agents-sdk-a2a-patterns/adk-agent/README.md ADK agent setup/run/deploy docs.
samples/python/m365-agents-sdk-a2a-patterns/adk-agent/Dockerfile Container image for ADK A2A server mode.
samples/python/m365-agents-sdk-a2a-patterns/a2a-client-agent/test_demo.py Interactive demo CLI for ping/stream/push experiences.
samples/python/m365-agents-sdk-a2a-patterns/a2a-client-agent/run_server.py Entry point for M365 agent aiohttp server.
samples/python/m365-agents-sdk-a2a-patterns/a2a-client-agent/requirements.txt Client agent Python dependencies.
samples/python/m365-agents-sdk-a2a-patterns/a2a-client-agent/env.TEMPLATE Client agent environment template.
samples/python/m365-agents-sdk-a2a-patterns/a2a-client-agent/cli_test.py Direct A2A protocol CLI (discover/ping/stream/push/status).
samples/python/m365-agents-sdk-a2a-patterns/a2a-client-agent/brand_intelligence_advisor/tools/brand_advisor.py Local parsing/domain knowledge + formatting utilities.
samples/python/m365-agents-sdk-a2a-patterns/a2a-client-agent/brand_intelligence_advisor/tools/a2a_client.py Async A2A client implementation (send/stream/push).
samples/python/m365-agents-sdk-a2a-patterns/a2a-client-agent/brand_intelligence_advisor/tools/init.py Tools package docs.
samples/python/m365-agents-sdk-a2a-patterns/a2a-client-agent/brand_intelligence_advisor/server.py aiohttp server with M365 /api/messages + webhook endpoints.
samples/python/m365-agents-sdk-a2a-patterns/a2a-client-agent/brand_intelligence_advisor/prompt.py System prompt for Semantic Kernel orchestration.
samples/python/m365-agents-sdk-a2a-patterns/a2a-client-agent/brand_intelligence_advisor/orchestrator.py Semantic Kernel ChatCompletionAgent + tool plugin.
samples/python/m365-agents-sdk-a2a-patterns/a2a-client-agent/brand_intelligence_advisor/agent.py M365 Agents SDK agent handlers + LLM/regex fallback routing.
samples/python/m365-agents-sdk-a2a-patterns/a2a-client-agent/brand_intelligence_advisor/init.py Package docs.
samples/python/m365-agents-sdk-a2a-patterns/a2a-client-agent/README.md Client agent setup and explanation.
samples/python/m365-agents-sdk-a2a-patterns/README.md Root sample README tying together all tracks/patterns.
samples/python/m365-agents-sdk-a2a-patterns/LICENSE Sample-level MIT license.
samples/python/m365-agents-sdk-a2a-patterns/.gitignore Sample-local ignore rules for env/log/artifacts.
samples/python/README.md Adds the new sample to the Python samples index.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +133 to +145
def click_at_coordinates(x: int, y: int) -> str:
"""Clicks at the specified coordinates on the screen."""
driver.execute_script(f"window.scrollTo({x}, {y});")
driver.find_element(By.TAG_NAME, "body").click()


def find_element_with_text(text: str) -> str:
"""Finds an element on the page with the given text."""
print(f"🔍 Finding element with text: '{text}'") # Added print statement

try:
element = driver.find_element(By.XPATH, f"//*[text()='{text}']")
if element:
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Several Selenium tools (click_at_coordinates, find_element_with_text, etc.) use the module-global driver without calling _ensure_driver() or checking for None. If go_to_url hasn’t been called (or driver init fails / is disabled), these tools will raise. Also click_at_coordinates doesn’t return a str on success, despite its annotation and being registered as a tool.

Copilot uses AI. Check for mistakes.
Comment on lines +17 to +35
SEARCH_RESULT_AGENT_PROMPT = """
You are an autonomous web search agent that extracts competitor product data.

<Process>
1. When you receive a request like "Find competing products for moisture management socks", extract the keyword
- The keyword is everything after "for" (e.g., "moisture wicking socks")

2. IMMEDIATELY call extract_google_shopping_products(keyword="[extracted_keyword]")
- Pass the keyword as a parameter
- Example: extract_google_shopping_products(keyword="moisture wicking socks")
- DO NOT ask for confirmation
- DO NOT say "I will search"
- JUST call the tool right away
- This tool automatically tries multiple sources:
* Amazon (primary - most stable)
* Google Shopping (fallback)
- Returns real product titles and prices from whichever source succeeds
- Always gets real data - no mock results

Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The search-results agent prompt is internally inconsistent: step numbering skips from 2 → 4, and it claims the tool’s primary source is Amazon with Google Shopping fallback, but the implementation now tries SerpAPI first (then Amazon, then Google Shopping). This can lead the LLM to describe the wrong behavior or make incorrect assumptions during execution.

Copilot uses AI. Check for mistakes.
Comment on lines +25 to +65
class TestBrandSearchOptimization:
@patch("brand_search_optimization.tools.bq_connector.client")
def test_get_product_details_for_brand_success(self, mock_client):
# Mock ToolContext
mock_tool_context = MagicMock(spec=ToolContext)
mock_tool_context.user_content.parts = [MagicMock(text="cymbal")]

# Mock BigQuery results
mock_row1 = MagicMock(
title="cymbal Air Max",
description="Comfortable running shoes",
attribute="Size: 10, Color: Blue",
brand="cymbal",
)
mock_row2 = MagicMock(
title="cymbal Sportswear T-Shirt",
description="Cotton blend, short sleeve",
attribute="Size: L, Color: Black",
brand="cymbal",
)
mock_row3 = MagicMock(
title="neuravibe Pro Training Shorts",
description="Moisture-wicking fabric",
attribute="Size: M, Color: Gray",
brand="neuravibe",
)
mock_results = [mock_row1, mock_row2, mock_row3]

# Mock QueryJob and its result
mock_query_job = MagicMock()
mock_query_job.result.return_value = mock_results
mock_client.query.return_value = mock_query_job

# Mock constants
with patch.object(constants, "PROJECT", "test_project"):
with patch.object(constants, "TABLE_ID", "test_table"):
# Call the function
markdown_output = bq_connector.get_product_details_for_brand(
mock_tool_context
)
assert "neuravibe Pro" not in markdown_output
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This unit test doesn’t match the implementation it targets: bq_connector.get_product_details_for_brand expects (brand: str, category: str | None), but the test passes a ToolContext mock. It also patches constants.TABLE_ID, which doesn’t exist in shared_libraries/constants.py. As written, this test will fail at runtime and won’t validate the intended behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +52 to +71
bucket = (
FLAGS.bucket
if FLAGS.bucket
else os.getenv("GOOGLE_CLOUD_STORAGE_BUCKET")
)

project_id = os.getenv("GOOGLE_CLOUD_PROJECT")
location = os.getenv("GOOGLE_CLOUD_LOCATION")
bucket = os.getenv("GOOGLE_CLOUD_STORAGE_BUCKET")

if not project_id:
print("Missing required environment variable: GOOGLE_CLOUD_PROJECT")
return
elif not location:
print("Missing required environment variable: GOOGLE_CLOUD_LOCATION")
return
elif not bucket:
print(
"Missing required environment variable: GOOGLE_CLOUD_STORAGE_BUCKET"
)
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bucket environment variable name here is GOOGLE_CLOUD_STORAGE_BUCKET, but elsewhere in this sample the staging bucket is STAGING_BUCKET (see shared_libraries/constants.py / env.example). This inconsistency will cause setup confusion; align on one variable name and update the error messages accordingly.

Copilot uses AI. Check for mistakes.
Comment on lines +100 to +105
# Build query with optional category filter
where_clause = f"WHERE LOWER(brand) = LOWER('{brand}')"
if category:
category = category.strip()
where_clause += f" AND LOWER(category) = LOWER('{category}')"

Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_product_details_for_brand builds the SQL WHERE clause via f-string interpolation of brand/category, unlike get_categories_for_brand which correctly uses query parameters. This is vulnerable to SQL injection and can also break on quotes in brand/category names. Use QueryJobConfig with ScalarQueryParameter (or at least parameterized @brand/@category) instead of interpolating user input into the query string.

Copilot uses AI. Check for mistakes.
Comment on lines +27 to +32
comparsion_critic_agent = Agent(
model=constants.MODEL,
name="comparison_critic_agent",
description="A helpful agent to critique comparison.",
instruction=prompt.COMPARISON_CRITIC_AGENT_PROMPT,
)
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in variable name comparsion_critic_agent (should be comparison_critic_agent). While it doesn’t break execution (it’s referenced consistently), it makes the code harder to read/search and is easy to propagate elsewhere.

Copilot uses AI. Check for mistakes.
Comment on lines +98 to +100
print(
"\n--- Instructions on how to add permissions to BQ Table are in the customiztion.md file ---"
)
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in the printed message: customiztion.mdcustomization.md (or update to the correct filename).

Copilot uses AI. Check for mistakes.
Comment on lines +15 to +60
"""Test deployment of FOMC Research Agent to Agent Engine."""

import asyncio
import os

import vertexai
from absl import app, flags
from dotenv import load_dotenv
from google.adk.sessions import VertexAiSessionService
from vertexai import agent_engines

FLAGS = flags.FLAGS

flags.DEFINE_string("project_id", None, "GCP project ID.")
flags.DEFINE_string("location", None, "GCP location.")
flags.DEFINE_string("bucket", None, "GCP bucket.")
flags.DEFINE_string(
"resource_id",
None,
"ReasoningEngine resource ID (returned after deploying the agent)",
)
flags.DEFINE_string("user_id", None, "User ID (can be any string).")
flags.mark_flag_as_required("resource_id")
flags.mark_flag_as_required("user_id")


def main(argv: list[str]) -> None: # pylint: disable=unused-argument
load_dotenv()

project_id = (
FLAGS.project_id
if FLAGS.project_id
else os.getenv("GOOGLE_CLOUD_PROJECT")
)
location = (
FLAGS.location if FLAGS.location else os.getenv("GOOGLE_CLOUD_LOCATION")
)
bucket = (
FLAGS.bucket
if FLAGS.bucket
else os.getenv("GOOGLE_CLOUD_STORAGE_BUCKET")
)

project_id = os.getenv("GOOGLE_CLOUD_PROJECT")
location = os.getenv("GOOGLE_CLOUD_LOCATION")
bucket = os.getenv("GOOGLE_CLOUD_STORAGE_BUCKET")
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This script ignores CLI flags because project_id, location, and bucket are re-assigned from environment variables immediately after computing the flag/env fallback (lines 44–60). Remove the second assignment block so flags actually work, or intentionally drop the flags. Also the module docstring mentions “FOMC Research Agent”, which doesn’t match this sample.

Copilot uses AI. Check for mistakes.
Comment on lines +241 to +252
payload = {
"jsonrpc": "2.0",
"id": request_id,
"method": "pushNotificationConfig/set",
"params": {
"taskId": task_id,
"pushNotificationConfig": {
"url": webhook_url,
"token": token,
},
},
}
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

register_push() uses JSON-RPC method pushNotificationConfig/set, but the rest of this sample (docs + cli_test.py) uses tasks/pushNotificationConfig/set and explicitly notes the tasks/ prefix requirement. If the server expects the tasks/-prefixed method, push registration from the SDK client/orchestrator will fail. Align the method name (and any required param shape) with the documented/working pattern.

Copilot uses AI. Check for mistakes.
Comment on lines +111 to +116
# 3b. Push notification sender — POSTs task updates to registered webhook URLs
httpx_client = httpx.AsyncClient(timeout=30.0)
push_sender = BasePushNotificationSender(
httpx_client=httpx_client,
config_store=push_config_store,
)
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

httpx.AsyncClient is created at module scope (httpx_client = httpx.AsyncClient(...)) but never closed. This can leak connections and emit warnings on shutdown. Consider creating it during app startup and closing it during shutdown (Starlette lifespan events), or using an async context manager around the server lifecycle.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

From Fork Samples Changes to Samples

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants