chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
# Azure AI Foundry Configuration (required for a2a_server.py and a2a_agent_as_function_tools.py)
|
||||
FOUNDRY_PROJECT_ENDPOINT=https://your-project.services.ai.azure.com/api/projects/your-project
|
||||
FOUNDRY_MODEL=gpt-4o
|
||||
|
||||
# A2A Client Configuration (required for agent_with_a2a.py and a2a_agent_as_function_tools.py)
|
||||
# The function-tools flow uses http://localhost:5000/ in the sample docs/output.
|
||||
# Update this value if you start a different local A2A server instance on another port.
|
||||
A2A_AGENT_HOST=http://localhost:5000/
|
||||
@@ -0,0 +1,94 @@
|
||||
# A2A Server Hosting Examples
|
||||
|
||||
This sample demonstrates how to **host** Agent Framework agents as A2A-compliant servers using the [A2A (Agent2Agent) protocol](https://a2a-protocol.org/latest/).
|
||||
|
||||
> **Looking for client samples?** See [`samples/02-agents/a2a/`](../../02-agents/a2a/) for consuming remote A2A agents.
|
||||
|
||||
## Server Samples
|
||||
|
||||
| Run this file | To... |
|
||||
|---------------|-------|
|
||||
| **[`a2a_server.py`](a2a_server.py)** | Host an Agent Framework agent as an A2A-compliant server (multi-agent). |
|
||||
| **[`agent_framework_to_a2a.py`](agent_framework_to_a2a.py)** | Minimal example: expose a single agent as an A2A server. |
|
||||
|
||||
## Supporting Modules
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| [`agent_definitions.py`](agent_definitions.py) | Agent and AgentCard factory definitions for invoice, policy, and logistics agents. |
|
||||
| [`invoice_data.py`](invoice_data.py) | Mock invoice data and tool functions for the invoice agent. |
|
||||
| [`a2a_server.http`](a2a_server.http) | REST Client requests for testing the server directly from VS Code. |
|
||||
|
||||
## Environment Variables
|
||||
|
||||
### Required (Server)
|
||||
- `FOUNDRY_PROJECT_ENDPOINT` — Your Azure AI Foundry project endpoint
|
||||
- `FOUNDRY_MODEL` — Model deployment name (e.g. `gpt-4o`)
|
||||
|
||||
## Quick Start
|
||||
|
||||
All commands below should be run from this directory:
|
||||
|
||||
```powershell
|
||||
cd python/samples/04-hosting/a2a
|
||||
```
|
||||
|
||||
### 0. Install Dependencies
|
||||
|
||||
Copy `.env.example` to `.env` and fill in your values:
|
||||
|
||||
```powershell
|
||||
copy .env.example .env
|
||||
```
|
||||
|
||||
**Option A — pip (standard):**
|
||||
|
||||
```powershell
|
||||
python -m venv .venv
|
||||
.venv\Scripts\Activate.ps1 # Windows
|
||||
# source .venv/bin/activate # macOS / Linux
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
**Option B — uv:**
|
||||
|
||||
```powershell
|
||||
uv run python a2a_server.py --agent-type policy
|
||||
```
|
||||
|
||||
### 1. Start the A2A Server
|
||||
|
||||
> **Note (Option A — pip users):** Replace `uv run python` with `python` in all `uv run` commands below. `uv` is not required once the virtual environment is activated.
|
||||
|
||||
Pick an agent type and start the server (each in its own terminal):
|
||||
|
||||
```powershell
|
||||
uv run python a2a_server.py --agent-type invoice --port 5000
|
||||
uv run python a2a_server.py --agent-type policy --port 5001
|
||||
uv run python a2a_server.py --agent-type logistics --port 5002
|
||||
```
|
||||
|
||||
You can run one agent or all three — each listens on its own port.
|
||||
|
||||
### 2. Run a Client
|
||||
|
||||
Once a server is running, use any of the client samples in [`samples/02-agents/a2a/`](../../02-agents/a2a/):
|
||||
|
||||
```powershell
|
||||
cd python/samples/02-agents/a2a
|
||||
$env:A2A_AGENT_HOST = "http://localhost:5001/"
|
||||
uv run python agent_with_a2a.py
|
||||
```
|
||||
|
||||
## Security considerations for multi-tenant hosting
|
||||
|
||||
The default `a2a-sdk` task/push-config stores scope ownership by `user_name` only. **Any host that mounts tenant-bearing routes must pass a tenant-aware `owner_resolver`** to the stores, e.g.:
|
||||
|
||||
```python
|
||||
from a2a.server.tasks import InMemoryTaskStore
|
||||
|
||||
def resolve_tenant_user_scope(context):
|
||||
# Derive tenant + user identity from your host's auth/session context.
|
||||
return f"{context.tenant}:{context.user.user_name}"
|
||||
task_store = InMemoryTaskStore(owner_resolver=resolve_tenant_user_scope)
|
||||
```
|
||||
@@ -0,0 +1,82 @@
|
||||
### Each A2A agent is available at a different host address
|
||||
@hostInvoice = http://localhost:5000
|
||||
@hostPolicy = http://localhost:5001
|
||||
@hostLogistics = http://localhost:5002
|
||||
|
||||
### Query agent card for the invoice agent
|
||||
GET {{hostInvoice}}/.well-known/agent.json
|
||||
|
||||
### Send a message to the invoice agent
|
||||
POST {{hostInvoice}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": "1",
|
||||
"jsonrpc": "2.0",
|
||||
"method": "message/send",
|
||||
"params": {
|
||||
"message": {
|
||||
"kind": "message",
|
||||
"role": "user",
|
||||
"messageId": "msg_1",
|
||||
"parts": [
|
||||
{
|
||||
"kind": "text",
|
||||
"text": "Show me all invoices for Contoso"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
### Query agent card for the policy agent
|
||||
GET {{hostPolicy}}/.well-known/agent.json
|
||||
|
||||
### Send a message to the policy agent
|
||||
POST {{hostPolicy}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": "2",
|
||||
"jsonrpc": "2.0",
|
||||
"method": "message/send",
|
||||
"params": {
|
||||
"message": {
|
||||
"kind": "message",
|
||||
"role": "user",
|
||||
"messageId": "msg_2",
|
||||
"parts": [
|
||||
{
|
||||
"kind": "text",
|
||||
"text": "What is the policy for short shipments?"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
### Query agent card for the logistics agent
|
||||
GET {{hostLogistics}}/.well-known/agent.json
|
||||
|
||||
### Send a message to the logistics agent
|
||||
POST {{hostLogistics}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": "3",
|
||||
"jsonrpc": "2.0",
|
||||
"method": "message/send",
|
||||
"params": {
|
||||
"message": {
|
||||
"kind": "message",
|
||||
"role": "user",
|
||||
"messageId": "msg_3",
|
||||
"parts": [
|
||||
{
|
||||
"kind": "text",
|
||||
"text": "What is the status for SHPMT-SAP-001?"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
import uvicorn
|
||||
from a2a.server.request_handlers import DefaultRequestHandler
|
||||
from a2a.server.routes import create_agent_card_routes, create_jsonrpc_routes
|
||||
from a2a.server.tasks import InMemoryTaskStore
|
||||
from agent_definitions import AGENT_CARD_FACTORIES, AGENT_FACTORIES # pyrefly: ignore[missing-import]
|
||||
from agent_framework.a2a import A2AExecutor
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
from starlette.applications import Starlette
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
"""
|
||||
A2A Server Sample — Host an Agent Framework agent as an A2A endpoint
|
||||
|
||||
This sample creates a Python-based A2A-compliant server that wraps an Agent
|
||||
Framework agent. The server uses the a2a-sdk's Starlette application to handle
|
||||
JSON-RPC requests and serves the AgentCard at /.well-known/agent.json.
|
||||
|
||||
Three agent types are available:
|
||||
- invoice — Answers invoice queries using mock data and function tools.
|
||||
- policy — Returns a fixed policy response.
|
||||
- logistics — Returns a fixed logistics response.
|
||||
|
||||
Usage:
|
||||
uv run python a2a_server.py --agent-type policy --port 5001
|
||||
uv run python a2a_server.py --agent-type invoice --port 5000
|
||||
uv run python a2a_server.py --agent-type logistics --port 5002
|
||||
|
||||
Environment variables:
|
||||
FOUNDRY_PROJECT_ENDPOINT — Your Azure AI Foundry project endpoint
|
||||
FOUNDRY_MODEL — Model deployment name (e.g. gpt-4o)
|
||||
"""
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="A2A Agent Server")
|
||||
parser.add_argument(
|
||||
"--agent-type",
|
||||
choices=["invoice", "policy", "logistics"],
|
||||
default="policy",
|
||||
help="Type of agent to host (default: policy)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--host",
|
||||
default="localhost",
|
||||
help="Host to bind to (default: localhost)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--port",
|
||||
type=int,
|
||||
default=5001,
|
||||
help="Port to listen on (default: 5001)",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
|
||||
# Validate environment
|
||||
project_endpoint = os.getenv("FOUNDRY_PROJECT_ENDPOINT")
|
||||
model = os.getenv("FOUNDRY_MODEL")
|
||||
|
||||
if not project_endpoint:
|
||||
print("Error: FOUNDRY_PROJECT_ENDPOINT environment variable is not set.")
|
||||
sys.exit(1)
|
||||
if not model:
|
||||
print("Error: FOUNDRY_MODEL environment variable is not set.")
|
||||
sys.exit(1)
|
||||
|
||||
# Create the LLM client
|
||||
credential = AzureCliCredential()
|
||||
client = FoundryChatClient(
|
||||
project_endpoint=project_endpoint,
|
||||
model=model,
|
||||
credential=credential,
|
||||
)
|
||||
|
||||
# Create the Agent Framework agent for the chosen type
|
||||
agent_factory = AGENT_FACTORIES[args.agent_type]
|
||||
agent = agent_factory(client)
|
||||
|
||||
# Build the A2A server components
|
||||
url = f"http://{args.host}:{args.port}/"
|
||||
agent_card = AGENT_CARD_FACTORIES[args.agent_type](url)
|
||||
executor = A2AExecutor(agent, stream=True)
|
||||
task_store = InMemoryTaskStore()
|
||||
request_handler = DefaultRequestHandler(
|
||||
agent_executor=executor,
|
||||
task_store=task_store,
|
||||
agent_card=agent_card,
|
||||
)
|
||||
|
||||
app = Starlette(
|
||||
routes=[
|
||||
*create_agent_card_routes(agent_card),
|
||||
*create_jsonrpc_routes(request_handler, "/"),
|
||||
]
|
||||
)
|
||||
|
||||
print(f"Starting A2A server: {agent_card.name}")
|
||||
print(f" Agent type : {args.agent_type}")
|
||||
print(f" Listening : {url}")
|
||||
print(f" Agent card : {url}.well-known/agent.json")
|
||||
print()
|
||||
|
||||
uvicorn.run(
|
||||
app,
|
||||
host=args.host,
|
||||
port=args.port,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,167 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""Agent definitions and AgentCard factories for the A2A server sample.
|
||||
|
||||
Provides factory functions to create Agent Framework agents and A2A
|
||||
AgentCards for the invoice, policy, and logistics agent types.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from a2a.types import AgentCapabilities, AgentCard, AgentInterface, AgentSkill
|
||||
from agent_framework import Agent
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from invoice_data import query_by_invoice_id, query_by_transaction_id, query_invoices # pyrefly: ignore[missing-import]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Agent instructions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
INVOICE_INSTRUCTIONS = "You specialize in handling queries related to invoices."
|
||||
|
||||
POLICY_INSTRUCTIONS = """\
|
||||
You specialize in handling queries related to policies and customer communications.
|
||||
|
||||
Always reply with exactly this text:
|
||||
|
||||
Policy: Short Shipment Dispute Handling Policy V2.1
|
||||
|
||||
Summary: "For short shipments reported by customers, first verify internal shipment records
|
||||
(SAP) and physical logistics scan data (BigQuery). If discrepancy is confirmed and logistics data
|
||||
shows fewer items packed than invoiced, issue a credit for the missing items. Document the
|
||||
resolution in SAP CRM and notify the customer via email within 2 business days, referencing the
|
||||
original invoice and the credit memo number. Use the 'Formal Credit Notification' email
|
||||
template."
|
||||
"""
|
||||
|
||||
LOGISTICS_INSTRUCTIONS = """\
|
||||
You specialize in handling queries related to logistics.
|
||||
|
||||
Always reply with exactly:
|
||||
|
||||
Shipment number: SHPMT-SAP-001
|
||||
Item: TSHIRT-RED-L
|
||||
Quantity: 900
|
||||
"""
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Agent factories
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def create_invoice_agent(client: FoundryChatClient) -> Agent:
|
||||
"""Create an invoice agent backed by the given client with query tools."""
|
||||
return Agent(
|
||||
client=client,
|
||||
name="InvoiceAgent",
|
||||
instructions=INVOICE_INSTRUCTIONS,
|
||||
tools=[query_invoices, query_by_transaction_id, query_by_invoice_id],
|
||||
)
|
||||
|
||||
|
||||
def create_policy_agent(client: FoundryChatClient) -> Agent:
|
||||
"""Create a policy agent backed by the given client."""
|
||||
return Agent(
|
||||
client=client,
|
||||
name="PolicyAgent",
|
||||
instructions=POLICY_INSTRUCTIONS,
|
||||
)
|
||||
|
||||
|
||||
def create_logistics_agent(client: FoundryChatClient) -> Agent:
|
||||
"""Create a logistics agent backed by the given client."""
|
||||
return Agent(
|
||||
client=client,
|
||||
name="LogisticsAgent",
|
||||
instructions=LOGISTICS_INSTRUCTIONS,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# AgentCard factories
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_CAPABILITIES = AgentCapabilities(streaming=True, push_notifications=False)
|
||||
|
||||
|
||||
def get_invoice_agent_card(url: str) -> AgentCard:
|
||||
"""Return an A2A AgentCard for the invoice agent."""
|
||||
return AgentCard(
|
||||
name="InvoiceAgent",
|
||||
description="Handles requests relating to invoices.",
|
||||
version="1.0.0",
|
||||
default_input_modes=["text"],
|
||||
default_output_modes=["text"],
|
||||
capabilities=_CAPABILITIES,
|
||||
supported_interfaces=[AgentInterface(url=url, protocol_binding="JSONRPC")],
|
||||
skills=[
|
||||
AgentSkill(
|
||||
id="id_invoice_agent",
|
||||
name="InvoiceQuery",
|
||||
description="Handles requests relating to invoices.",
|
||||
tags=["invoice", "agent-framework"],
|
||||
examples=["List the latest invoices for Contoso."],
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def get_policy_agent_card(url: str) -> AgentCard:
|
||||
"""Return an A2A AgentCard for the policy agent."""
|
||||
return AgentCard(
|
||||
name="PolicyAgent",
|
||||
description="Handles requests relating to policies and customer communications.",
|
||||
version="1.0.0",
|
||||
default_input_modes=["text"],
|
||||
default_output_modes=["text"],
|
||||
capabilities=_CAPABILITIES,
|
||||
supported_interfaces=[AgentInterface(url=url, protocol_binding="JSONRPC")],
|
||||
skills=[
|
||||
AgentSkill(
|
||||
id="id_policy_agent",
|
||||
name="PolicyAgent",
|
||||
description="Handles requests relating to policies and customer communications.",
|
||||
tags=["policy", "agent-framework"],
|
||||
examples=["What is the policy for short shipments?"],
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def get_logistics_agent_card(url: str) -> AgentCard:
|
||||
"""Return an A2A AgentCard for the logistics agent."""
|
||||
return AgentCard(
|
||||
name="LogisticsAgent",
|
||||
description="Handles requests relating to logistics.",
|
||||
version="1.0.0",
|
||||
default_input_modes=["text"],
|
||||
default_output_modes=["text"],
|
||||
capabilities=_CAPABILITIES,
|
||||
supported_interfaces=[AgentInterface(url=url, protocol_binding="JSONRPC")],
|
||||
skills=[
|
||||
AgentSkill(
|
||||
id="id_logistics_agent",
|
||||
name="LogisticsQuery",
|
||||
description="Handles requests relating to logistics.",
|
||||
tags=["logistics", "agent-framework"],
|
||||
examples=["What is the status for SHPMT-SAP-001"],
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lookup helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
AGENT_FACTORIES = {
|
||||
"invoice": create_invoice_agent,
|
||||
"policy": create_policy_agent,
|
||||
"logistics": create_logistics_agent,
|
||||
}
|
||||
|
||||
AGENT_CARD_FACTORIES = {
|
||||
"invoice": get_invoice_agent_card,
|
||||
"policy": get_policy_agent_card,
|
||||
"logistics": get_logistics_agent_card,
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import uvicorn
|
||||
from a2a.server.request_handlers import DefaultRequestHandler
|
||||
from a2a.server.routes import create_agent_card_routes, create_jsonrpc_routes
|
||||
from a2a.server.tasks import InMemoryTaskStore
|
||||
from a2a.types import (
|
||||
AgentCapabilities,
|
||||
AgentCard,
|
||||
AgentInterface,
|
||||
AgentSkill,
|
||||
)
|
||||
from agent_framework import Agent
|
||||
from agent_framework.a2a import A2AExecutor
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
from dotenv import load_dotenv
|
||||
from starlette.applications import Starlette
|
||||
|
||||
load_dotenv()
|
||||
|
||||
if __name__ == "__main__":
|
||||
# --8<-- [start:AgentSkill]
|
||||
flight_skill = AgentSkill(
|
||||
id="Flight_Booking",
|
||||
name="Flight Booking",
|
||||
description="Search and book flights across Europe.",
|
||||
tags=["flights", "travel", "europe"],
|
||||
examples=[],
|
||||
)
|
||||
hotel_skill = AgentSkill(
|
||||
id="Hotel_Booking",
|
||||
name="Hotel Booking",
|
||||
description="Search and book hotels across Europe.",
|
||||
tags=["hotels", "travel", "accommodation"],
|
||||
examples=[],
|
||||
)
|
||||
# --8<-- [end:AgentSkill]
|
||||
|
||||
# --8<-- [start:AgentCard]
|
||||
# This will be the public-facing agent card
|
||||
public_agent_card = AgentCard(
|
||||
name="Europe Travel Agent",
|
||||
description="A helpful Europe Travel Agent that can help users search and book flights and hotels across Europe.",
|
||||
version="1.0.0",
|
||||
default_input_modes=["text"],
|
||||
default_output_modes=["text"],
|
||||
capabilities=AgentCapabilities(streaming=True),
|
||||
supported_interfaces=[AgentInterface(url="http://localhost:9999/", protocol_binding="JSONRPC")],
|
||||
skills=[flight_skill, hotel_skill],
|
||||
)
|
||||
# --8<-- [end:AgentCard]
|
||||
|
||||
agent = Agent(
|
||||
client=OpenAIChatClient(),
|
||||
name="Europe Travel Agent",
|
||||
instructions="You are a helpful Europe Travel Agent. You can help users search and book flights and hotels across Europe.",
|
||||
)
|
||||
|
||||
request_handler = DefaultRequestHandler(
|
||||
agent_executor=A2AExecutor(agent),
|
||||
task_store=InMemoryTaskStore(),
|
||||
agent_card=public_agent_card,
|
||||
)
|
||||
|
||||
server = Starlette(
|
||||
routes=[
|
||||
*create_agent_card_routes(public_agent_card),
|
||||
*create_jsonrpc_routes(request_handler, "/"),
|
||||
]
|
||||
)
|
||||
|
||||
uvicorn.run(server, host="0.0.0.0", port=9999)
|
||||
@@ -0,0 +1,227 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""Mock invoice data and tool functions for the A2A server sample.
|
||||
|
||||
Provides mock invoice data and query tools for the A2A server sample,
|
||||
enabling invoice-related queries through the A2A protocol.
|
||||
"""
|
||||
|
||||
import json
|
||||
import random
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Annotated
|
||||
|
||||
from agent_framework import tool
|
||||
from pydantic import Field
|
||||
|
||||
|
||||
@dataclass
|
||||
class Product:
|
||||
"""A product line item on an invoice."""
|
||||
|
||||
name: str
|
||||
quantity: int
|
||||
price_per_unit: float
|
||||
|
||||
@property
|
||||
def total_price(self) -> float:
|
||||
return self.quantity * self.price_per_unit
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"name": self.name,
|
||||
"quantity": self.quantity,
|
||||
"price_per_unit": self.price_per_unit,
|
||||
"total_price": self.total_price,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class Invoice:
|
||||
"""An invoice record with products."""
|
||||
|
||||
transaction_id: str
|
||||
invoice_id: str
|
||||
company_name: str
|
||||
invoice_date: datetime
|
||||
products: list[Product] = field(default_factory=list)
|
||||
|
||||
@property
|
||||
def total_invoice_price(self) -> float:
|
||||
return sum(p.total_price for p in self.products)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"transaction_id": self.transaction_id,
|
||||
"invoice_id": self.invoice_id,
|
||||
"company_name": self.company_name,
|
||||
"invoice_date": self.invoice_date.strftime("%Y-%m-%d"),
|
||||
"products": [p.to_dict() for p in self.products],
|
||||
"total_invoice_price": self.total_invoice_price,
|
||||
}
|
||||
|
||||
|
||||
def _random_date_within_last_two_months() -> datetime:
|
||||
end_date = datetime.now(timezone.utc)
|
||||
start_date = end_date - timedelta(days=60)
|
||||
random_days = random.randint(0, 60)
|
||||
return start_date + timedelta(days=random_days)
|
||||
|
||||
|
||||
def _build_invoices() -> list[Invoice]:
|
||||
"""Build 10 mock invoices."""
|
||||
return [
|
||||
Invoice(
|
||||
"TICKET-XYZ987",
|
||||
"INV789",
|
||||
"Contoso",
|
||||
_random_date_within_last_two_months(),
|
||||
[
|
||||
Product("T-Shirts", 150, 10.00),
|
||||
Product("Hats", 200, 15.00),
|
||||
Product("Glasses", 300, 5.00),
|
||||
],
|
||||
),
|
||||
Invoice(
|
||||
"TICKET-XYZ111",
|
||||
"INV111",
|
||||
"XStore",
|
||||
_random_date_within_last_two_months(),
|
||||
[
|
||||
Product("T-Shirts", 2500, 12.00),
|
||||
Product("Hats", 1500, 8.00),
|
||||
Product("Glasses", 200, 20.00),
|
||||
],
|
||||
),
|
||||
Invoice(
|
||||
"TICKET-XYZ222",
|
||||
"INV222",
|
||||
"Cymbal Direct",
|
||||
_random_date_within_last_two_months(),
|
||||
[
|
||||
Product("T-Shirts", 1200, 14.00),
|
||||
Product("Hats", 800, 7.00),
|
||||
Product("Glasses", 500, 25.00),
|
||||
],
|
||||
),
|
||||
Invoice(
|
||||
"TICKET-XYZ333",
|
||||
"INV333",
|
||||
"Contoso",
|
||||
_random_date_within_last_two_months(),
|
||||
[
|
||||
Product("T-Shirts", 400, 11.00),
|
||||
Product("Hats", 600, 15.00),
|
||||
Product("Glasses", 700, 5.00),
|
||||
],
|
||||
),
|
||||
Invoice(
|
||||
"TICKET-XYZ444",
|
||||
"INV444",
|
||||
"XStore",
|
||||
_random_date_within_last_two_months(),
|
||||
[
|
||||
Product("T-Shirts", 800, 10.00),
|
||||
Product("Hats", 500, 18.00),
|
||||
Product("Glasses", 300, 22.00),
|
||||
],
|
||||
),
|
||||
Invoice(
|
||||
"TICKET-XYZ555",
|
||||
"INV555",
|
||||
"Cymbal Direct",
|
||||
_random_date_within_last_two_months(),
|
||||
[
|
||||
Product("T-Shirts", 1100, 9.00),
|
||||
Product("Hats", 900, 12.00),
|
||||
Product("Glasses", 1200, 15.00),
|
||||
],
|
||||
),
|
||||
Invoice(
|
||||
"TICKET-XYZ666",
|
||||
"INV666",
|
||||
"Contoso",
|
||||
_random_date_within_last_two_months(),
|
||||
[
|
||||
Product("T-Shirts", 2500, 8.00),
|
||||
Product("Hats", 1200, 10.00),
|
||||
Product("Glasses", 1000, 6.00),
|
||||
],
|
||||
),
|
||||
Invoice(
|
||||
"TICKET-XYZ777",
|
||||
"INV777",
|
||||
"XStore",
|
||||
_random_date_within_last_two_months(),
|
||||
[
|
||||
Product("T-Shirts", 1900, 13.00),
|
||||
Product("Hats", 1300, 16.00),
|
||||
Product("Glasses", 800, 19.00),
|
||||
],
|
||||
),
|
||||
Invoice(
|
||||
"TICKET-XYZ888",
|
||||
"INV888",
|
||||
"Cymbal Direct",
|
||||
_random_date_within_last_two_months(),
|
||||
[
|
||||
Product("T-Shirts", 2200, 11.00),
|
||||
Product("Hats", 1700, 8.50),
|
||||
Product("Glasses", 600, 21.00),
|
||||
],
|
||||
),
|
||||
Invoice(
|
||||
"TICKET-XYZ999",
|
||||
"INV999",
|
||||
"Contoso",
|
||||
_random_date_within_last_two_months(),
|
||||
[
|
||||
Product("T-Shirts", 1400, 10.50),
|
||||
Product("Hats", 1100, 9.00),
|
||||
Product("Glasses", 950, 12.00),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
# Module-level singleton so dates are stable for the lifetime of the server
|
||||
INVOICES = _build_invoices()
|
||||
|
||||
|
||||
@tool(approval_mode="never_require")
|
||||
def query_invoices(
|
||||
company_name: Annotated[str, Field(description="The company name to filter invoices by.")],
|
||||
start_date: Annotated[str | None, Field(description="Optional start date (YYYY-MM-DD) to filter invoices.")] = None,
|
||||
end_date: Annotated[str | None, Field(description="Optional end date (YYYY-MM-DD) to filter invoices.")] = None,
|
||||
) -> str:
|
||||
"""Retrieves invoices for the specified company and optionally within the specified time range."""
|
||||
results = [i for i in INVOICES if i.company_name.lower() == company_name.lower()]
|
||||
|
||||
if start_date:
|
||||
start = datetime.strptime(start_date, "%Y-%m-%d").replace(tzinfo=timezone.utc)
|
||||
results = [i for i in results if i.invoice_date >= start]
|
||||
|
||||
if end_date:
|
||||
end = datetime.strptime(end_date, "%Y-%m-%d").replace(tzinfo=timezone.utc) + timedelta(days=1)
|
||||
results = [i for i in results if i.invoice_date < end]
|
||||
|
||||
return json.dumps([i.to_dict() for i in results], indent=2)
|
||||
|
||||
|
||||
@tool(approval_mode="never_require")
|
||||
def query_by_transaction_id(
|
||||
transaction_id: Annotated[str, Field(description="The transaction ID to look up (e.g. TICKET-XYZ987).")],
|
||||
) -> str:
|
||||
"""Retrieves invoice using the transaction id."""
|
||||
results = [i for i in INVOICES if i.transaction_id.lower() == transaction_id.lower()]
|
||||
return json.dumps([i.to_dict() for i in results], indent=2)
|
||||
|
||||
|
||||
@tool(approval_mode="never_require")
|
||||
def query_by_invoice_id(
|
||||
invoice_id: Annotated[str, Field(description="The invoice ID to look up (e.g. INV789).")],
|
||||
) -> str:
|
||||
"""Retrieves invoice using the invoice id."""
|
||||
results = [i for i in INVOICES if i.invoice_id.lower() == invoice_id.lower()]
|
||||
return json.dumps([i.to_dict() for i in results], indent=2)
|
||||
@@ -0,0 +1,23 @@
|
||||
# Agent Framework packages
|
||||
# To use the deployed version, uncomment the lines below and comment out the local installation lines
|
||||
# agent-framework-a2a
|
||||
# agent-framework-foundry
|
||||
|
||||
# Local installation (for development and testing)
|
||||
# Each package must be listed explicitly because pip doesn't resolve uv workspace sources.
|
||||
# Without explicit entries, pip would fetch transitive dependencies from PyPI instead of local source.
|
||||
-e ../../../packages/core # Core framework - base dependency for all packages
|
||||
-e ../../../packages/foundry # Foundry support - dependency for FoundryChatClient in a2a_server.py
|
||||
-e ../../../packages/a2a # A2A integration - provides A2AAgent and a2a-sdk
|
||||
|
||||
# Azure authentication
|
||||
azure-identity
|
||||
|
||||
# A2A server runtime
|
||||
uvicorn
|
||||
|
||||
# HTTP client used by A2A client samples
|
||||
httpx
|
||||
|
||||
# Environment variable loading
|
||||
python-dotenv
|
||||
Reference in New Issue
Block a user