use async_trait::async_trait;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
pub mod db;
pub mod http;
pub mod memory;
pub use db::DbRegistry;
pub use http::HttpRegistry;
pub use memory::MemoryRegistry;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum PresenceState {
Offline,
Away,
Available,
Ringing {
call_id: Option<String>,
},
Busy {
call_id: Option<String>,
},
Wrapup {
call_id: Option<String>,
},
Dnd,
Custom(String),
}
impl PresenceState {
pub fn can_receive_calls(&self) -> bool {
matches!(self, PresenceState::Available)
}
pub fn is_call_active(&self) -> bool {
matches!(
self,
PresenceState::Ringing { .. } | PresenceState::Busy { .. }
)
}
pub fn is_custom(&self) -> bool {
matches!(self, PresenceState::Custom(_))
}
pub fn custom_name(&self) -> Option<&str> {
match self {
PresenceState::Custom(name) => Some(name),
_ => None,
}
}
pub fn as_str(&self) -> String {
match self {
PresenceState::Offline => "offline".to_string(),
PresenceState::Away => "away".to_string(),
PresenceState::Available => "available".to_string(),
PresenceState::Ringing { .. } => "ringing".to_string(),
PresenceState::Busy { .. } => "busy".to_string(),
PresenceState::Wrapup { .. } => "wrapup".to_string(),
PresenceState::Dnd => "dnd".to_string(),
PresenceState::Custom(name) => format!("custom:{}", name),
}
}
pub fn parse_state(s: &str) -> Option<Self> {
match s {
"offline" => Some(PresenceState::Offline),
"away" => Some(PresenceState::Away),
"available" => Some(PresenceState::Available),
"ringing" => Some(PresenceState::Ringing { call_id: None }),
"busy" => Some(PresenceState::Busy { call_id: None }),
"wrapup" => Some(PresenceState::Wrapup { call_id: None }),
"dnd" => Some(PresenceState::Dnd),
_ => {
if let Some(custom_name) = s.strip_prefix("custom:") {
if !custom_name.is_empty() {
Some(PresenceState::Custom(custom_name.to_string()))
} else {
None
}
} else {
None
}
}
}
}
pub fn display_name(&self) -> String {
match self {
PresenceState::Offline => "Offline".to_string(),
PresenceState::Away => "Away".to_string(),
PresenceState::Available => "Available".to_string(),
PresenceState::Ringing { .. } => "Ringing".to_string(),
PresenceState::Busy { .. } => "Busy".to_string(),
PresenceState::Wrapup { .. } => "Wrap-up".to_string(),
PresenceState::Dnd => "Do Not Disturb".to_string(),
PresenceState::Custom(name) => name.clone(),
}
}
}
#[derive(Debug, Clone)]
pub struct AgentRecord {
pub agent_id: String,
pub display_name: String,
pub uri: String, pub skills: Vec<String>,
pub max_concurrency: u32,
pub current_calls: u32,
pub presence: PresenceState,
pub last_state_change: Instant,
pub total_calls_handled: u64,
pub total_talk_time_secs: u64,
pub last_call_end: Option<Instant>,
pub custom_data: HashMap<String, String>,
}
pub type AgentEventHandler = Box<dyn Fn(&AgentRecord) + Send + Sync>;
impl AgentRecord {
pub fn has_capacity(&self) -> bool {
self.current_calls < self.max_concurrency && self.presence.can_receive_calls()
}
pub fn has_skills(&self, required: &[String]) -> bool {
if required.is_empty() {
return true;
}
required.iter().all(|skill| self.skills.contains(skill))
}
pub fn idle_duration(&self) -> Duration {
match self.last_call_end {
Some(t) => t.elapsed(),
None => Duration::from_secs(u64::MAX), }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum RoutingStrategy {
#[default]
LongestIdle,
RoundRobin,
SkillBased,
LeastCalls,
External,
}
impl RoutingStrategy {
pub fn as_str(&self) -> &'static str {
match self {
RoutingStrategy::LongestIdle => "longest_idle",
RoutingStrategy::RoundRobin => "round_robin",
RoutingStrategy::SkillBased => "skill_based",
RoutingStrategy::LeastCalls => "least_calls",
RoutingStrategy::External => "external",
}
}
}
#[async_trait]
pub trait AgentRegistry: Send + Sync {
async fn register(
&self,
agent_id: String,
display_name: String,
uri: String,
skills: Vec<String>,
max_concurrency: u32,
) -> anyhow::Result<()>;
async fn unregister(&self, agent_id: &str) -> anyhow::Result<()>;
async fn get_agent(&self, agent_id: &str) -> Option<AgentRecord>;
async fn list_agents(&self) -> Vec<AgentRecord>;
async fn update_presence(&self, agent_id: &str, new_state: PresenceState)
-> anyhow::Result<()>;
async fn start_call(&self, agent_id: &str) -> anyhow::Result<()>;
async fn end_call(&self, agent_id: &str, talk_time_secs: u64) -> anyhow::Result<()>;
async fn find_available_agents(&self, required_skills: &[String]) -> Vec<AgentRecord>;
async fn select_agent(
&self,
required_skills: &[String],
strategy: RoutingStrategy,
) -> Option<AgentRecord>;
async fn select_agent_with_policy(
&self,
required_skills: &[String],
strategy: RoutingStrategy,
_policy: Option<&str>,
) -> Option<AgentRecord> {
self.select_agent(required_skills, strategy).await
}
async fn resolve_target(&self, target_uri: &str) -> Vec<String>;
async fn resolve_target_with_policy(
&self,
target_uri: &str,
_policy: Option<&str>,
) -> Vec<String> {
self.resolve_target(target_uri).await
}
async fn get_acd_snapshots(&self) -> Vec<AgentRecord> {
self.find_available_agents(&[]).await
}
fn is_valid_transition(from: &PresenceState, to: &PresenceState) -> bool
where
Self: Sized,
{
match (from, to) {
(_, PresenceState::Offline) => true,
(
PresenceState::Away
| PresenceState::Wrapup { .. }
| PresenceState::Dnd
| PresenceState::Custom(_),
PresenceState::Available,
) => true,
(PresenceState::Available, PresenceState::Ringing { .. }) => true,
(PresenceState::Ringing { .. }, PresenceState::Busy { .. }) => true,
(PresenceState::Busy { .. }, PresenceState::Wrapup { .. }) => true,
(
PresenceState::Available | PresenceState::Custom(_),
PresenceState::Away | PresenceState::Dnd,
) => true,
(
PresenceState::Available
| PresenceState::Away
| PresenceState::Dnd
| PresenceState::Wrapup { .. },
PresenceState::Custom(_),
) => true,
(a, b) if a == b => true,
_ => false,
}
}
}
pub enum RegistryType {
Memory,
Db { connection_string: String },
Http {
base_url: String,
api_key: Option<String>,
},
}
impl RegistryType {
pub async fn create(&self) -> anyhow::Result<Arc<dyn AgentRegistry>> {
match self {
RegistryType::Memory => Ok(Arc::new(MemoryRegistry::new())),
RegistryType::Db { connection_string } => {
let db = sea_orm::Database::connect(connection_string).await?;
Ok(Arc::new(DbRegistry::new(db)))
}
RegistryType::Http { base_url, api_key } => Ok(Arc::new(HttpRegistry::new(
base_url.clone(),
api_key.clone(),
))),
}
}
}
pub fn select_best_agent(
mut candidates: Vec<AgentRecord>,
strategy: RoutingStrategy,
rr_counter: &mut u64,
) -> Option<AgentRecord> {
if candidates.is_empty() {
return None;
}
match strategy {
RoutingStrategy::LongestIdle => {
candidates.sort_by(|a, b| b.idle_duration().cmp(&a.idle_duration()));
candidates.into_iter().next()
}
RoutingStrategy::RoundRobin => {
let idx = (*rr_counter as usize) % candidates.len();
*rr_counter += 1;
Some(candidates.remove(idx))
}
RoutingStrategy::SkillBased => {
candidates.sort_by(|a, b| {
let a_matches = a.skills.len();
let b_matches = b.skills.len();
b_matches.cmp(&a_matches)
});
candidates.into_iter().next()
}
RoutingStrategy::LeastCalls => {
candidates.sort_by(|a, b| a.total_calls_handled.cmp(&b.total_calls_handled));
candidates.into_iter().next()
}
RoutingStrategy::External => {
candidates.into_iter().next()
}
}
}