748 lines
29 KiB
Markdown
748 lines
29 KiB
Markdown
# Assignment v2 Feature Documentation
|
|
|
|
## Overview
|
|
|
|
Assignment v2 is an intelligent, automated conversation distribution system that ensures conversations are fairly and efficiently assigned to available agents. It replaces the legacy assignment mechanism with a more sophisticated approach that considers multiple factors like agent capacity, availability, and conversation priority.
|
|
|
|
## Purpose
|
|
|
|
Instead of conversations sitting unassigned or being manually assigned one-by-one, Assignment v2 automatically distributes them across your team based on configurable rules and policies. This ensures:
|
|
|
|
- **No conversations are left unassigned** — Open conversations get assigned to agents automatically
|
|
- **Fair workload distribution** — Conversations are spread evenly across the team
|
|
- **Respects agent capacity** — Agents don't get overloaded with too many conversations
|
|
- **Flexible prioritization** — You control how conversations are prioritized (oldest first vs. longest waiting)
|
|
|
|
---
|
|
|
|
## How It Works: The Auto-Assignment Flow
|
|
|
|
Assignment v2 operates through two parallel mechanisms:
|
|
|
|
### 1. Real-time Assignment Trigger (AutoAssignmentHandler)
|
|
|
|
The system monitors conversation changes in real-time through the `AutoAssignmentHandler` concern:
|
|
|
|
- **Triggers on**: Every conversation save operation
|
|
- **Assignment occurs when**:
|
|
- A conversation transitions to `status: 'open'` with no assignee
|
|
- A conversation has an assignee who is no longer an inbox member
|
|
- **Action**: Immediately enqueues an `AutoAssignment::AssignmentJob` for that inbox
|
|
- **Fallback**: If assignment_v2 is not enabled, falls back to legacy `AgentAssignmentService`
|
|
|
|
This ensures conversations get assigned immediately when they become eligible, without waiting for the periodic job.
|
|
|
|
### 2. Periodic Assignment Cycle
|
|
|
|
The system also runs a **30-minute cycle** through an automated job (`AutoAssignment::PeriodicAssignmentJob`) configured in `config/schedule.yml`:
|
|
|
|
1. **Discovery Phase**: The system checks all accounts in batches
|
|
- Only accounts with the `assignment_v2` feature flag enabled are processed
|
|
- Uses `find_in_batches` for memory efficiency with large datasets
|
|
|
|
2. **Queue Phase**: For each inbox in those accounts:
|
|
- Only inboxes with an `assignment_policy` linked are considered
|
|
- The inbox must have `enable_auto_assignment` set to true
|
|
- The linked assignment policy must be enabled
|
|
- An `AutoAssignment::AssignmentJob` is queued for each eligible inbox
|
|
|
|
3. **Processing Phase**: Each assignment job:
|
|
- Fetches up to 100 unassigned, open conversations (configurable via `AUTO_ASSIGNMENT_BULK_LIMIT` env var)
|
|
- Processes conversations one at a time in the order determined by the priority policy
|
|
|
|
4. **Assignment Phase**: For each conversation, the system:
|
|
- Checks if the conversation is assignable (open status, no current assignee)
|
|
- Finds agents who are available (online status) and have capacity
|
|
- Filters agents by rate limiting rules
|
|
- Selects one agent using the configured selection strategy
|
|
- Assigns the conversation to that agent
|
|
- Tracks the assignment in Redis for rate limiting
|
|
- Dispatches an `ASSIGNEE_CHANGED` event
|
|
|
|
### Key Architectural Details
|
|
|
|
- **Job Queues**:
|
|
- `PeriodicAssignmentJob` runs in the `scheduled_jobs` queue
|
|
- `AssignmentJob` runs in the `default` queue
|
|
|
|
- **Batched Processing**: Uses `find_in_batches` for accounts and inboxes to handle large-scale deployments efficiently
|
|
|
|
- **Error Handling**:
|
|
- Errors are logged with the inbox ID
|
|
- In test environments, errors are re-raised for debugging
|
|
- In production, errors don't crash the entire job cycle
|
|
|
|
- **Conversation Limits**: Default is 100 conversations per job run, preventing memory issues and long-running jobs
|
|
|
|
---
|
|
|
|
## Prerequisites & Feature Enablement
|
|
|
|
Assignment v2 requires **all** of the following conditions to be met:
|
|
|
|
1. **Account-Level Feature Flag**: The account must have the `assignment_v2` feature enabled (configured in `config/features.yml`)
|
|
2. **Assignment Policy**: An `AssignmentPolicy` must exist and be linked to the inbox via `InboxAssignmentPolicy`
|
|
3. **Policy Enabled**: The `AssignmentPolicy` must have `enabled: true` (acts as a soft switch)
|
|
4. **Inbox Auto-Assignment**: The inbox must have `enable_auto_assignment: true`
|
|
|
|
If any of these conditions is not met, the inbox is skipped entirely and no assignments occur. The system checks these conditions via the `Inbox#auto_assignment_v2_enabled?` method.
|
|
|
|
---
|
|
|
|
## Assignment Policies
|
|
|
|
Policies determine HOW conversations are selected and assigned. Here are the main policies:
|
|
|
|
### 1. Conversation Priority Policy
|
|
|
|
**What it does**: Controls which conversations get assigned first
|
|
|
|
**Options**:
|
|
|
|
- **Longest Waiting Mode** (`longest_waiting`):
|
|
- Prioritizes conversations based on `last_activity_at` (oldest first)
|
|
- Uses `last_activity_at ASC, created_at ASC` ordering
|
|
- Ensures customers waiting longest for a response get priority
|
|
- Ideal for support teams focused on response time SLAs
|
|
|
|
- **Default Mode** (or any other value):
|
|
- Conversations are assigned in the order they were created
|
|
- Uses `created_at ASC` ordering
|
|
- First In, First Out (FIFO) approach
|
|
- Ideal for teams that want to clear backlogs chronologically
|
|
|
|
**Example**:
|
|
```
|
|
Conversation A: Created 2 hours ago, last activity 2 hours ago
|
|
Conversation B: Created 1 hour ago, last activity 30 minutes ago
|
|
Conversation C: Created 30 minutes ago, last activity 30 minutes ago
|
|
|
|
Longest Waiting: A → B → C (prioritizes least recent activity)
|
|
Default: A → B → C (prioritizes creation order)
|
|
```
|
|
|
|
### 2. Fair Distribution / Rate Limiting Policy
|
|
|
|
**What it does**: Prevents any single agent from being overwhelmed by too many assignments in a short time window
|
|
|
|
**Configuration**:
|
|
- `fair_distribution_limit`: Maximum number of assignments per agent within the time window (integer)
|
|
- `fair_distribution_window`: Time window in seconds (integer, defaults to 3600 seconds / 1 hour)
|
|
|
|
**How it works**:
|
|
- When enabled, the system tracks each assignment in Redis
|
|
- Redis key pattern: `chatwoot:assignment:{inbox_id}:{agent_id}:{conversation_id}`
|
|
- Each key has a TTL (Time To Live) equal to the configured window
|
|
- Before assigning, the system counts existing keys for that agent
|
|
- If count >= limit, the agent is filtered out from eligible agents
|
|
- After assignment, a new Redis key is created with the window TTL
|
|
|
|
**Behavior**:
|
|
- If `fair_distribution_limit` is not set or is 0, rate limiting is disabled
|
|
- Rate limiting is applied **per inbox per agent**
|
|
- Once the time window expires, old assignments are automatically removed from Redis
|
|
- Agents become eligible again once their count drops below the limit
|
|
|
|
**Example**:
|
|
```
|
|
Configuration: limit=5, window=3600 (1 hour)
|
|
|
|
Agent A is assigned 5 conversations between 10:00-10:15am
|
|
At 10:16am, Agent A is filtered out (at limit)
|
|
At 11:01am (after the first assignment expires), Agent A is eligible again
|
|
```
|
|
|
|
**Edge Cases**:
|
|
- If ALL agents hit their rate limit, no assignments occur (0 assignments returned)
|
|
- Rate limits are checked AFTER agent availability but BEFORE capacity checks
|
|
- Rate limiting tracks assignments, not current workload (different from capacity)
|
|
|
|
### 3. Agent Availability Policy
|
|
|
|
**What it does**: Ensures only agents who are actually available get new conversations
|
|
|
|
**How it works**:
|
|
- Uses `OnlineStatusTracker` to check real-time agent status
|
|
- Fetches all users with their status from Redis
|
|
- Only agents with status exactly equal to `'online'` are eligible
|
|
- Agents with status `'busy'`, `'offline'`, or any other value are filtered out
|
|
|
|
**Integration**:
|
|
- The system calls `inbox.available_agents` which:
|
|
1. Fetches online agent IDs from `OnlineStatusTracker`
|
|
2. Filters inbox members to only those with online status
|
|
3. Returns `InboxMember` records (not `User` records)
|
|
|
|
**Edge Cases**:
|
|
- If no agents are online for an inbox, 0 assignments occur
|
|
- Agent availability is checked at the time of assignment (not when the job starts)
|
|
- If an agent goes offline mid-job, they won't receive more assignments
|
|
|
|
**Important**: The system does NOT check:
|
|
- Agent's working hours
|
|
- Agent's timezone
|
|
- Agent's custom availability settings (these would need to be handled separately)
|
|
|
|
### 4. Capacity Management Policy (Enterprise Feature)
|
|
|
|
**What it does**: Advanced workload balancing based on agent's current load and conversation history
|
|
|
|
**How it works**:
|
|
- Each agent can be assigned to an `AgentCapacityPolicy`
|
|
- The policy defines `InboxCapacityLimit` records (one per inbox)
|
|
- Each limit specifies a `conversation_limit` (integer)
|
|
- The system counts the agent's current **open** conversations in that specific inbox
|
|
- If `current_count >= conversation_limit`, the agent is filtered out
|
|
|
|
**Key Characteristics**:
|
|
- **Inbox-specific**: Limits apply per inbox, so an agent can have different limits for different inboxes
|
|
- **Open conversations only**: Only counts conversations with `status: 'open'`
|
|
- **Optional**: If an agent has no `AgentCapacityPolicy`, they have unlimited capacity
|
|
- **Per-inbox limits**: If a capacity policy exists but has no `InboxCapacityLimit` for a specific inbox, the agent has unlimited capacity for that inbox
|
|
|
|
**Example**:
|
|
```
|
|
Agent A has a capacity policy with:
|
|
- Inbox 1 (Support): limit = 10
|
|
- Inbox 2 (Sales): limit = 20
|
|
|
|
Agent A currently has:
|
|
- 10 open conversations in Inbox 1
|
|
- 5 open conversations in Inbox 2
|
|
|
|
Result:
|
|
- Agent A is at capacity for Inbox 1 (10 >= 10) → filtered out
|
|
- Agent A has capacity for Inbox 2 (5 < 20) → eligible
|
|
```
|
|
|
|
**Edge Cases**:
|
|
- Resolved/closed conversations do NOT count toward capacity
|
|
- Capacity is checked at assignment time, not continuously
|
|
- If an agent goes over capacity manually (outside auto-assignment), they're still filtered out
|
|
|
|
### 5. Exclusion Rules (Enterprise Feature)
|
|
|
|
**What it does**: Allows excluding certain conversations from auto-assignment based on labels or age
|
|
|
|
**Configuration** (on `AgentCapacityPolicy`):
|
|
```ruby
|
|
exclusion_rules = {
|
|
'excluded_labels' => ['VIP', 'Escalation', 'Manual'],
|
|
'exclude_older_than_hours' => 24
|
|
}
|
|
```
|
|
|
|
**Behavior**:
|
|
|
|
**Label-Based Exclusions**:
|
|
- Conversations tagged with any of the excluded labels are skipped
|
|
- Uses the `tagged_with` method with `exclude: true`
|
|
- Multiple labels are treated as OR (conversation with ANY excluded label is skipped)
|
|
- Label matching is exact (case-sensitive)
|
|
|
|
**Age-Based Exclusions**:
|
|
- Conversations older than the specified hours are skipped
|
|
- Age is calculated from `created_at`, not `last_activity_at`
|
|
- Uses `where('conversations.created_at >= ?', hours.hours.ago)`
|
|
- If set to 24, conversations created more than 24 hours ago are excluded
|
|
|
|
**Combined Exclusions**:
|
|
- Both rules are applied together (AND logic)
|
|
- A conversation must pass both checks to be eligible
|
|
- Example: Must be less than 24 hours old AND not have an excluded label
|
|
|
|
**Edge Cases**:
|
|
- If `exclusion_rules` is nil or empty, no exclusions apply
|
|
- If there's no capacity policy linked to the inbox, exclusion rules don't apply
|
|
- Exclusions are applied BEFORE agent selection (reduces conversations to assign)
|
|
|
|
---
|
|
|
|
## Selection Strategies
|
|
|
|
Once eligible agents are identified (after availability, rate limiting, and capacity checks), the system uses a selection strategy to pick the agent:
|
|
|
|
### Round-Robin (Default)
|
|
|
|
**Implementation**: Uses `AutoAssignment::RoundRobinSelector` which delegates to `AutoAssignment::InboxRoundRobinService`
|
|
|
|
**How it works**:
|
|
- Maintains a Redis-based queue of agent IDs for the inbox
|
|
- Each time an agent is selected, they're moved to the back of the queue
|
|
- The next agent in the queue is always selected
|
|
- Ensures even distribution over time
|
|
|
|
**Characteristics**:
|
|
- **Fair over time**: Each agent gets an equal turn
|
|
- **Stateful**: Uses Redis to maintain queue state across jobs
|
|
- **Inbox-specific**: Each inbox has its own round-robin queue
|
|
|
|
**Example**:
|
|
```
|
|
Queue: [Agent A, Agent B, Agent C]
|
|
|
|
Assignment 1: Agent A (queue becomes [Agent B, Agent C, Agent A])
|
|
Assignment 2: Agent B (queue becomes [Agent C, Agent A, Agent B])
|
|
Assignment 3: Agent C (queue becomes [Agent A, Agent B, Agent C])
|
|
```
|
|
|
|
**Edge Cases**:
|
|
- If an agent is removed from the inbox, they're removed from the queue
|
|
- If a new agent is added, they're added to the queue
|
|
- The queue is validated and reset if it becomes inconsistent (membership drift)
|
|
- The `InboxRoundRobinService` validates the queue and resets it when agent membership changes
|
|
|
|
### Balanced / Workload-Based (Enterprise)
|
|
|
|
**Implementation**: Uses `Enterprise::AutoAssignment::BalancedSelector`
|
|
|
|
**How it works**:
|
|
- For each eligible agent, counts their current **open** conversations in the inbox
|
|
- Selects the agent with the **minimum** count
|
|
- Uses `min_by` which returns the first agent if there's a tie
|
|
|
|
**Characteristics**:
|
|
- **Real-time balancing**: Based on current workload, not historical assignments
|
|
- **Inbox-specific**: Only counts conversations in the specific inbox
|
|
- **Open conversations only**: Ignores resolved/closed conversations
|
|
- **Prioritizes new agents**: Agents with 0 conversations are always selected first
|
|
|
|
**Example**:
|
|
```
|
|
Agent A: 5 open conversations
|
|
Agent B: 3 open conversations
|
|
Agent C: 8 open conversations
|
|
|
|
Selection: Agent B (has the least workload)
|
|
|
|
After assignment:
|
|
Agent A: 5, Agent B: 4, Agent C: 8
|
|
Next selection: Agent B again (still has least)
|
|
```
|
|
|
|
**Comparison**:
|
|
|
|
| Aspect | Round-Robin | Balanced |
|
|
|--------|-------------|----------|
|
|
| **Goal** | Equal distribution over time | Equal workload at any moment |
|
|
| **State** | Redis queue | Database query |
|
|
| **Speed** | Very fast (Redis lookup) | Slower (DB count query) |
|
|
| **Accuracy** | Equal turns, not equal workload | Equal workload |
|
|
| **Best for** | High-volume, fast assignments | Teams where agents have varying resolve rates |
|
|
|
|
**Configuration**:
|
|
- In OSS: Always uses Round-Robin
|
|
- In Enterprise: Can set `balanced: true` on the assignment policy to use Balanced strategy
|
|
|
|
---
|
|
|
|
## Important Behaviors & Edge Cases
|
|
|
|
### Conversation Eligibility
|
|
|
|
A conversation is only eligible for auto-assignment if ALL of the following are true:
|
|
|
|
1. **Status is 'open'**: Resolved, pending, or snoozed conversations are never assigned
|
|
2. **No current assignee**: Conversations with an existing assignee are never reassigned
|
|
3. **Passes exclusion rules** (if Enterprise): Must not have excluded labels and must be within age threshold
|
|
4. **In an eligible inbox**: The inbox must have auto-assignment v2 enabled
|
|
|
|
**Edge Cases**:
|
|
- If a conversation is manually assigned mid-job, it's skipped (already has assignee)
|
|
- If a conversation is resolved mid-job, it's skipped (no longer open)
|
|
- Assignment v2 NEVER reassigns conversations (even if the current agent goes offline)
|
|
|
|
### Agent Eligibility
|
|
|
|
An agent is only eligible for assignment if ALL of the following are true:
|
|
|
|
1. **Member of the inbox**: Agent must be an `InboxMember`
|
|
2. **Status is 'online'**: Checked via `OnlineStatusTracker` at assignment time
|
|
3. **Within rate limit** (if configured): Agent hasn't exceeded assignments in the current window
|
|
4. **Has capacity** (if Enterprise): Agent's open conversation count is below their limit for this inbox
|
|
|
|
**Edge Cases**:
|
|
- If an agent meets all criteria but goes offline between the eligibility check and assignment, they may still receive the conversation (race condition)
|
|
- If ALL agents are filtered out, the conversation remains unassigned until the next cycle
|
|
|
|
### Zero-Assignment Scenarios
|
|
|
|
The system may assign 0 conversations if:
|
|
|
|
1. No unassigned, open conversations exist in the inbox
|
|
2. All conversations are excluded by exclusion rules
|
|
3. No agents are online
|
|
4. All online agents are at their rate limit
|
|
5. All online agents are at capacity (Enterprise)
|
|
6. The assignment policy is disabled mid-job
|
|
|
|
### Assignment Events
|
|
|
|
When a conversation is successfully assigned, the system:
|
|
|
|
1. Updates the conversation's `assignee_id` in the database
|
|
2. Creates a Redis key to track the assignment (for rate limiting)
|
|
3. Dispatches an `Events::Types::ASSIGNEE_CHANGED` event with:
|
|
- `conversation`: The conversation object
|
|
- `user`: The assigned agent
|
|
- Timestamp of the assignment
|
|
|
|
**Event Integration**:
|
|
- Webhooks subscribed to `ASSIGNEE_CHANGED` will be triggered
|
|
- The conversation model also dispatches a `conversation.updated` event
|
|
- These events can be used for notifications, integrations, or analytics
|
|
|
|
### Error Handling
|
|
|
|
**Job-Level Errors**:
|
|
- If an `AssignmentJob` encounters an error, it logs the error with the inbox ID
|
|
- In production, the error is caught and logged, allowing other jobs to continue
|
|
- In test environment, errors are re-raised for debugging
|
|
|
|
**Service-Level Errors**:
|
|
- If a single conversation assignment fails, it's skipped and the job continues
|
|
- The `assigned_count` only includes successful assignments
|
|
- Database errors (like constraint violations) are logged but don't stop the job
|
|
|
|
**Graceful Degradation**:
|
|
- If Redis is unavailable, rate limiting is skipped (all assignments proceed)
|
|
- If OnlineStatusTracker fails, no agents are considered online (0 assignments)
|
|
- If the inbox is deleted mid-job, the job exits early
|
|
|
|
### Performance Considerations
|
|
|
|
**Batch Processing**:
|
|
- Accounts are processed in batches to avoid loading all accounts into memory
|
|
- Inboxes are processed in batches to avoid loading all inboxes into memory
|
|
- Conversations are limited to 100 per job to prevent long-running jobs
|
|
|
|
**Redis Usage**:
|
|
- Rate limiting creates one Redis key per assignment
|
|
- Keys automatically expire based on the configured window
|
|
- Pattern matching (`keys_count`) is used to count assignments (can be expensive with many agents)
|
|
|
|
**Database Queries**:
|
|
- Uses `includes(:user)` in `InboxAgentAvailability#available_agents` to preload users and avoid N+1 queries
|
|
- Falls back to `inbox_members.none` when no agents are online to avoid unnecessary database hits
|
|
- Uses `group(:assignee_id).count` for balanced selector (efficient aggregation)
|
|
- Uses `limit(100)` to prevent loading thousands of conversations at once
|
|
|
|
**Scaling Recommendations**:
|
|
- For high-volume inboxes, increase `AUTO_ASSIGNMENT_BULK_LIMIT` (e.g., 200-500)
|
|
- Consider shorter rate limiting windows for faster cycling (e.g., 1800 seconds instead of 3600)
|
|
- Use balanced selector for teams where workload balance is critical
|
|
|
|
---
|
|
|
|
## API Endpoints
|
|
|
|
Assignment v2 provides REST API endpoints for managing assignment policies:
|
|
|
|
### Policy Management
|
|
- **CRUD Operations**: `Api::V1::Accounts::AssignmentPoliciesController`
|
|
- `GET /api/v1/accounts/:account_id/assignment_policies` - List all policies
|
|
- `POST /api/v1/accounts/:account_id/assignment_policies` - Create a policy
|
|
- `PUT /api/v1/accounts/:account_id/assignment_policies/:id` - Update a policy
|
|
- `DELETE /api/v1/accounts/:account_id/assignment_policies/:id` - Delete a policy
|
|
- **Accepts**: `assignment_order`, `conversation_priority`, `fair_distribution_limit`, `fair_distribution_window`, `enabled`
|
|
|
|
### Inbox-Policy Linking
|
|
- **Link/Unlink**: `Api::V1::Accounts::Inboxes::AssignmentPoliciesController`
|
|
- `POST /api/v1/accounts/:account_id/inboxes/:inbox_id/assignment_policy` - Attach policy to inbox
|
|
- `DELETE /api/v1/accounts/:account_id/inboxes/:inbox_id/assignment_policy` - Detach policy from inbox
|
|
- **Note**: Only one policy can be linked to an inbox at a time
|
|
|
|
### Policy-Inbox Queries
|
|
- **List Linked Inboxes**: `Api::V1::Accounts::AssignmentPolicies::InboxesController`
|
|
- `GET /api/v1/accounts/:account_id/assignment_policies/:policy_id/inboxes` - List all inboxes using a policy
|
|
|
|
### Authorization
|
|
- All endpoints require proper account scoping
|
|
- Requires `AssignmentPolicy` authorization for the requesting user
|
|
|
|
---
|
|
|
|
## Configuration & Flexibility
|
|
|
|
Assignment v2 is designed to be flexible at multiple levels:
|
|
|
|
### Account Level
|
|
- **Feature Flag**: `assignment_v2` must be enabled on the account
|
|
- **Enterprise Features**: Capacity and exclusion rules require Enterprise
|
|
|
|
### Policy Level
|
|
- **Assignment Order**: `round_robin` (default) or `balanced` (Enterprise only)
|
|
- **Conversation Priority**: `longest_waiting` or `earliest_created` (default/FIFO)
|
|
- **Fair Distribution Limit**: Integer (e.g., 5)
|
|
- **Fair Distribution Window**: Integer in seconds (defaults to 3600 when omitted)
|
|
- **Enabled**: Boolean soft switch to enable/disable the policy
|
|
|
|
### Inbox Level
|
|
- **Auto-Assignment**: `enable_auto_assignment` boolean
|
|
- **Policy Link**: `InboxAssignmentPolicy` links inbox to a policy (one-to-one relationship)
|
|
- **Capacity Limits** (Enterprise): Per-inbox conversation limits
|
|
- **Configuration Overrides**:
|
|
- **OSS**: The `inbox.auto_assignment_config` JSONB field can override policy settings for per-inbox customization
|
|
- **Enterprise**: Policy settings take precedence over `inbox.auto_assignment_config` - edit the policy directly
|
|
|
|
### Agent Level
|
|
- **Capacity Policy** (Enterprise): Optional `AgentCapacityPolicy` per agent
|
|
- **Availability**: Real-time online status tracked by `OnlineStatusTracker`
|
|
|
|
### Environment Level
|
|
- **Bulk Limit**: `AUTO_ASSIGNMENT_BULK_LIMIT` env var (default: 100)
|
|
- **Job Schedule**: Configurable via job scheduler (default: 30 minutes)
|
|
|
|
---
|
|
|
|
## System Architecture
|
|
|
|
### Components
|
|
|
|
1. **Jobs**:
|
|
- `AutoAssignment::PeriodicAssignmentJob`: Scheduled job that discovers eligible inboxes
|
|
- `AutoAssignment::AssignmentJob`: Per-inbox job that performs assignments
|
|
|
|
2. **Services**:
|
|
- `AutoAssignment::AssignmentService`: Core assignment logic (OSS)
|
|
- `Enterprise::AutoAssignment::AssignmentService`: Extended with capacity and exclusion rules
|
|
- `AutoAssignment::RateLimiter`: Rate limiting logic using Redis
|
|
- `Enterprise::AutoAssignment::CapacityService`: Capacity checking logic
|
|
|
|
3. **Selectors**:
|
|
- `AutoAssignment::RoundRobinSelector`: Round-robin selection strategy
|
|
- `Enterprise::AutoAssignment::BalancedSelector`: Workload-based selection strategy
|
|
|
|
4. **Models**:
|
|
- `AssignmentPolicy`: Configures assignment behavior
|
|
- `InboxAssignmentPolicy`: Links inbox to policy
|
|
- `AgentCapacityPolicy` (Enterprise): Defines capacity rules
|
|
- `InboxCapacityLimit` (Enterprise): Per-inbox limits
|
|
|
|
5. **Concerns**:
|
|
- `InboxAgentAvailability`: Provides `available_agents` method for inboxes
|
|
|
|
### Data Flow
|
|
|
|
```
|
|
1. PeriodicAssignmentJob (every 30 min)
|
|
↓
|
|
2. For each Account with assignment_v2
|
|
↓
|
|
3. For each Inbox with auto_assignment_v2_enabled?
|
|
↓
|
|
4. Queue AssignmentJob(inbox_id)
|
|
↓
|
|
5. AssignmentService.perform_bulk_assignment(limit: 100)
|
|
↓
|
|
6. Fetch unassigned conversations (with priority ordering)
|
|
↓
|
|
7. For each conversation:
|
|
a. Check assignable? (open + no assignee)
|
|
b. Find available agents (online + rate limit + capacity)
|
|
c. Select agent (round-robin or balanced)
|
|
d. Assign conversation
|
|
e. Track in Redis
|
|
f. Dispatch event
|
|
↓
|
|
8. Return assigned_count
|
|
```
|
|
|
|
### Redis Keys
|
|
|
|
**Rate Limiting**:
|
|
- Pattern: `chatwoot:assignment:{inbox_id}:{agent_id}:*`
|
|
- Key: `chatwoot:assignment:{inbox_id}:{agent_id}:{conversation_id}`
|
|
- TTL: Equal to `fair_distribution_window` (default 3600 seconds)
|
|
|
|
**Round-Robin Queue**:
|
|
- Key: `chatwoot:round_robin:{inbox_id}`
|
|
- Type: List (LPUSH/RPOP operations)
|
|
- Persistent (no TTL)
|
|
|
|
---
|
|
|
|
## Advantages Over Manual Assignment
|
|
|
|
| Aspect | Manual | Assignment v2 |
|
|
|--------|--------|---|
|
|
| **Speed** | Slow (requires human action) | Instant (automatic every 30 min) |
|
|
| **Consistency** | Variable (depends on person) | Consistent (follows rules) |
|
|
| **Fairness** | Prone to bias | Fair and data-driven |
|
|
| **Scalability** | Doesn't scale (1 person = bottleneck) | Scales infinitely |
|
|
| **Workload Balance** | Hard to maintain | Automatically balanced |
|
|
| **Priority** | Manual judgment | Configurable policies |
|
|
| **24/7 Operation** | Requires shifts | Automated |
|
|
| **Onboarding** | New agents may be overlooked | New agents automatically included |
|
|
| **Audit Trail** | Limited | Full event log |
|
|
|
|
---
|
|
|
|
## Integration Points
|
|
|
|
Assignment v2 integrates with several Chatwoot systems:
|
|
|
|
1. **Conversation Model**:
|
|
- Reads: `status`, `assignee_id`, `last_activity_at`, `created_at`
|
|
- Writes: `assignee_id`
|
|
- Scopes: `unassigned`, `open`
|
|
|
|
2. **OnlineStatusTracker**:
|
|
- Reads: Real-time agent online status from Redis
|
|
- Returns: Hash of `{user_id => status}`
|
|
|
|
3. **Redis**:
|
|
- Rate limiting: Stores assignment keys with TTL
|
|
- Round-robin: Maintains agent queues
|
|
- Performance: All Redis operations are non-blocking
|
|
|
|
4. **Events System**:
|
|
- Dispatches: `Events::Types::ASSIGNEE_CHANGED`
|
|
- Consumers: Webhooks, notifications, analytics
|
|
|
|
5. **Assignment Policy**:
|
|
- Reads: Priority, limits, windows, balanced mode
|
|
- Links: Via `InboxAssignmentPolicy`
|
|
|
|
6. **Capacity Policy** (Enterprise):
|
|
- Reads: Per-inbox limits, exclusion rules
|
|
- Links: Via `AccountUser` and `InboxCapacityLimit`
|
|
|
|
7. **Labels** (Enterprise):
|
|
- Reads: Conversation labels for exclusion rules
|
|
- Uses: ActsAsTaggableOn gem for label filtering
|
|
|
|
---
|
|
|
|
## For Non-Technical Stakeholders
|
|
|
|
Think of Assignment v2 as an intelligent dispatcher at a taxi company:
|
|
|
|
### Without Assignment v2
|
|
- Customers (conversations) call in and wait on hold
|
|
- Someone manually decides which driver (agent) should take each ride
|
|
- Some drivers get overloaded while others sit idle
|
|
- Customers who called first might not get picked up first
|
|
- The dispatcher becomes a bottleneck
|
|
|
|
### With Assignment v2
|
|
- The system automatically dispatches rides to available drivers
|
|
- Drivers who are online and not at capacity get new rides
|
|
- The system balances workload so no driver gets too many rides at once
|
|
- You can set rules: "Prioritize customers waiting longest" or "No driver gets more than 5 rides per hour"
|
|
- Drivers with VIP rides (excluded labels) can be handled separately
|
|
- Everything happens automatically every 30 minutes
|
|
|
|
### Benefits
|
|
- **Faster**: Customers get assigned immediately
|
|
- **Fairer**: All drivers get equal opportunities
|
|
- **Scalable**: Works with 5 drivers or 500
|
|
- **Configurable**: Rules can be adjusted based on business needs
|
|
- **Reliable**: Runs 24/7 without human intervention
|
|
|
|
---
|
|
|
|
## Troubleshooting
|
|
|
|
### "Conversations aren't being assigned"
|
|
|
|
Check the following in order:
|
|
|
|
1. **Feature flag**: Is `assignment_v2` enabled for the account?
|
|
2. **Assignment policy**: Does the inbox have a linked, enabled assignment policy?
|
|
3. **Inbox setting**: Is `enable_auto_assignment` true for the inbox?
|
|
4. **Agent availability**: Are any agents online?
|
|
5. **Rate limiting**: Are all agents at their rate limit? Check Redis keys
|
|
6. **Capacity** (Enterprise): Are all agents at capacity? Check open conversation counts
|
|
7. **Exclusion rules** (Enterprise): Are all conversations being excluded by labels or age?
|
|
8. **Job running**: Is `PeriodicAssignmentJob` scheduled and running?
|
|
|
|
### "Assignments are uneven"
|
|
|
|
- If using round-robin: Check the Redis queue for the inbox
|
|
- If some agents never get assignments: Check their online status
|
|
- If using balanced selector: Check open conversation counts per agent
|
|
- If rate limiting is too strict: Increase the limit or window
|
|
|
|
### "Too many/too few assignments"
|
|
|
|
- Check `AUTO_ASSIGNMENT_BULK_LIMIT` (default 100)
|
|
- Check rate limiting configuration
|
|
- Check capacity limits (Enterprise)
|
|
- Check conversation priority (longest_waiting vs. FIFO)
|
|
|
|
### "Redis keys not expiring"
|
|
|
|
- Verify `fair_distribution_window` is set correctly
|
|
- Check Redis configuration for key eviction policies
|
|
- Manually inspect keys: `Redis::Alfred.keys('chatwoot:assignment:*')`
|
|
|
|
---
|
|
|
|
## Testing Guidelines
|
|
|
|
### Test Coverage Touchpoints
|
|
|
|
**Core Spec Files**:
|
|
- `spec/services/auto_assignment/assignment_service_spec.rb` - Verifies open-only assignment, respect for limits, conversation priority, fair distribution, and event dispatching
|
|
- `spec/services/auto_assignment/rate_limiter_spec.rb` - Covers Redis key semantics, limit/window handling, and edge cases when configuration is absent
|
|
- `spec/services/auto_assignment/round_robin_selector_spec.rb` - Ensures proper Redis-backed round robin queue and handles empty agent pools
|
|
- `spec/jobs/auto_assignment/*` - Covers both immediate job and periodic scheduler behavior
|
|
- `spec/enterprise/services/enterprise/auto_assignment/*` - Asserts balanced selection, capacity filtering, and exclusion rule behavior
|
|
|
|
### Key Test Scenarios
|
|
|
|
1. **Basic Assignment**:
|
|
- Create unassigned, open conversation
|
|
- Create online agent
|
|
- Run assignment service
|
|
- Verify conversation is assigned
|
|
|
|
2. **Rate Limiting**:
|
|
- Configure rate limit (e.g., 2 per hour)
|
|
- Assign 2 conversations to an agent
|
|
- Verify 3rd conversation goes to a different agent
|
|
|
|
3. **Capacity Management**:
|
|
- Set capacity limit (e.g., 5 conversations)
|
|
- Create 5 open conversations for an agent
|
|
- Verify agent is filtered out of eligibility
|
|
|
|
4. **Exclusion Rules**:
|
|
- Configure excluded labels
|
|
- Create conversation with excluded label
|
|
- Verify conversation is not assigned
|
|
|
|
5. **Priority**:
|
|
- Create multiple conversations with different `last_activity_at`
|
|
- Configure `longest_waiting` priority
|
|
- Verify oldest `last_activity_at` is assigned first
|
|
|
|
6. **Agent Availability**:
|
|
- Set agent status to 'busy' or 'offline'
|
|
- Verify agent is not eligible
|
|
- Set status to 'online'
|
|
- Verify agent is now eligible
|
|
|
|
7. **Zero Assignments**:
|
|
- Disable assignment policy
|
|
- Verify 0 assignments occur
|
|
- Set all agents offline
|
|
- Verify 0 assignments occur
|
|
|
|
---
|
|
|
|
## Next Steps / Additional Reading
|
|
|
|
- To enable Assignment v2 for your account, contact your Chatwoot administrator
|
|
- For Enterprise features, see the Enterprise-specific capacity policies documentation
|
|
- For webhook integrations, refer to the Assignment Events documentation
|
|
- For performance tuning, consult the Scaling Chatwoot guide
|
|
|
|
---
|
|
|
|
**Last Updated**: November 2024
|
|
**Feature Status**: Stable (v2 final)
|
|
**Related PR**: #12320
|
|
**Architecture**: Modular with Enterprise extensions via `prepend_mod_with`
|