use std::sync::Arc; use ::agent::chat::ChatService; use ::agent::client::AiClientConfig; use ::agent::tool::ToolRegistry; use ::agent::{EmbedService, TaskService, new_embed_client}; use avatar::AppAvatar; use config::AppConfig; use db::cache::AppCache; use db::database::AppDatabase; use email::AppEmail; use queue::{ EmailEnvelope, EmailSendFn, EmailSendFut, MessageProducer, NatsClient, start_email_worker, }; use room::RoomService; use room::metrics::RoomMetrics; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; use ws_token::WsTokenService; pub mod storage; pub use storage::AppStorage; pub mod push; pub use push::{PushPayload, WebPushService}; pub mod push_helper; #[derive(Clone)] pub struct AppService { pub db: AppDatabase, pub config: AppConfig, pub cache: AppCache, pub email: AppEmail, pub avatar: AppAvatar, pub room: RoomService, pub ws_token: Arc, pub queue_producer: MessageProducer, pub storage: Option, pub push: Option>, pub embed_service: Option>, pub nats: Option>, pub chat_service: Option>, } impl AppService { pub fn send_push_to_user(&self, user_id: uuid::Uuid, payload: PushPayload) { push_helper::spawn_push_notification(self.push.clone(), self.db.clone(), user_id, payload); } } impl AppService { pub async fn start_room_workers( &self, shutdown_rx: tokio::sync::broadcast::Receiver<()>, ) -> anyhow::Result<()> { self.room.start_workers(shutdown_rx).await } pub async fn new(config: AppConfig) -> anyhow::Result { let db = AppDatabase::init(&config).await?; let cache = AppCache::init(&config).await?; let email = AppEmail::init(&config).await?; let avatar = AppAvatar::init(&config).await?; let storage = match AppStorage::new(&config) { Ok(s) => { tracing::info!(path = %s.base_path.display(), "Storage initialized"); Some(s) } Err(e) => { tracing::warn!(error = %e, "Storage not available"); None } }; let push = match (config.vapid_public_key(), config.vapid_private_key()) { (Some(public_key), Some(private_key)) => { match WebPushService::new(public_key, private_key, config.vapid_sender_email()) { Ok(s) => { tracing::info!("WebPush initialized"); Some(Arc::new(s)) } Err(e) => { tracing::warn!(error = %e, "WebPush not available"); None } } } _ => { tracing::warn!("WebPush disabled - VAPID keys not configured"); None } }; // Redis connection getter — used by MessageProducer (for cache/seq) and WsTokenService let get_redis: Arc< dyn Fn() -> tokio::task::JoinHandle> + Send + Sync, > = Arc::new({ let pool = cache.redis_pool().clone(); move || { let pool = pool.clone(); tokio::spawn(async move { pool.get().await.map_err(|e| anyhow::anyhow!("{}", e)) }) } }); // Connect to NATS (if configured) let nats: Option> = match NatsClient::connect(&config).await { Some(c) => Some(Arc::new(c)), None => None, }; // Build MessageProducer with NATS let message_producer = MessageProducer::new(nats.clone(), get_redis.clone()); // Build RoomService let task_service = Arc::new(TaskService::new(db.clone())); let room_metrics = Arc::new(RoomMetrics::default()); let room_manager = Arc::new(room::connection::RoomConnectionManager::new( room_metrics.clone(), cache.clone(), )); // Build EmbedService if Qdrant and embedding model are configured (graceful degradation) let embed_service: Option> = match new_embed_client(&config).await { Ok(client) => { let model_name = config .get_embed_model_name() .unwrap_or_else(|_| "text-embedding-3-small".into()); let dimensions = config.get_embed_model_dimensions().unwrap_or(1536); let svc = EmbedService::new(client, db.writer().clone(), model_name, dimensions); let _ = svc.ensure_collections().await; tracing::info!("EmbedService initialized (Qdrant + embeddings)"); Some(Arc::new(svc)) } Err(e) => { tracing::warn!(error = %e, "EmbedService not available - vector search disabled"); None } }; let embed_service_for_app = embed_service.clone(); // Build ChatService if AI is configured; otherwise AI chat is disabled (graceful degradation) let chat_service: Option> = match (config.ai_api_key(), config.ai_basic_url()) { (Ok(api_key), Ok(base_url)) => { tracing::info!(url = %base_url, "AI chat enabled"); let ai_client_config = AiClientConfig::new(api_key).with_base_url(&base_url); let compact_service = ::agent::CompactService::new( db.writer().clone(), ai_client_config.clone(), "gpt-4o-mini".to_string(), ); let mut registry = ToolRegistry::new(); fctool::git_tools::register_all(&mut registry); fctool::file_tools::register_all(&mut registry); fctool::project_tools::register_all(&mut registry); fctool::chat_tools::register_all(&mut registry); let mut chat_svc = ChatService::new() .with_ai_client_config(ai_client_config) .with_compact_service(compact_service) .with_tool_registry(registry); if let Some(ref es) = embed_service { chat_svc = chat_svc.with_embed_service((**es).clone()); } Some(Arc::new(chat_svc)) } (Err(e), _) => { tracing::warn!(error = %e, "AI chat disabled"); None } (_, Err(e)) => { tracing::warn!(error = %e, "AI chat disabled"); None } }; // Build push notification callback for RoomService let push_fn: Option = push .as_ref() .map(|push_svc| push_helper::create_push_notification_fn(push_svc.clone(), db.clone())); let room = RoomService::new( db.clone(), cache.clone(), config.clone(), message_producer.clone(), room_manager, nats.clone(), chat_service.clone(), Some(task_service.clone()), None, push_fn, embed_service, ); // Build WsTokenService (still uses Redis for token cache) let ws_token = Arc::new(WsTokenService::new(get_redis)); Ok(Self { db, config, cache, email, avatar, room, ws_token, queue_producer: message_producer, storage, push, embed_service: embed_service_for_app, nats, chat_service, }) } pub async fn start_email_workers( &self, shutdown_rx: tokio::sync::broadcast::Receiver<()>, ) -> anyhow::Result<()> { let email = self.email.clone(); let send_fn: EmailSendFn = Arc::new(move |envelopes: Vec| -> EmailSendFut { let email = email.clone(); Box::pin(async move { for envelope in envelopes { let to = envelope.to.clone(); let msg = email::EmailMessage { to: envelope.to, subject: envelope.subject, body: envelope.body, }; if let Err(e) = email.send(msg).await { tracing::error!(to = %to, error = %e, "email send failed"); } } Ok(()) }) }); let nats_for_email = self.nats.clone(); start_email_worker(nats_for_email, send_fn, shutdown_rx).await; Ok(()) } } pub mod agent; pub mod auth; pub mod chat; pub mod error; pub mod git; pub mod issue; pub mod project; pub mod pull_request; pub mod search; pub mod skill; pub mod user; pub mod utils; pub mod ws_token; #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] pub struct Pager { pub page: i64, #[serde(alias = "par_page")] pub per_page: i64, }