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

Open
wants to merge 16 commits into
base: dev
Choose a base branch
from

Conversation

joulei
Copy link
Collaborator

@joulei joulei commented Dec 11, 2024

Changes

This PR adds support for multiple MoFa severs and multiple agents.
I've extended the settings UI to support adding servers at runtime, and testing the connection.

Limitations

  • Currently we don't support editing a server address, this is because modifying a server address while in use adds a lot of complexity (some kind of mutex around the address, agent list diff reconciliation, agent id reconciliation, etc.). We could explore this in the future.
  • There are some minor improvements we can tackle separately:
    • Add confirmation dialog when deleting a server
    • Supporting server shutdown and turn on - we could keep the servers on the list but turn them on/off on demand
    • Periodically check for server connection health and agent list refresh
  • Most MoFa server don't yet support sending an actual list of agents on the v1/models endpoint, therefore we're still hardcoding a Reasoner agent.

Screen recordings

Adding a new server and talking to an agent:

Screen.Recording.2024-12-13.at.4.24.40.PM.mov

Handling of no agents, disconnected servers or no servers:

Screen.Recording.Dec.13.2024.mp4

@joulei joulei marked this pull request as ready for review December 13, 2024 07:39
@joulei joulei requested a review from noxware December 13, 2024 07:52
Copy link
Collaborator

@noxware noxware left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have to pause for a moment, I will finish the review when back

moly-mofa/src/lib.rs Show resolved Hide resolved
Comment on lines +76 to +87
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()),
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't be more idiomatic and explicit to just deal with Option<MofaAgent> when necessary?

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>) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: add Sender to the use statement we already have and shorten all the mpsc::Sender

///
/// 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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, since we are doing .recv().unwrap(), the channel being closed is also an unrecoverable error.

If we want the thread to finish gracefully, we would need to do something like

while let Some(message) = command_receiver.recv() { ... }

instead of the loop with unwrap().

Comment on lines +164 to +168

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))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need to move address itself inside the coroutine. The async block is needed to run the async task, but the request can be defined outside and moved later. Making this clone unnecessary with almost no changes.

let req = client
    .post(format!("{}/v1/chat/completions", &address))
    .json(&data);

current_request = Some(rt.spawn(async move {
    let resp = req.send().await.expect("Failed to send request");

   // everything else stays the same...
}));


enum Item<'a> {
ChatsHeader,
AgentsHeader,
NoAgentsWarning(&'a str),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe irrelevant note: I don't know if it is more idiomatic, but as long as this is not dynamic, this can be 'static, which may be a more informative lifetime, as you could quickly know by reading this line that messages are pre-defined and never constructed at runtime.

.available_agents
.get(&agent_id)
.cloned()
.unwrap_or_default();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mmm i think it may be better to ensure we can't start a chat with an unrecognized agent, and panic or eprintln something here to indicate a dev mistake

.chats.available_agents
.get(&agent_id)
.cloned()
.unwrap_or_default();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would divide this flow to handle the case of agent missing, or at least, if you really want to do this, I would make a constructor MofaAgent::unknown() or something similar instead of using the default as that

choose your pill

@@ -135,7 +154,7 @@ impl EntityButton {
}

pub fn set_agent(&mut self, agent: &MofaAgent) {
self.set_entity(ChatEntityRef::Agent(agent));
self.set_entity(ChatEntityRef::Agent(&agent));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agent is already of type &MofaAgent so the added & is not necessary

Comment on lines +129 to +131
pub fn set_agent(&mut self, agent: &MofaAgent) {
let Some(mut inner) = self.borrow_mut() else { return };
inner.entity = ModelSelectorEntity::Agent(agent);
inner.entity = ModelSelectorEntity::Agent(agent.clone());
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You want to work with an owned MofaAgent, so keep taking an owned MofaAgent :P

I would avoid cloning inside the function if possible and let the caller clone if it's really necessary.

Does inverting the two last lines work?

pub fn set_agent(&mut self, agent: MofaAgent) {
    let Some(mut inner) = self.borrow_mut() else {
        return;
    };
    inner.chat_agent_avatar(id!(avatar)).set_agent(&agent);
    inner.entity = ModelSelectorEntity::Agent(agent);
}

Copy link
Collaborator

@noxware noxware left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ufff took me a lot longer than I expected but it's done, not the perfect review but it's a review :D

@@ -75,11 +74,11 @@ impl Widget for ModelSelectorList {
}

fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep {
let store = scope.data.get::<Store>().unwrap();
let mut store = scope.data.get_mut::<Store>().unwrap();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems like store doesn't need to be mutable

src/data/chats/chat.rs Show resolved Hide resolved
@@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize};
pub enum ChatEntityId {
ModelFile(FileID),
/// Since agents are currently fixed enum values, the agent itself is the identifier.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Time to remove this comment, thanks :)

use std::{cell::RefCell, path::PathBuf, rc::Rc};

use super::filesystem::setup_chats_folder;

#[derive(Clone, Debug)]
pub struct MofaServer {
pub address: String,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the client already contains the address 🤔

#[derive(Clone, Debug)]
pub struct MofaServer {
pub address: String,
pub client: Rc<MofaClient>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why wrapping it in an Rc?

@@ -149,14 +158,15 @@ impl Widget for ModelList {
}

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();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_mut but variable is not mutable, so probably unnecessary

*margin_bottom = 0.0;
let agents_availability = store.chats.are_agents_available();
match agents_availability {
AgentsAvailability::NoServers => items.push(Item::NoAgentsWarning("Not connected to any MoFa servers.")),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmmm... i suppose these strings are the same as in chat_history, maybe you could add a to_human_message()-like function to AgentsAvailability if you want to dedup, but I also like messages defined in the UI itself so, don't mind, just thinking aloud

Comment on lines +26 to +28
ConnectionStatusIcon = <View> {
visible: false
cursor: Hand
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this as a whole is a button, not an icon, right?

color: #000
separator = <View> {
margin: {left: 20, right: 20, top: 0, bottom: 10}
width: Fill,
Copy link
Collaborator

@noxware noxware Dec 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stupid comment crossing my mind: views are width: Fill, height: Fill by default. We are not being explicit about height being Fill, but we are explicit on width being Fill. Shall we remove width: Fill? add height? LET'S CHANGE THE WHOLE CODEBASE TO BE CONSISTENT :D 🔥

}

let server = &servers[item_id].clone();
let mut item_scope = Scope::with_props(server);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:o

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants