-
Notifications
You must be signed in to change notification settings - Fork 23
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
base: dev
Are you sure you want to change the base?
Conversation
There was a problem hiding this 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
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()), | ||
} | ||
} |
There was a problem hiding this comment.
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>) { |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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()
.
|
||
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)) |
There was a problem hiding this comment.
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), |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
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
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()); |
There was a problem hiding this comment.
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);
}
There was a problem hiding this 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(); |
There was a problem hiding this comment.
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
@@ -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. |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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>, |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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.")), |
There was a problem hiding this comment.
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
ConnectionStatusIcon = <View> { | ||
visible: false | ||
cursor: Hand |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
:o
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
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