use std::collections::VecDeque;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use async_trait::async_trait;
use serde_json::json;
use crate::brain::AgentBrain;
use crate::error::FlowcatError;
use crate::realtime::RealtimeLlm;
use crate::session::SessionSource;
use crate::transport::socket::MediaSocket;
use crate::types::{
AudioChunk, BrainAction, Finalize, RealtimeEvent, RealtimeSetup, ResolvedCall, ToolDecl,
UploadTarget, Usage, WsIn, WsOut,
};
use crate::realtime::RealtimeKickoff;
pub(crate) const CARRIER_RATE: u32 = 8_000;
pub(crate) const END_TOOL: &str = "end_call";
pub(crate) fn b64(bytes: &[u8]) -> String {
use base64::Engine as _;
base64::engine::general_purpose::STANDARD.encode(bytes)
}
pub(crate) fn plivo_start() -> WsIn {
WsIn::Text(
json!({
"event": "start",
"start": { "streamId": "strm-test", "callId": "call-test",
"mediaFormat": {"encoding": "audio/x-mulaw", "sampleRate": 8000} }
})
.to_string(),
)
}
pub(crate) fn plivo_media(n: usize) -> WsIn {
let pcm: Vec<i16> = (0..n).map(|i| ((i as i16 % 64) * 200) - 6400).collect();
let ulaw = crate::codec::pcm16_to_ulaw(&pcm);
WsIn::Text(json!({ "event": "media", "media": { "payload": b64(&ulaw) } }).to_string())
}
pub(crate) struct MockSocket {
ready: VecDeque<WsIn>,
delayed_stop: Option<WsIn>,
sent: Arc<Mutex<Vec<WsOut>>>,
}
impl MockSocket {
pub(crate) fn new(sent: Arc<Mutex<Vec<WsOut>>>) -> Self {
let mut ready = VecDeque::new();
ready.push_back(plivo_start());
ready.push_back(plivo_media(400));
ready.push_back(plivo_media(400));
Self {
ready,
delayed_stop: Some(WsIn::Text(json!({ "event": "stop" }).to_string())),
sent,
}
}
}
#[async_trait]
impl MediaSocket for MockSocket {
async fn recv(&mut self) -> Option<WsIn> {
if let Some(f) = self.ready.pop_front() {
return Some(f);
}
if let Some(stop) = self.delayed_stop.take() {
tokio::time::sleep(Duration::from_millis(200)).await;
return Some(stop);
}
std::future::pending::<()>().await;
None
}
async fn send_text(&mut self, s: String) -> Result<(), FlowcatError> {
self.sent.lock().unwrap().push(WsOut::Text(s));
Ok(())
}
async fn send_binary(&mut self, b: Vec<u8>) -> Result<(), FlowcatError> {
self.sent.lock().unwrap().push(WsOut::Binary(b));
Ok(())
}
}
pub(crate) struct MockRealtime {
connected: Arc<AtomicBool>,
kicked_off: Arc<AtomicBool>,
audio_received: Arc<Mutex<usize>>,
tool_results: Arc<Mutex<Vec<(String, String)>>>,
script: VecDeque<RealtimeEvent>,
}
impl MockRealtime {
pub(crate) fn new(
connected: Arc<AtomicBool>,
audio_received: Arc<Mutex<usize>>,
tool_results: Arc<Mutex<Vec<(String, String)>>>,
) -> Self {
let bot_pcm: Vec<i16> = (0..720).map(|i| ((i as i16 % 32) * 400) - 6400).collect();
let mut script = VecDeque::new();
script.push_back(RealtimeEvent::AudioOut(AudioChunk::new(bot_pcm, 24_000)));
script.push_back(RealtimeEvent::BotText("Hello, how can I help?".into()));
script.push_back(RealtimeEvent::UserInterimText("I'd ".into()));
script.push_back(RealtimeEvent::UserInterimText("like to end now".into()));
script.push_back(RealtimeEvent::UserText("I'd like to end now".into()));
script.push_back(RealtimeEvent::Usage(Usage {
input_tokens: Some(10),
output_tokens: Some(5),
total_tokens: Some(15),
extra: None,
}));
script.push_back(RealtimeEvent::ToolCall {
id: "fc-end-1".into(),
name: END_TOOL.into(),
args: json!({}),
});
script.push_back(RealtimeEvent::Closed);
Self {
connected,
kicked_off: Arc::new(AtomicBool::new(false)),
audio_received,
tool_results,
script,
}
}
}
#[async_trait]
impl RealtimeLlm for MockRealtime {
async fn connect(&mut self, _setup: RealtimeSetup) -> Result<(), FlowcatError> {
self.connected.store(true, Ordering::SeqCst);
Ok(())
}
async fn send_audio(&mut self, _chunk: AudioChunk) -> Result<(), FlowcatError> {
*self.audio_received.lock().unwrap() += 1;
Ok(())
}
async fn update_system(
&mut self,
_prompt: String,
_tools: Vec<ToolDecl>,
) -> Result<(), FlowcatError> {
Ok(())
}
async fn send_tool_result(
&mut self,
id: String,
result: serde_json::Value,
) -> Result<(), FlowcatError> {
let status = result
.get("status")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
self.tool_results.lock().unwrap().push((id, status));
Ok(())
}
async fn next_event(&mut self) -> Option<RealtimeEvent> {
while !self.kicked_off.load(Ordering::SeqCst) {
tokio::time::sleep(Duration::from_millis(1)).await;
}
tokio::time::sleep(Duration::from_millis(1)).await;
self.script.pop_front()
}
}
#[async_trait]
impl RealtimeKickoff for MockRealtime {
async fn kickoff(&mut self) -> Result<(), FlowcatError> {
self.kicked_off.store(true, Ordering::SeqCst);
Ok(())
}
}
pub(crate) struct MockBrain {
seen_tools: Arc<Mutex<Vec<String>>>,
}
impl MockBrain {
pub(crate) fn new(seen_tools: Arc<Mutex<Vec<String>>>) -> Self {
Self { seen_tools }
}
}
impl AgentBrain for MockBrain {
fn system_prompt(&self) -> String {
"You are a test agent.".into()
}
fn tools(&self) -> Vec<ToolDecl> {
vec![ToolDecl {
name: END_TOOL.into(),
description: "End the call.".into(),
params: json!({ "type": "object", "properties": {} }),
}]
}
fn current_node_id(&self) -> String {
"start".into()
}
fn on_tool_call(&mut self, name: &str, _args: &serde_json::Value) -> BrainAction {
self.seen_tools.lock().unwrap().push(name.to_string());
if name == END_TOOL {
BrainAction::End {
disposition: Some("completed".into()),
}
} else {
BrainAction::Stay
}
}
fn is_finished(&self) -> bool {
false
}
fn collected_vars(&self) -> serde_json::Value {
json!({ "name": "Ada", "intent": "support" })
}
}
#[derive(Default)]
pub(crate) struct Captured {
pub(crate) complete_calls: usize,
pub(crate) finalize: Option<Finalize>,
pub(crate) uploads: Vec<(String, String, usize)>,
pub(crate) tool_calls: Vec<(String, String, serde_json::Value)>,
}
pub(crate) struct MockSession {
captured: Arc<Mutex<Captured>>,
node_tools: Vec<ToolDecl>,
tool_result: String,
}
impl MockSession {
pub(crate) fn new(captured: Arc<Mutex<Captured>>) -> Self {
Self {
captured,
node_tools: vec![],
tool_result: String::new(),
}
}
pub(crate) fn with_node_tools(
captured: Arc<Mutex<Captured>>,
tools: Vec<ToolDecl>,
result: impl Into<String>,
) -> Self {
Self {
captured,
node_tools: tools,
tool_result: result.into(),
}
}
}
#[async_trait]
impl SessionSource for MockSession {
async fn resolve(&self, _run_id: i64, _token: &str) -> Result<ResolvedCall, FlowcatError> {
Ok(ResolvedCall {
provider: "plivo".into(),
brain_config: json!({}),
is_completed: false,
})
}
async fn complete(
&self,
_run_id: i64,
_token: &str,
fin: Finalize,
) -> Result<(), FlowcatError> {
let mut c = self.captured.lock().unwrap();
c.complete_calls += 1;
c.finalize = Some(fin);
Ok(())
}
async fn artifact_upload_url(
&self,
_run_id: i64,
_token: &str,
kind: &str,
) -> Result<UploadTarget, FlowcatError> {
Ok(UploadTarget {
url: format!("https://uploads.test/{kind}?sig=secret"),
key: format!("runs/4242/{kind}"),
content_type: String::new(), })
}
async fn put_bytes(
&self,
url: &str,
bytes: Vec<u8>,
content_type: &str,
) -> Result<(), FlowcatError> {
self.captured.lock().unwrap().uploads.push((
url.to_string(),
content_type.to_string(),
bytes.len(),
));
Ok(())
}
async fn node_tools(
&self,
_run_id: i64,
_token: &str,
_node_id: &str,
) -> Result<Vec<ToolDecl>, FlowcatError> {
Ok(self.node_tools.clone())
}
async fn tool_call(
&self,
_run_id: i64,
_token: &str,
node_id: &str,
tool_name: &str,
args: &serde_json::Value,
) -> Result<String, FlowcatError> {
self.captured.lock().unwrap().tool_calls.push((
node_id.to_string(),
tool_name.to_string(),
args.clone(),
));
Ok(self.tool_result.clone())
}
}
pub(crate) struct McpMockRealtime {
kicked_off: Arc<AtomicBool>,
raw_results: Arc<Mutex<Vec<(String, serde_json::Value)>>>,
advertised: Arc<Mutex<Vec<Vec<String>>>>,
script: VecDeque<RealtimeEvent>,
}
impl McpMockRealtime {
pub(crate) fn new(
raw_results: Arc<Mutex<Vec<(String, serde_json::Value)>>>,
advertised: Arc<Mutex<Vec<Vec<String>>>>,
mcp_tool_name: &str,
) -> Self {
let mut script = VecDeque::new();
script.push_back(RealtimeEvent::ToolCall {
id: "fc-mcp-1".into(),
name: mcp_tool_name.into(),
args: json!({ "specialty": "dentistry" }),
});
script.push_back(RealtimeEvent::ToolCall {
id: "fc-end-1".into(),
name: END_TOOL.into(),
args: json!({}),
});
script.push_back(RealtimeEvent::Closed);
Self {
kicked_off: Arc::new(AtomicBool::new(false)),
raw_results,
advertised,
script,
}
}
}
#[async_trait]
impl RealtimeLlm for McpMockRealtime {
async fn connect(&mut self, setup: RealtimeSetup) -> Result<(), FlowcatError> {
self.advertised
.lock()
.unwrap()
.push(setup.tools.iter().map(|t| t.name.clone()).collect());
Ok(())
}
async fn send_audio(&mut self, _chunk: AudioChunk) -> Result<(), FlowcatError> {
Ok(())
}
async fn update_system(
&mut self,
_prompt: String,
tools: Vec<ToolDecl>,
) -> Result<(), FlowcatError> {
self.advertised
.lock()
.unwrap()
.push(tools.iter().map(|t| t.name.clone()).collect());
Ok(())
}
async fn send_tool_result(
&mut self,
id: String,
result: serde_json::Value,
) -> Result<(), FlowcatError> {
self.raw_results.lock().unwrap().push((id, result));
Ok(())
}
async fn next_event(&mut self) -> Option<RealtimeEvent> {
while !self.kicked_off.load(Ordering::SeqCst) {
tokio::time::sleep(Duration::from_millis(1)).await;
}
tokio::time::sleep(Duration::from_millis(1)).await;
self.script.pop_front()
}
}
#[async_trait]
impl RealtimeKickoff for McpMockRealtime {
async fn kickoff(&mut self) -> Result<(), FlowcatError> {
self.kicked_off.store(true, Ordering::SeqCst);
Ok(())
}
}