diff --git a/moly-mofa/src/lib.rs b/moly-mofa/src/lib.rs index aa87f0eb..2926cfd7 100644 --- a/moly-mofa/src/lib.rs +++ b/moly-mofa/src/lib.rs @@ -2,7 +2,7 @@ use moly_protocol::open_ai::{ ChatResponseData, ChoiceData, MessageData, Role, StopReason, UsageData, }; use serde::{Deserialize, Deserializer, Serialize}; -use std::sync::mpsc::{self, channel}; +use std::sync::mpsc::{self, channel, Sender}; use tokio::task::JoinHandle; #[derive(Debug, Serialize, Deserialize)] @@ -50,121 +50,130 @@ pub struct MofaResponseSearchAssistant { pub result: MofaResponseSearchAssistantResult, } -#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)] -pub enum MofaAgent { - Example, +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Hash, Eq)] +pub struct AgentId(pub String); + +impl AgentId { + pub fn new(agent_name: &str, server_address: &str) -> Self { + AgentId(format!("{}-{}", agent_name, server_address)) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum AgentType { Reasoner, SearchAssistant, ResearchScholar, + MakepadExpert, } -impl MofaAgent { - pub fn name(&self) -> &str { - match self { - MofaAgent::Example => "Example Agent", - MofaAgent::Reasoner => "Reasoner Agent", - MofaAgent::SearchAssistant => "Search Assistant", - MofaAgent::ResearchScholar => "Research Scholar", - } - } +#[derive(Debug, Default, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)] +pub struct MofaServerId(pub String); - pub fn short_description(&self) -> &str { - match self { - MofaAgent::Example => "This is an example agent implemented by MoFa", - MofaAgent::Reasoner => "Helps to find good questions about any topic", - MofaAgent::SearchAssistant => "Your assistant to find information on the web", - MofaAgent::ResearchScholar => "Expert in academic research", - } - } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MofaAgent { + pub id: AgentId, + pub name: String, + pub description: String, + pub agent_type: AgentType, + pub server_id: MofaServerId, } -#[derive(Default)] -pub struct MofaOptions { - pub address: Option, +impl MofaAgent { + /// Returns a dummy agent whenever the corresponding Agent cannot be found + /// (due to the server not being available, the server no longer providing the agent, etc.). + pub fn unknown() -> Self { + MofaAgent { + id: AgentId("unknown".to_string()), + name: "Inaccesible Agent".to_string(), + description: "This agent is not currently reachable, its information is not available".to_string(), + agent_type: AgentType::Reasoner, + server_id: MofaServerId("Unknown".to_string()), + } + } } -pub enum TestServerResponse { - Success(String), - Failure(String), +pub enum MofaServerResponse { + Connected(String, Vec), + Unavailable(String), } pub enum MofaAgentCommand { - SendTask(String, MofaAgent, mpsc::Sender), + SendTask(String, MofaAgent, Sender), CancelTask, - UpdateServerAddress(String), - TestServer(mpsc::Sender), + FetchAgentsFromServer(Sender), } -pub struct MofaBackend { - pub command_sender: mpsc::Sender, +#[derive(Clone, Debug)] +pub struct MofaClient { + command_sender: Sender, + pub address: String, } -impl Default for MofaBackend { - fn default() -> Self { - Self::new() - } +#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)] +pub enum BackendType { + Local, + Remote, } -const DEFAULT_MOFA_ADDRESS: &str = "http://localhost:8000"; +impl MofaClient { + pub fn cancel_task(&self) { + self.command_sender.send(MofaAgentCommand::CancelTask).unwrap(); + } -impl MofaBackend { - pub fn available_agents() -> Vec { - vec![ - MofaAgent::Example, - // Keeping only one agent for now. We will revisit this later when MoFa is more stable. + pub fn fetch_agents(&self, tx: Sender) { + self.command_sender.send(MofaAgentCommand::FetchAgentsFromServer(tx)).unwrap(); + } - // MofaAgent::Reasoner, - // MofaAgent::SearchAssistant, - // MofaAgent::ResearchScholar, - ] + pub fn send_message_to_agent(&self, agent: &MofaAgent, prompt: &String, tx: Sender) { + self.command_sender.send(MofaAgentCommand::SendTask(prompt.clone(), agent.clone(), tx)).unwrap(); } - pub fn new() -> Self { + pub fn new(address: String) -> Self { if should_be_real() { let (command_sender, command_receiver) = channel(); - let backend = Self { command_sender }; - + let address_clone = address.clone(); std::thread::spawn(move || { - Self::main_loop(command_receiver); + Self::process_agent_commands(command_receiver, address_clone); }); - backend + Self { command_sender, address } } else { Self::new_fake() } } - pub fn main_loop(command_receiver: mpsc::Receiver) { - println!("MoFa backend started"); + /// Handles the communication between the MofaClient and the MoFa server. + /// + /// This function runs in a separate thread and processes commands received through the command channel. + /// + /// The loop continues until the command channel is closed or an unrecoverable error occurs. + fn process_agent_commands(command_receiver: mpsc::Receiver, address: String) { let rt = tokio::runtime::Runtime::new().unwrap(); let mut current_request: Option> = None; - let mut options = MofaOptions::default(); - loop { - match command_receiver.recv().unwrap() { - MofaAgentCommand::SendTask(task, _agent, tx) => { + while let Ok(command) = command_receiver.recv() { + match command { + MofaAgentCommand::SendTask(task, agent, tx) => { if let Some(handle) = current_request.take() { handle.abort(); } + let data = ChatRequest { - model: "example".to_string(), + model: agent.name, messages: vec![MessageData { role: Role::User, content: task, }], }; let client = reqwest::Client::new(); - let url = options - .address - .clone() - .unwrap_or(DEFAULT_MOFA_ADDRESS.to_string()); + + let req = client + .post(format!("{}/v1/chat/completions", &address)) + .json(&data); + current_request = Some(rt.spawn(async move { - let resp = client - .post(format!("{}/v1/chat/completions", url)) - .json(&data) - .send() - .await - .expect("Failed to send request"); + let resp = req.send().await.expect("Failed to send request"); let resp: Result = resp.json().await; match resp { @@ -183,14 +192,8 @@ impl MofaBackend { } continue; } - MofaAgentCommand::UpdateServerAddress(address) => { - options.address = Some(address); - } - MofaAgentCommand::TestServer(tx) => { - let url = options - .address - .clone() - .unwrap_or(DEFAULT_MOFA_ADDRESS.to_string()); + MofaAgentCommand::FetchAgentsFromServer(tx) => { + let url = address.clone(); let resp = reqwest::blocking::ClientBuilder::new() .timeout(std::time::Duration::from_secs(5)) .no_proxy() @@ -200,31 +203,38 @@ impl MofaBackend { .send(); match resp { - Ok(r) => { - if r.status().is_success() { - tx.send(TestServerResponse::Success(url)).unwrap(); - } else { - tx.send(TestServerResponse::Failure(url)).unwrap(); - } + Ok(r) if r.status().is_success() => { + let agents = vec![ + MofaAgent { + id: AgentId::new("Reasoner", &url), + name: "Reasoner".to_string(), + description: "An agent that will help you find good questions about any topic".to_string(), + agent_type: AgentType::Reasoner, + server_id: MofaServerId(url.clone()), + }, + ]; + tx.send(MofaServerResponse::Connected(url, agents)).unwrap(); } - Err(e) => { - tx.send(TestServerResponse::Failure(url)).unwrap(); - eprintln!("{e}"); + _ => { + tx.send(MofaServerResponse::Unavailable(url)).unwrap(); } - }; + } } } } + + // Clean up any pending request when the channel is closed + if let Some(handle) = current_request { + handle.abort(); + } } // For testing purposes pub fn new_fake() -> Self { let (command_sender, command_receiver) = channel(); - let backend = Self { command_sender }; - + std::thread::spawn(move || { loop { - // Receive command from frontend match command_receiver.recv().unwrap() { MofaAgentCommand::SendTask(_task, _agent, tx) => { let data = ChatResponseData { @@ -255,7 +265,7 @@ impl MofaBackend { } }); - backend + Self { command_sender, address: "localhost:8000".to_string() } } } @@ -273,8 +283,6 @@ pub enum ChatResponse { ChatFinalResponseData(ChatResponseData), } -// ==== - #[derive(Clone, Debug, Serialize, Deserialize)] struct ChatRequest { model: String, diff --git a/resources/images/circle_check_icon.png b/resources/images/circle_check_icon.png new file mode 100644 index 00000000..c749dfd3 Binary files /dev/null and b/resources/images/circle_check_icon.png differ diff --git a/resources/images/globe_icon.png b/resources/images/globe_icon.png new file mode 100644 index 00000000..7b43a1fd Binary files /dev/null and b/resources/images/globe_icon.png differ diff --git a/resources/images/laptop_icon.png b/resources/images/laptop_icon.png new file mode 100644 index 00000000..66a47c2f Binary files /dev/null and b/resources/images/laptop_icon.png differ diff --git a/resources/images/loader_icon.png b/resources/images/loader_icon.png new file mode 100644 index 00000000..642e2944 Binary files /dev/null and b/resources/images/loader_icon.png differ diff --git a/resources/images/makepad_expert_agent_icon.png b/resources/images/makepad_expert_agent_icon.png new file mode 100644 index 00000000..0d322275 Binary files /dev/null and b/resources/images/makepad_expert_agent_icon.png differ diff --git a/resources/images/refresh_error_icon.png b/resources/images/refresh_error_icon.png new file mode 100644 index 00000000..6e73f759 Binary files /dev/null and b/resources/images/refresh_error_icon.png differ diff --git a/src/app.rs b/src/app.rs index b976af4f..c7126900 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,5 +1,6 @@ use crate::chat::chat_panel::ChatPanelAction; use crate::chat::model_selector_list::ModelSelectorListAction; +use crate::data::chats::{MoFaTestServerAction, MofaServerConnectionStatus}; use crate::data::downloads::download::DownloadFileAction; use crate::data::downloads::DownloadPendingNotification; use crate::data::store::*; @@ -12,6 +13,7 @@ use crate::shared::download_notification_popup::{ use crate::shared::popup_notification::PopupNotificationWidgetRefExt; use makepad_widgets::*; use markdown::MarkdownAction; +use moly_mofa::MofaServerId; live_design! { use link::theme::*; @@ -249,6 +251,10 @@ impl MatchEvent for App { discover_radio_button.select(cx, &mut Scope::empty()); } + self.store.handle_mofa_test_server_action(action.cast()); + // redraw the UI to reflect the connection status + self.ui.redraw(cx); + if matches!( action.cast(), DownloadNotificationPopupAction::ActionLinkClicked diff --git a/src/chat/chat_history.rs b/src/chat/chat_history.rs index c4ceb10c..393dea1a 100644 --- a/src/chat/chat_history.rs +++ b/src/chat/chat_history.rs @@ -1,10 +1,11 @@ use super::chat_history_card::ChatHistoryCardWidgetRefExt; use crate::chat::entity_button::EntityButtonWidgetRefExt; use crate::data::chats::chat::ChatID; +use crate::data::chats::AgentsAvailability; use crate::data::store::Store; use crate::shared::actions::ChatAction; use makepad_widgets::*; -use moly_mofa::{MofaAgent, MofaBackend}; +use moly_mofa::MofaAgent; live_design! { use link::theme::*; @@ -24,7 +25,16 @@ live_design! { margin: {left: 4, bottom: 4}, draw_text:{ text_style: {font_size: 10}, - color: #667085 + color: #3 + } + } + + NoAgentsWarning =