use crate::llm::LLMProvider;
use anyhow::Result;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::{RwLock, broadcast};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskOrigin {
pub routing_key: String,
#[serde(flatten)]
pub metadata: HashMap<String, Value>,
}
impl TaskOrigin {
pub fn new(routing_key: impl Into<String>) -> Self {
Self {
routing_key: routing_key.into(),
metadata: HashMap::new(),
}
}
pub fn from_channel(channel: &str, chat_id: &str) -> Self {
Self::new(format!("{}:{}", channel, chat_id))
}
pub fn with_metadata(mut self, key: impl Into<String>, value: Value) -> Self {
self.metadata.insert(key.into(), value);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum TaskStatus {
Pending,
Running,
Completed(String),
Failed(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BackgroundTask {
pub id: String,
pub prompt: String,
pub origin: TaskOrigin,
pub status: TaskStatus,
pub started_at: DateTime<Utc>,
#[serde(skip_serializing_if = "Option::is_none")]
pub completed_at: Option<DateTime<Utc>>,
}
impl BackgroundTask {
pub fn new(prompt: impl Into<String>, origin: TaskOrigin) -> Self {
Self {
id: Uuid::new_v4().to_string()[..8].to_string(),
prompt: prompt.into(),
origin,
status: TaskStatus::Pending,
started_at: Utc::now(),
completed_at: None,
}
}
pub fn mark_running(&mut self) {
self.status = TaskStatus::Running;
}
pub fn mark_completed(&mut self, result: impl Into<String>) {
self.status = TaskStatus::Completed(result.into());
self.completed_at = Some(Utc::now());
}
pub fn mark_failed(&mut self, error: impl Into<String>) {
self.status = TaskStatus::Failed(error.into());
self.completed_at = Some(Utc::now());
}
pub fn is_finished(&self) -> bool {
matches!(
self.status,
TaskStatus::Completed(_) | TaskStatus::Failed(_)
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskResult {
pub task_id: String,
pub origin: TaskOrigin,
pub content: String,
pub success: bool,
pub timestamp: DateTime<Utc>,
}
impl TaskResult {
pub fn success(
task_id: impl Into<String>,
origin: TaskOrigin,
content: impl Into<String>,
) -> Self {
Self {
task_id: task_id.into(),
origin,
content: content.into(),
success: true,
timestamp: Utc::now(),
}
}
pub fn failure(
task_id: impl Into<String>,
origin: TaskOrigin,
error: impl Into<String>,
) -> Self {
Self {
task_id: task_id.into(),
origin,
content: error.into(),
success: false,
timestamp: Utc::now(),
}
}
}
#[derive(Debug, Clone)]
pub struct TaskOrchestratorConfig {
pub max_concurrent_tasks: usize,
pub default_model: String,
}
impl Default for TaskOrchestratorConfig {
fn default() -> Self {
Self {
max_concurrent_tasks: 10,
default_model: "gpt-4o-mini".to_string(),
}
}
}
pub struct TaskOrchestrator {
provider: Arc<dyn LLMProvider>,
active_tasks: Arc<RwLock<HashMap<String, BackgroundTask>>>,
result_sender: broadcast::Sender<TaskResult>,
config: TaskOrchestratorConfig,
}
impl TaskOrchestrator {
pub fn new(provider: Arc<dyn LLMProvider>, config: TaskOrchestratorConfig) -> Self {
let (result_sender, _) = broadcast::channel(100);
Self {
provider,
active_tasks: Arc::new(RwLock::new(HashMap::new())),
result_sender,
config,
}
}
pub fn with_defaults(provider: Arc<dyn LLMProvider>) -> Self {
Self::new(provider, TaskOrchestratorConfig::default())
}
pub async fn spawn(&self, prompt: &str, origin: TaskOrigin) -> Result<String> {
let active_count = self.active_tasks.read().await.len();
if active_count >= self.config.max_concurrent_tasks {
return Err(anyhow::anyhow!(
"Maximum concurrent tasks ({}) reached",
self.config.max_concurrent_tasks
));
}
let mut task = BackgroundTask::new(prompt, origin.clone());
let task_id = task.id.clone();
task.mark_running();
self.active_tasks
.write()
.await
.insert(task_id.clone(), task.clone());
let provider = Arc::clone(&self.provider);
let active_tasks = Arc::clone(&self.active_tasks);
let result_sender = self.result_sender.clone();
let model = self.config.default_model.clone();
let prompt = prompt.to_string();
let task_id_clone = task_id.clone();
tokio::spawn(async move {
let result = Self::run_task(&provider, &model, &prompt).await;
{
let mut tasks = active_tasks.write().await;
if let Some(task) = tasks.get_mut(&task_id_clone) {
match &result {
Ok(content) => task.mark_completed(content),
Err(e) => task.mark_failed(e.to_string()),
}
}
}
let task_result = match &result {
Ok(content) => TaskResult::success(&task_id_clone, origin, content),
Err(e) => TaskResult::failure(&task_id_clone, origin, e.to_string()),
};
let _ = result_sender.send(task_result);
tokio::time::sleep(tokio::time::Duration::from_secs(300)).await;
let mut tasks = active_tasks.write().await;
tasks.remove(&task_id_clone);
});
Ok(task_id)
}
async fn run_task(
provider: &Arc<dyn LLMProvider>,
model: &str,
prompt: &str,
) -> Result<String> {
use crate::llm::types::ChatCompletionRequest;
let request = ChatCompletionRequest::new(model)
.system(
"You are a helpful assistant. Complete the given task thoroughly and concisely.",
)
.user(prompt);
let response = provider.chat(request).await?;
response
.content()
.map(|s| s.to_string())
.ok_or_else(|| anyhow::anyhow!("No response content"))
}
pub fn subscribe_results(&self) -> broadcast::Receiver<TaskResult> {
self.result_sender.subscribe()
}
pub async fn get_active_tasks(&self) -> Vec<BackgroundTask> {
self.active_tasks.read().await.values().cloned().collect()
}
pub async fn get_task(&self, task_id: &str) -> Option<BackgroundTask> {
self.active_tasks.read().await.get(task_id).cloned()
}
pub fn config(&self) -> &TaskOrchestratorConfig {
&self.config
}
}