<!--
Copyright 2026 The Dapr Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
    http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->

# Multi-Agent Workflows with Configuration Classes

This quickstart demonstrates how to create and orchestrate event-driven workflows with multiple autonomous agents using **Agent Configuration Classes** from Dapr Agents. You'll learn how to set up agents as separate services, implement three different workflow orchestration patterns, and enable real-time agent collaboration through pub/sub messaging—all using the modern configuration-based approach.

## What You'll Learn

- How to use **Agent Configuration Classes** for production-ready multi-agent systems
- Configure multiple agents with **AgentProfileConfig**, **AgentPubSubConfig**, **AgentStateConfig**, **AgentRegistryConfig**, and **AgentMemoryConfig**
- Implement three orchestration patterns: **Random**, **Round-Robin**, and **LLM-based**
- Deploy agents as separate microservices using **Dapr Multi-App Run**
- Enable agent collaboration through pub/sub messaging and shared registries
- Send messages to orchestrators or directly to individual agents

## Prerequisites

- uv package manager
- Dapr CLI and Docker
- OpenAI API key (or compatible LLM provider)

## Environment Setup

```bash
uv venv
# Activate the virtual environment
# On Windows:
.venv\Scripts\activate
# On macOS/Linux:
source .venv/bin/activate
uv sync --active
```

## The Fellowship: Multi-Agent Architecture

This quickstart features four agents from the Fellowship of the Ring, each with distinct roles and expertise:

| Agent | Role | Expertise | Topic |
|-------|------|-----------|-------|
| **Frodo** | Hobbit & Ring-bearer | Endurance, stealth, burden-bearing | `fellowship.frodo.requests` |
| **Sam** | Logistics & Support | Supplies, provisions, morale | `fellowship.sam.requests` |
| **Gandalf** | Wizard & Loremaster | Strategy, magic, ancient knowledge | `fellowship.gandalf.requests` |
| **Legolas** | Elf Scout & Marksman | Scouting, archery, terrain navigation | `fellowship.legolas.requests` |

### Orchestration Patterns

Three orchestrators coordinate the Fellowship (all using `DurableAgent` with different `orchestration_mode` values):

1. **Random Orchestrator** (`fellowship.orchestrator.random.requests`)
   - Uses `orchestration_mode="random"`
   - Randomly selects an agent for each task with avoidance logic
   - Good for load distribution and testing

2. **Round-Robin Orchestrator** (`fellowship.orchestrator.roundrobin.requests`)
   - Uses `orchestration_mode="roundrobin"`
   - Cycles through agents sequentially in deterministic order
   - Ensures fair task distribution

3. **Agent Orchestrator** (`llm.orchestrator.requests`)
   - Uses `orchestration_mode="agent"` (formerly "LLM Orchestrator")
   - Plan-based orchestration with AI-powered agent selection
   - Intelligent routing based on execution plan and task content

## Environment Setup

```bash
uv venv
# Activate the virtual environment
# On Windows:
.venv\Scripts\activate
# On macOS/Linux:
source .venv/bin/activate

# Install dependencies
pip install -r requirements.txt
```

## Configuration

### Option 1: Using Environment Variables (Recommended)

1. Create a `.env` file in the project root:

```env
OPENAI_API_KEY=your_api_key_here
```

2. Export environment variables before running:

#### macOS / Linux (Bash)

```bash
# Get environment variables from .env file
export $(grep -v '^#' ../../.env | xargs)
```

#### Windows (PowerShell)

```powershell
# Get the environment variables from the .env file:
Get-Content .env | Where-Object { $_ -and -not $_.StartsWith("#") } | ForEach-Object {
    $name, $value = $_.Split('=', 2)
    [System.Environment]::SetEnvironmentVariable($name, $value, "Process")
}
```

### Option 2: Direct Component Configuration

Update the `key` in [resources/openai.yaml](resources/openai.yaml):

```yaml
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: openai
spec:
  type: conversation.openai
  metadata:
    - name: key
      value: "YOUR_OPENAI_API_KEY"
```

### Required Components

Make sure Dapr is initialized:

```bash
dapr init
```

This quickstart uses these Dapr components (in `resources/` directory):

- `openai.yaml`: LLM conversation component
- `workflowstate.yaml`: Workflow state storage
- `memorystore.yaml`: Conversation memory storage
- `agentregistrystore.yaml`: Agent registry storage (shared across all agents)
- `messagepubsub.yaml`: Pub/sub messaging for agent communication

## Project Structure

```
05-multi-agent-workflows-new/
├── resources/                    # Dapr configuration files
│   ├── agentregistrystore.yaml    # Shared agent registry
│   ├── memorystore.yaml           # Conversation memory
│   ├── messagepubsub.yaml         # Pub/sub messaging
│   ├── openai.yaml                # LLM provider
│   └── workflowstate.yaml         # Workflow state
├── services/                      # Agent and orchestrator services
│   ├── frodo/                     # Frodo agent service
│   │   └── app.py
│   ├── sam/                       # Sam agent service
│   │   └── app.py
│   ├── gandalf/                   # Gandalf agent service
│   │   └── app.py
│   ├── legolas/                   # Legolas agent service
│   │   └── app.py
│   ├── workflow-random/           # Random orchestrator (DurableAgent with orchestration_mode="random")
│   │   └── app.py
│   ├── workflow-roundrobin/       # Round-robin orchestrator (DurableAgent with orchestration_mode="roundrobin")
│   │   └── app.py
│   ├── workflow-agent/            # Agent orchestrator (DurableAgent with orchestration_mode="agent")
│   │   └── app.py
│   └── client/                    # Client for publishing messages
│       └── pubsub_client.py
├── dapr-random.yaml               # Multi-app config: Random orchestrator
├── dapr-roundrobin.yaml           # Multi-app config: Round-robin orchestrator
├── dapr-agent.yaml                # Multi-app config: Agent orchestrator
└── README.md
```

## Agent Configuration with Config Classes

Each agent in this quickstart uses the latest **Agent Configuration Classes** updates.

### Key Benefits of Configuration Classes

1. **Shared Registry**: All agents use the same `team_name="fellowship"`, enabling discovery and coordination
2. **Isolated State**: Each agent has its own state store with unique key prefix
3. **Individual Topics**: Each agent subscribes to its own direct message topic
4. **Broadcast Channel**: All agents can receive team-wide broadcasts via `fellowship.broadcast`
5. **Type Safety**: Configuration classes provide validation and type checking

## Running the Multi-Agent System

The quickstart includes three Dapr Multi-App Run configurations, each showcasing a different orchestration pattern.

### Option 1: Random Orchestrator

Randomly selects agents for each task:

```bash
dapr run -f dapr-random.yaml
```

**What's running:**

- ✅ Frodo agent
- ✅ Sam agent
- ✅ Random orchestrator
- ✅ Client (publishes to `fellowship.orchestrator.random.requests`)

### Option 2: Round-Robin Orchestrator

Cycles through agents sequentially:

```bash
dapr run -f dapr-roundrobin.yaml
```

**What's running:**

- ✅ Frodo agent
- ✅ Sam agent
- ✅ Gandalf agent
- ✅ Round-robin orchestrator
- ✅ Client (publishes to `fellowship.orchestrator.roundrobin.requests`)

### Option 3: Agent Orchestrator

Plan-based AI-powered agent selection:

```bash
dapr run -f dapr-agent.yaml
```

**What's running:**

- ✅ Frodo agent
- ✅ Sam agent
- ✅ Gandalf agent
- ✅ LLM orchestrator
- ✅ HTTP client (hits the orchestrator’s `/agent/run` endpoint)

### Triggering Workflows

Each `dapr-*.yaml` spins up an `HttpClientApp` that keeps POSTing to `/agent/run` until the orchestrator responds (max 10 tries with a 5s delay). You’ll see the returned instance ID printed in that client’s console.

If you prefer to drive the system via pub/sub (for multiple tasks, or to target a specific orchestrator), run:

```bash
uv run python services/client/pubsub_client.py --orchestrator llm
uv run python services/client/pubsub_client.py --orchestrator random
uv run python services/client/pubsub_client.py --orchestrator roundrobin
```

That publishes `TriggerAction` messages onto the same topics the orchestrators subscribe to.

### Expected Output

You'll see logs from all services showing:

1. **Agent startup**: Each agent registers with the fellowship registry
2. **Client message**: Task published to orchestrator topic
3. **Orchestration**: Orchestrator selects and routes to an agent
4. **Agent response**: Selected agent processes the task
5. **Workflow completion**: Final result logged

## How Agent Coordination Works

### 1. Shared Registry

All agents register themselves in the fellowship registry:

```python
registry = AgentRegistryConfig(
    store=StateStoreService(store_name="agentregistrystore"),
    team_name="fellowship",  # SAME for all agents
)
```

### 2. Message Flow

**Via Orchestrator:**

```
Client → Orchestrator Topic → Orchestrator Logic → Agent Topic → Specific Agent
```

**Direct to Agent:**

```
Client → Agent Topic → Specific Agent
```

### 3. Agent Discovery

Orchestrators query the registry to discover available agents:

```python
# Orchestrator can find all fellowship members
available_agents = registry.get_team_members("fellowship")
# Returns: [frodo, sam, gandalf, legolas]
```

### 4. Broadcast Messages

Send to all agents simultaneously:

```python
# All agents subscribed to fellowship.broadcast receive this
pubsub_client.py --topic fellowship.broadcast --task "Emergency: Nazgûl approaching!"
```

## Agent Profiles Summary

### Frodo - Ring-bearer

- **Focus**: Endurance, stealth, burden management
- **Style**: Humble, determined, shows vulnerability but maintains courage
- **Best for**: Tasks requiring persistence and careful navigation

### Sam - Logistics Expert

- **Focus**: Supplies, provisions, morale, practical support
- **Style**: Warm, plain-spoken, grounded, loyal
- **Best for**: Resource management and practical problem-solving

### Gandalf - Wizard & Strategist

- **Focus**: Strategy, lore, magic, long-term planning
- **Style**: Wise, patient, mysterious yet clear in critical moments
- **Best for**: Complex decisions requiring wisdom and foresight

### Legolas - Scout & Marksman

- **Focus**: Scouting, threat detection, terrain navigation, ranged tactics
- **Style**: Graceful, precise, observant, elvish elegance
- **Best for**: Reconnaissance and tactical positioning

## Troubleshooting

### Issue: Agents not discovering each other

**Solution:** Verify all agents use the same registry configuration:

```python
registry = AgentRegistryConfig(
    store=StateStoreService(store_name="agentregistrystore"),
    team_name="fellowship",  # Must be identical
)
```

### Issue: Messages not reaching agents

**Solution:** Check topic names match between client and agent configurations:

- Client publishes to: `fellowship.frodo.requests`
- Agent subscribes to: `fellowship.frodo.requests` (must match exactly)

### Issue: Orchestrator not finding agents

**Solution:** Ensure orchestrator has access to the registry and agents are started first.

## Learn More

- **Configuration Classes Guide**: [`03-durable-agent-with-configs`](../03-durable-agent-with-configs/) - Complete guide to all configuration classes
- **Simple Agent Example**: [`03-durable-agent-tool-call`](../03-durable-agent-tool-call/) - Basic single-agent pattern
- **Message Router**: [`04-message-router-workflow`](../04-message-router-workflow/) - Pub/sub workflow patterns
- **Dapr Multi-App Run**: [Dapr Documentation](https://docs.dapr.io/developing-applications/local-development/multi-app-dapr-run/)
- **Dapr Pub/Sub**: [Pub/Sub Building Block](https://docs.dapr.io/developing-applications/building-blocks/pubsub/)
