Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support multiple MoFa agents and servers #325

Merged
merged 27 commits into from
Dec 26, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 86 additions & 83 deletions moly-mofa/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,117 +50,122 @@ 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);

#[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 {
joulei marked this conversation as resolved.
Show resolved Hide resolved
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<String>,
impl Default for 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.).
fn default() -> Self {
MofaAgent {
id: AgentId("default".to_string()),
name: "Inaccesible Agent".to_string(),
description: "Default".to_string(),
agent_type: AgentType::Reasoner,
server_id: MofaServerId("default".to_string()),
}
}
joulei marked this conversation as resolved.
Show resolved Hide resolved
}

pub enum TestServerResponse {
Success(String),
Failure(String),
pub enum MofaServerResponse {
Connected(String, Vec<MofaAgent>),
Unavailable(String),
}

pub enum MofaAgentCommand {
SendTask(String, MofaAgent, mpsc::Sender<ChatResponse>),
CancelTask,
UpdateServerAddress(String),
TestServer(mpsc::Sender<TestServerResponse>),
FetchAgentsFromServer(mpsc::Sender<MofaServerResponse>),
}

pub struct MofaBackend {
pub command_sender: mpsc::Sender<MofaAgentCommand>,
#[derive(Clone, Debug)]
pub struct MofaClient {
command_sender: mpsc::Sender<MofaAgentCommand>,
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<MofaAgent> {
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: mpsc::Sender<MofaServerResponse>) {
joulei marked this conversation as resolved.
Show resolved Hide resolved
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: mpsc::Sender<ChatResponse>) {
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<MofaAgentCommand>) {
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.
joulei marked this conversation as resolved.
Show resolved Hide resolved
fn process_agent_commands(command_receiver: mpsc::Receiver<MofaAgentCommand>, address: String) {
let rt = tokio::runtime::Runtime::new().unwrap();
let mut current_request: Option<JoinHandle<()>> = None;
let mut options = MofaOptions::default();

loop {
match command_receiver.recv().unwrap() {
MofaAgentCommand::SendTask(task, _agent, tx) => {
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 address_clone = address.clone();
current_request = Some(rt.spawn(async move {
let resp = client
.post(format!("{}/v1/chat/completions", url))
.post(format!("{}/v1/chat/completions", address_clone))
joulei marked this conversation as resolved.
Show resolved Hide resolved
.json(&data)
.send()
.await
Expand All @@ -183,14 +188,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()
Expand All @@ -200,18 +199,22 @@ 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: unique_agent_id(&AgentId("Reasoner".to_string()), &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();
}
};
}
}
}
}
Expand All @@ -220,11 +223,9 @@ impl MofaBackend {
// 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 {
Expand Down Expand Up @@ -255,7 +256,7 @@ impl MofaBackend {
}
});

backend
Self { command_sender, address: "localhost:8000".to_string() }
}
}

Expand All @@ -273,10 +274,12 @@ pub enum ChatResponse {
ChatFinalResponseData(ChatResponseData),
}

// ====

#[derive(Clone, Debug, Serialize, Deserialize)]
struct ChatRequest {
model: String,
messages: Vec<MessageData>,
}

fn unique_agent_id(agent_id: &AgentId, server_address: &str) -> AgentId {
joulei marked this conversation as resolved.
Show resolved Hide resolved
AgentId(format!("{}-{}", agent_id.0, server_address))
}
Binary file added resources/images/circle_check_icon.png
joulei marked this conversation as resolved.
Show resolved Hide resolved
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added resources/images/globe_icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added resources/images/laptop_icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added resources/images/loader_icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added resources/images/makepad_expert_agent_icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added resources/images/refresh_error_icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
@@ -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::*;
Expand All @@ -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::*;
Expand Down Expand Up @@ -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
Expand Down
41 changes: 33 additions & 8 deletions src/chat/chat_history.rs
Original file line number Diff line number Diff line change
@@ -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::*;
Expand All @@ -24,7 +25,16 @@ live_design! {
margin: {left: 4, bottom: 4},
draw_text:{
text_style: <BOLD_FONT>{font_size: 10},
color: #667085
color: #3
}
}

NoAgentsWarning = <Label> {
margin: {left: 4, bottom: 4},
width: Fill
draw_text:{
text_style: {font_size: 8.5},
color: #3
}
}

Expand All @@ -48,7 +58,10 @@ live_design! {
list = <PortalList> {
drag_scrolling: false,
AgentHeading = <HeadingLabel> { text: "AGENTS" }
Agent = <EntityButton> {}
NoAgentsWarning = <NoAgentsWarning> {}
Agent = <EntityButton> {
server_url_visible: true,
}
ChatsHeading = <HeadingLabel> { text: "CHATS", margin: {top: 10}, }
ChatHistoryCard = <ChatHistoryCard> {
padding: {top: 4}
Expand Down Expand Up @@ -94,12 +107,13 @@ impl Widget for ChatHistory {
}

fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep {
let store = scope.data.get::<Store>().unwrap();
let agents = MofaBackend::available_agents();
let store = scope.data.get_mut::<Store>().unwrap();
let agents = store.chats.get_agents_list();

enum Item<'a> {
ChatsHeader,
AgentsHeader,
NoAgentsWarning(&'a str),
joulei marked this conversation as resolved.
Show resolved Hide resolved
AgentButton(&'a MofaAgent),
ChatButton(&'a ChatID),
}
Expand All @@ -108,9 +122,15 @@ impl Widget for ChatHistory {

if moly_mofa::should_be_visible() {
items.push(Item::AgentsHeader);

for agent in &agents {
items.push(Item::AgentButton(agent));
match store.chats.are_agents_available() {
joulei marked this conversation as resolved.
Show resolved Hide resolved
AgentsAvailability::NoServers => items.push(Item::NoAgentsWarning("Not connected to any MoFa servers.")),
AgentsAvailability::ServersNotConnected => items.push(Item::NoAgentsWarning("Could not connect to some servers.\nCheck your MoFa settings.")),
AgentsAvailability::NoAgents => items.push(Item::NoAgentsWarning("No agents found in the connected servers.")),
AgentsAvailability::Available => {
for agent in &agents {
items.push(Item::AgentButton(agent));
}
}
}
}

Expand Down Expand Up @@ -146,6 +166,11 @@ impl Widget for ChatHistory {
let item = list.item(cx, item_id, live_id!(AgentHeading));
item.draw_all(cx, scope);
}
Item::NoAgentsWarning(text) => {
let item = list.item(cx, item_id, live_id!(NoAgentsWarning));
item.set_text(text);
item.draw_all(cx, scope);
}
Item::AgentButton(agent) => {
let item = list.item(cx, item_id, live_id!(Agent));
item.as_entity_button().set_agent(agent);
Expand Down
Loading