//! Classroom multi-agent chat commands //! //! - `classroom_chat` — send a message and receive multi-agent responses //! - `classroom_chat_history` — retrieve chat history for a classroom use std::sync::Arc; use tokio::sync::Mutex; use serde::{Deserialize, Serialize}; use tauri::State; use zclaw_kernel::generation::{ AgentProfile, AgentRole, ClassroomChatMessage, ClassroomChatState, ClassroomChatRequest, build_chat_prompt, parse_chat_responses, }; use zclaw_runtime::CompletionRequest; use super::ClassroomStore; use crate::kernel_commands::KernelState; // --------------------------------------------------------------------------- // State // --------------------------------------------------------------------------- /// Chat state store: classroom_id → chat state pub type ChatStore = Arc>>; pub fn create_chat_state() -> ChatStore { Arc::new(Mutex::new(std::collections::HashMap::new())) } // --------------------------------------------------------------------------- // Request / Response // --------------------------------------------------------------------------- #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ClassroomChatCmdRequest { pub classroom_id: String, pub user_message: String, pub scene_context: Option, } // --------------------------------------------------------------------------- // Commands // --------------------------------------------------------------------------- /// Send a message in the classroom chat and get multi-agent responses. // @connected #[tauri::command] pub async fn classroom_chat( store: State<'_, ClassroomStore>, chat_store: State<'_, ChatStore>, kernel_state: State<'_, KernelState>, persistence: State<'_, crate::classroom_commands::persist::ClassroomPersistence>, request: ClassroomChatCmdRequest, ) -> Result, String> { if request.user_message.trim().is_empty() { return Err("Message cannot be empty".to_string()); } // Get classroom data let classroom = { let s = store.lock().await; s.get(&request.classroom_id) .cloned() .ok_or_else(|| format!("Classroom '{}' not found", request.classroom_id))? }; // Create user message let user_msg = ClassroomChatMessage::user_message(&request.user_message); // Get chat history for context let history: Vec = { let cs = chat_store.lock().await; cs.get(&request.classroom_id) .map(|s| s.messages.clone()) .unwrap_or_default() }; // Try LLM-powered multi-agent responses, fallback to placeholder let agent_responses = match generate_llm_responses(&kernel_state, &classroom.agents, &request.user_message, request.scene_context.as_deref(), &history).await { Ok(responses) => responses, Err(e) => { tracing::warn!("LLM chat generation failed, using placeholders: {}", e); generate_placeholder_responses( &classroom.agents, &request.user_message, request.scene_context.as_deref(), ) } }; // Store in chat state { let mut cs = chat_store.lock().await; let state = cs.entry(request.classroom_id.clone()) .or_insert_with(|| ClassroomChatState { messages: vec![], active: true, }); state.messages.push(user_msg); state.messages.extend(agent_responses.clone()); // Persist chat to SQLite if let Err(e) = persistence.save_chat(&request.classroom_id, &state.messages).await { tracing::warn!("[ClassroomChat] Failed to persist chat for {}: {}", request.classroom_id, e); } } Ok(agent_responses) } /// Retrieve chat history for a classroom // @connected #[tauri::command] pub async fn classroom_chat_history( chat_store: State<'_, ChatStore>, classroom_id: String, ) -> Result, String> { let cs = chat_store.lock().await; Ok(cs.get(&classroom_id) .map(|s| s.messages.clone()) .unwrap_or_default()) } // --------------------------------------------------------------------------- // Placeholder response generation // --------------------------------------------------------------------------- fn generate_placeholder_responses( agents: &[AgentProfile], user_message: &str, scene_context: Option<&str>, ) -> Vec { let mut responses = Vec::new(); // Teacher always responds if let Some(teacher) = agents.iter().find(|a| a.role == AgentRole::Teacher) { let context_hint = scene_context .map(|ctx| format!("关于「{}」,", ctx)) .unwrap_or_default(); responses.push(ClassroomChatMessage::agent_message( teacher, &format!("{}这是一个很好的问题!让我来详细解释一下「{}」的核心概念...", context_hint, user_message), )); } // Assistant chimes in if let Some(assistant) = agents.iter().find(|a| a.role == AgentRole::Assistant) { responses.push(ClassroomChatMessage::agent_message( assistant, "我来补充一下要点 📌", )); } // One student responds if let Some(student) = agents.iter().find(|a| a.role == AgentRole::Student) { responses.push(ClassroomChatMessage::agent_message( student, &format!("谢谢老师!我大概理解了{}", user_message), )); } responses } // --------------------------------------------------------------------------- // LLM-powered response generation // --------------------------------------------------------------------------- async fn generate_llm_responses( kernel_state: &State<'_, KernelState>, agents: &[AgentProfile], user_message: &str, scene_context: Option<&str>, history: &[ClassroomChatMessage], ) -> Result, String> { let driver = { let ks = kernel_state.lock().await; ks.as_ref() .map(|k| k.driver()) .ok_or_else(|| "Kernel not initialized".to_string())? }; if !driver.is_configured() { return Err("LLM driver not configured".to_string()); } // Build the chat request for prompt generation (include history) let chat_request = ClassroomChatRequest { classroom_id: String::new(), user_message: user_message.to_string(), agents: agents.to_vec(), scene_context: scene_context.map(|s| s.to_string()), history: history.to_vec(), }; let prompt = build_chat_prompt(&chat_request); let request = CompletionRequest { model: "default".to_string(), system: Some("你是一个课堂多智能体讨论的协调器。".to_string()), messages: vec![zclaw_types::Message::User { content: prompt, }], ..Default::default() }; let response = driver.complete(request).await .map_err(|e| format!("LLM call failed: {}", e))?; // Extract text from response let text = response.content.iter() .filter_map(|block| match block { zclaw_runtime::ContentBlock::Text { text } => Some(text.as_str()), _ => None, }) .collect::>() .join(""); let responses = parse_chat_responses(&text, agents); if responses.is_empty() { return Err("LLM returned no parseable agent responses".to_string()); } Ok(responses) }