use std::sync::{Arc, Mutex};
use std::time::Duration;
use async_trait::async_trait;
use futures::future::BoxFuture;
use serde_json::Value;
use crate::context::LLMContext;
use crate::error::{PipecatError, Result};
use crate::frames::{
DataFrame, Frame, FrameDirection, FrameHandler, FrameInner, FrameProcessor, ProcessorBusHandle,
};
use super::task::{TaskContext, TaskResult};
pub const DEFAULT_CALL_TIMEOUT: Duration = Duration::from_secs(90);
pub struct CoordinatorCtx {
source: String,
bus: Option<Arc<dyn ProcessorBusHandle>>,
}
impl CoordinatorCtx {
pub async fn call(&self, target: &str, task: &str, payload: Value) -> Result<TaskResult> {
self.call_with_timeout(target, task, payload, DEFAULT_CALL_TIMEOUT)
.await
}
pub async fn call_with_timeout(
&self,
target: &str,
task: &str,
payload: Value,
timeout: Duration,
) -> Result<TaskResult> {
let task_ctx = self.task_ctx()?;
let handle = task_ctx
.dispatch(&self.source, target, Some(task.to_string()), Some(payload))
.await?;
handle.await_completion(Some(timeout)).await
}
fn task_ctx(&self) -> Result<&TaskContext> {
let bus = self.bus.as_ref().ok_or_else(|| {
PipecatError::pipeline(
"CoordinatorProcessor: no bus handle — is it inside a BaseAgent pipeline?",
)
})?;
bus.as_any().downcast_ref::<TaskContext>().ok_or_else(|| {
PipecatError::pipeline("CoordinatorProcessor: bus handle is not a TaskContext")
})
}
}
pub type CoordinatorFn = Arc<
dyn Fn(CoordinatorCtx, Arc<Mutex<LLMContext>>) -> BoxFuture<'static, String> + Send + Sync,
>;
pub struct CoordinatorProcessor {
agent_name: String,
handler: CoordinatorFn,
}
impl CoordinatorProcessor {
pub fn new<F>(agent_name: impl Into<String>, handler: F) -> Self
where
F: Fn(CoordinatorCtx, Arc<Mutex<LLMContext>>) -> BoxFuture<'static, String>
+ Send
+ Sync
+ 'static,
{
Self {
agent_name: agent_name.into(),
handler: Arc::new(handler),
}
}
pub fn into_processor(self) -> FrameProcessor {
FrameProcessor::new("CoordinatorProcessor", Box::new(self), false)
}
}
#[async_trait]
impl FrameHandler for CoordinatorProcessor {
async fn on_process_frame(
&self,
processor: &FrameProcessor,
frame: Frame,
direction: FrameDirection,
) -> Result<()> {
match &frame.inner {
FrameInner::Data(DataFrame::LLMContextFrame(context)) => {
let cx = CoordinatorCtx {
source: self.agent_name.clone(),
bus: processor.bus_handle(),
};
let answer = (self.handler)(cx, context.clone()).await;
processor
.push_frame(Frame::llm_full_response_start(), FrameDirection::Downstream)
.await?;
if !answer.trim().is_empty() {
processor
.push_frame(Frame::llm_text(answer), FrameDirection::Downstream)
.await?;
}
processor
.push_frame(Frame::llm_full_response_end(), FrameDirection::Downstream)
.await?;
}
_ => {
processor.push_frame(frame, direction).await?;
}
}
Ok(())
}
}