Experimental distributed application built with .NET + Python using .NET Aspire and the Agent‑to‑Agent (A2A) protocol.
It demonstrates orchestration from .NET into Python agents powered by Azure AI.
- Aspire AppHost – Orchestrates all services locally
- .NET API Service – Entry point + agent orchestrator
- Python Backend Service – Hosts A2A agents
- Azure AI Foundry integration – LLM‑powered orchestration
- In‑tool data access (Python) – Data processing happens inside agent tools
Conceptually, this is a minimal distributed agent system:
flowchart LR
Client -->|HTTP| DotNetAPI
DotNetAPI -->|A2A JSON-RPC| PythonAgent
DotNetAPI -->|LLM Orchestration| AzureAI
PythonAgent -->|Tool Execution| DataLogic
- .NET 10 SDK
- Python 3.12+
- uv (Python package manager)
- .NET Aspire
The following must be configured in Aspire AppHost settings or user-secrets:
Parameters:AiFoundryProjectEndpointParameters:ModelDeploymentName
These are injected into the .NET API service via Aspire configuration binding.
If deploying beyond local development, ensure:
- Azure AI credentials are available as environment variables
- Services can resolve each other via Aspire service discovery
- Proper HTTP ports are exposed for A2A endpoints
dotnet user-secrets set "Parameters:AiFoundryProjectEndpoint" "https://<your-project>.services.ai.azure.com/api/projects/<project-id>" --project src/dotnet/AspireTrial.AppHost
dotnet user-secrets set "Parameters:ModelDeploymentName" "gpt-4o" --project src/dotnet/AspireTrial.AppHostConfiguration model:
src/dotnet/AspireTrial.ApiService/Options/AzureAIOptions.cs
More info:
https://learn.microsoft.com/azure/ai-studio/
cd src/python
uv sync --all-groupsThis creates .venv and installs runtime + dev dependencies.
aspire runAspire starts:
- AppHost
- .NET API
- Python backend
Client
↓
.NET API (Orchestrator)
↓ (A2A - JSON-RPC 2.0)
Python Agent
↓
Tool / Data Logic
- Client calls
.NETendpoint (e.g.,/countLetters-a2a). - .NET uses Azure AI to determine intent / orchestration.
- .NET discovers the Python agent via its Agent Card.
- .NET invokes the agent using A2A (JSON-RPC 2.0 over HTTP).
- Python executes tool logic and returns structured result.
Relevant files:
- .NET orchestrator:
src/dotnet/AspireTrial.ApiService/Services/AgentCollaboration.cs - Python agent router:
src/python/src/aspire_backend_service/routers/agents.py
This project uses Microsoft’s Agent‑to‑Agent (A2A) protocol:
https://github.com/microsoft/agents
- JSON-RPC endpoint:
/agents/count-letters-a2a - Agent card:
/agents/count-letters/.well-known/agent-card.json
The Agent Card enables discovery and describes:
- Capabilities
- Input/output schema
- Invocation endpoint
The .NET service:
- Fetches the agent card
- Constructs a JSON-RPC request
- Sends invocation over HTTP
- Handles structured response
This keeps services loosely coupled and independently deployable.
sequenceDiagram
participant Client
participant DotNetAPI
participant AzureAI
participant PythonAgent
Client->>DotNetAPI: HTTP Request
DotNetAPI->>AzureAI: Orchestration Prompt
AzureAI-->>DotNetAPI: Tool Decision
DotNetAPI->>PythonAgent: JSON-RPC (A2A)
PythonAgent-->>DotNetAPI: Structured Result
DotNetAPI-->>Client: HTTP Response
Current example:
- Receives text
- Counts letters
- Returns structured result
This is intentionally simple but demonstrates:
- Agent discovery
- Cross-runtime invocation (.NET → Python)
- Tool-based execution
- LLM-assisted orchestration
The system can be extended with:
- Additional Python agents
- Domain-specific tools
- Multi-agent collaboration
The Python backend follows a tool-centric design:
- Agents expose capabilities
- Tools perform actual computation / data logic
- Data access happens inside tools
Conceptually:
flowchart TD
Agent --> Tool
Tool --> DataAccess
DataAccess --> Result
Result --> Agent
Benefits:
- Clear separation of orchestration vs execution
- Testable tool layer
- Replaceable data sources
- Safer LLM integration (bounded capabilities)
This pattern aligns with modern agent architectures used in:
- Azure AI Foundry Agents
- Tool-calling LLM systems
This repository demonstrates:
- A distributed Aspire application
- .NET orchestrating Python agents
- A2A-based agent discovery and invocation
- Azure AI–powered orchestration
- Tool-based execution with in-process data access
After reading this, an engineer should understand:
- How to run the system locally
- How .NET invokes Python via A2A
- Where orchestration vs execution lives
- How agents and tools are structured
- How to extend the system with new agents
Use this as a foundation for experimenting with cross-runtime, multi-agent systems.
In a production setting, services are independently scalable and deployable.
flowchart LR
Client --> APIGateway
APIGateway --> DotNetAPI
DotNetAPI --> AzureAI
DotNetAPI --> PythonAgent
PythonAgent --> DataStore
Possible hosting options:
- Azure Container Apps
- AKS (Kubernetes)
- Azure App Service (mixed runtime)
Key characteristics:
- Independent scaling of .NET and Python services
- Centralized AI configuration
- A2A over internal service network
- Secure environment variable injection for secrets
This separation ensures the system remains loosely coupled while enabling horizontal scaling and independent evolution of agents.
.NET Aspire provides built-in observability for local development:
- Centralized logs
- Distributed tracing
- Service health monitoring
- Dependency visualization
When running aspire run, open the Aspire dashboard to:
- Inspect inter-service HTTP calls
- View A2A JSON-RPC requests
- Monitor startup dependencies
Conceptually:
flowchart LR
DotNetAPI -->|Logs/Traces| AspireDashboard
PythonAgent -->|Logs/Traces| AspireDashboard
AspireDashboard --> Developer
In production, integrate with:
- Azure Monitor
- Application Insights
- OpenTelemetry collectors
Both .NET and Python services can emit structured logs and traces for full distributed visibility.
The current implementation demonstrates a single Python agent.
The architecture supports multiple agents collaborating via A2A.
flowchart TD
Client --> DotNetOrchestrator
DotNetOrchestrator --> AgentA
DotNetOrchestrator --> AgentB
AgentA --> SharedData
AgentB --> SharedData
Example scenario:
- Agent A: Text analysis
- Agent B: Data enrichment
- Orchestrator decides execution order
- Results aggregated and returned
Because A2A is protocol-based and runtime-agnostic:
- Agents may be written in different languages
- Agents may live in separate deployments
- Orchestration logic remains centralized (or delegated)
This enables scalable, domain-specific, distributed AI systems built from composable agents.
This section explains how to debug the system end-to-end during development.
aspire runOpen the Aspire dashboard to observe:
- Service startup order
- HTTP traffic between services
- Logs and traces
- Open the solution in your IDE.
- Set a breakpoint in:
src/dotnet/AspireTrial.ApiService/Services/AgentCollaboration.cs - Trigger the endpoint (e.g.,
/countLetters-a2a).
You can inspect:
- The orchestration prompt
- The fetched agent card
- The constructed JSON-RPC payload
- The structured response from Python
- Open:
src/python/src/aspire_backend_service/routers/agents.py - Set breakpoints in the agent handler or tool logic.
- Re-trigger the .NET endpoint.
You can inspect:
- Incoming JSON-RPC request
- Parsed parameters
- Tool execution logic
- Returned structured result
Using Aspire dashboard or logs, verify:
- JSON-RPC 2.0 request structure
- Method name and params
- Response
resultvserror
You can also manually test the Python agent:
curl http://localhost:<python-port>/agents/count-letters-a2a \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":"1","method":"invoke","params":{"text":"hello"}}'- ✅ Agent card URL reachable
- ✅ JSON-RPC schema matches expected input
- ✅ Azure AI configuration correctly injected
- ✅ Services resolve via Aspire service discovery
This layered debugging approach (Client → .NET → A2A → Python → Tool) makes it easy to isolate issues in distributed agent systems.