Initial commit
This commit is contained in:
418
src/controller/agent/create/mod.rs
Normal file
418
src/controller/agent/create/mod.rs
Normal file
@@ -0,0 +1,418 @@
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use mxlink::MessageResponseType;
|
||||
|
||||
use crate::agent::provider::{ControllerTrait, PingResult};
|
||||
use crate::agent::PublicIdentifier;
|
||||
use crate::agent::{create_from_provider_and_yaml_value_config, AgentDefinition};
|
||||
use crate::agent::{AgentInstance, AgentProvider};
|
||||
use crate::controller::utils::get_text_body_or_complain;
|
||||
use crate::entity::globalconfig::GlobalConfigurationManager;
|
||||
use crate::entity::roomconfig::RoomConfigurationManager;
|
||||
use crate::strings;
|
||||
use crate::{entity::MessageContext, Bot};
|
||||
|
||||
struct ParsedAgentConfig {
|
||||
agent: AgentInstance,
|
||||
config: serde_yaml::Value,
|
||||
}
|
||||
|
||||
pub async fn handle_room_local(
|
||||
bot: &Bot,
|
||||
room_config_manager: &tokio::sync::Mutex<RoomConfigurationManager>,
|
||||
message_context: &MessageContext,
|
||||
provider: &str,
|
||||
agent_id_prefixless: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
if !message_context.sender_can_manage_room_local_agents()? {
|
||||
bot.messaging()
|
||||
.send_error_markdown_no_fail(
|
||||
message_context.room(),
|
||||
&strings::agent::not_allowed_to_manage_room_local_agents_in_room(),
|
||||
MessageResponseType::Reply(message_context.thread_info().root_event_id.clone()),
|
||||
)
|
||||
.await;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let Ok(provider) = AgentProvider::from_string(provider) else {
|
||||
bot.messaging()
|
||||
.send_error_markdown_no_fail(
|
||||
message_context.room(),
|
||||
&strings::provider::invalid(provider),
|
||||
MessageResponseType::Reply(message_context.thread_info().root_event_id.clone()),
|
||||
)
|
||||
.await;
|
||||
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let agent_identifier = PublicIdentifier::DynamicRoomLocal(agent_id_prefixless.to_owned());
|
||||
if let Err(err) = agent_identifier.validate() {
|
||||
bot.messaging()
|
||||
.send_error_markdown_no_fail(
|
||||
message_context.room(),
|
||||
&strings::agent::invalid_id_validation_error(err),
|
||||
MessageResponseType::Reply(message_context.thread_info().root_event_id.clone()),
|
||||
)
|
||||
.await;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let agent_exists = bot
|
||||
.agent_manager()
|
||||
.available_room_agents_by_room_config_context(message_context.room_config_context())
|
||||
.iter()
|
||||
.any(|agent| *agent.identifier() == agent_identifier);
|
||||
|
||||
if agent_exists {
|
||||
bot.messaging()
|
||||
.send_error_markdown_no_fail(
|
||||
message_context.room(),
|
||||
&strings::agent::already_exists_see_help(agent_id_prefixless, bot.command_prefix()),
|
||||
MessageResponseType::InThread(message_context.thread_info().clone()),
|
||||
)
|
||||
.await;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if message_context.thread_info().is_thread_root_only() {
|
||||
return send_guide(bot, message_context, &agent_identifier, &provider).await;
|
||||
}
|
||||
|
||||
let Some(text_message_content) = get_text_body_or_complain(bot, message_context).await else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let parsed_config = parse_agent_config_from_message_or_complain(
|
||||
bot,
|
||||
message_context,
|
||||
&provider,
|
||||
&agent_identifier,
|
||||
text_message_content,
|
||||
)
|
||||
.await;
|
||||
let Some(parsed_config) = parsed_config else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
message_context.room().typing_notice(true).await?;
|
||||
|
||||
if !try_to_ping_agent_or_complain(bot, message_context, &parsed_config.agent).await {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let agent_definition = AgentDefinition::new(
|
||||
agent_identifier.prefixless(),
|
||||
provider,
|
||||
parsed_config.config.clone(),
|
||||
);
|
||||
|
||||
let mut room_config = message_context.room_config().clone();
|
||||
|
||||
room_config.agents.push(agent_definition.clone());
|
||||
|
||||
room_config_manager
|
||||
.lock()
|
||||
.await
|
||||
.persist(message_context.room(), &room_config)
|
||||
.await?;
|
||||
|
||||
send_completion_wrap_up(
|
||||
bot,
|
||||
message_context,
|
||||
&agent_identifier,
|
||||
&parsed_config.agent,
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_global(
|
||||
bot: &Bot,
|
||||
global_config_manager: &tokio::sync::Mutex<GlobalConfigurationManager>,
|
||||
message_context: &MessageContext,
|
||||
provider: &str,
|
||||
agent_id_prefixless: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
if !message_context.sender_can_manage_global_config()? {
|
||||
bot.messaging()
|
||||
.send_error_markdown_no_fail(
|
||||
message_context.room(),
|
||||
strings::global_config::no_permissions_to_administrate(),
|
||||
MessageResponseType::Reply(message_context.thread_info().root_event_id.clone()),
|
||||
)
|
||||
.await;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let Ok(provider) = AgentProvider::from_string(provider) else {
|
||||
bot.messaging()
|
||||
.send_error_markdown_no_fail(
|
||||
message_context.room(),
|
||||
&strings::provider::invalid(provider),
|
||||
MessageResponseType::Reply(message_context.thread_info().root_event_id.clone()),
|
||||
)
|
||||
.await;
|
||||
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let agent_identifier = PublicIdentifier::DynamicGlobal(agent_id_prefixless.to_owned());
|
||||
if let Err(err) = agent_identifier.validate() {
|
||||
bot.messaging()
|
||||
.send_error_markdown_no_fail(
|
||||
message_context.room(),
|
||||
&strings::agent::invalid_id_validation_error(err),
|
||||
MessageResponseType::Reply(message_context.thread_info().root_event_id.clone()),
|
||||
)
|
||||
.await;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let agent_exists = bot
|
||||
.agent_manager()
|
||||
.available_room_agents_by_room_config_context(message_context.room_config_context())
|
||||
.iter()
|
||||
.any(|agent| *agent.identifier() == agent_identifier);
|
||||
|
||||
if agent_exists {
|
||||
bot.messaging()
|
||||
.send_error_markdown_no_fail(
|
||||
message_context.room(),
|
||||
&strings::agent::already_exists_see_help(agent_id_prefixless, bot.command_prefix()),
|
||||
MessageResponseType::InThread(message_context.thread_info().clone()),
|
||||
)
|
||||
.await;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if message_context.thread_info().is_thread_root_only() {
|
||||
return send_guide(bot, message_context, &agent_identifier, &provider).await;
|
||||
}
|
||||
|
||||
let Some(text_message_content) = get_text_body_or_complain(bot, message_context).await else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let parsed_config = parse_agent_config_from_message_or_complain(
|
||||
bot,
|
||||
message_context,
|
||||
&provider,
|
||||
&agent_identifier,
|
||||
text_message_content,
|
||||
)
|
||||
.await;
|
||||
let Some(parsed_config) = parsed_config else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
message_context.room().typing_notice(true).await?;
|
||||
|
||||
if !try_to_ping_agent_or_complain(bot, message_context, &parsed_config.agent).await {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let agent_definition = AgentDefinition::new(
|
||||
agent_identifier.prefixless(),
|
||||
provider,
|
||||
parsed_config.config.clone(),
|
||||
);
|
||||
|
||||
let mut global_config = message_context.global_config().clone();
|
||||
global_config.agents.push(agent_definition.clone());
|
||||
|
||||
global_config_manager
|
||||
.lock()
|
||||
.await
|
||||
.persist(&global_config)
|
||||
.await?;
|
||||
|
||||
send_completion_wrap_up(
|
||||
bot,
|
||||
message_context,
|
||||
&agent_identifier,
|
||||
&parsed_config.agent,
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_guide(
|
||||
bot: &Bot,
|
||||
message_context: &MessageContext,
|
||||
agent_identifier: &PublicIdentifier,
|
||||
provider: &AgentProvider,
|
||||
) -> anyhow::Result<()> {
|
||||
let sample_config = crate::agent::default_config_for_provider(provider);
|
||||
let sample_config_pretty_yaml = serde_yaml::to_string(&sample_config)?;
|
||||
|
||||
bot.messaging()
|
||||
.send_text_markdown_no_fail(
|
||||
message_context.room(),
|
||||
strings::agent::creation_guide(agent_identifier, provider, &sample_config_pretty_yaml),
|
||||
MessageResponseType::InThread(message_context.thread_info().clone()),
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_from_message_to_yaml_value(text: &str) -> Result<serde_yaml::Value, String> {
|
||||
let mut text = text.trim();
|
||||
|
||||
if text.starts_with("```") {
|
||||
// Try to strip ```yml and ```yaml first and fall back to the generic ``` later.
|
||||
text = text.trim_start_matches("```yml");
|
||||
text = text.trim_start_matches("```yaml");
|
||||
text = text.trim_start_matches("```");
|
||||
text = text.trim_end_matches("```");
|
||||
}
|
||||
|
||||
let config: serde_yaml::Value = serde_yaml::from_str(text).map_err(|e| e.to_string())?;
|
||||
|
||||
match config {
|
||||
serde_yaml::Value::Mapping(_) => {}
|
||||
_ => {
|
||||
return Err("Not a valid YAML hashmap".to_owned());
|
||||
}
|
||||
};
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
async fn parse_agent_config_from_message_or_complain(
|
||||
bot: &Bot,
|
||||
message_context: &MessageContext,
|
||||
provider: &AgentProvider,
|
||||
agent_identifier: &PublicIdentifier,
|
||||
text: &str,
|
||||
) -> Option<ParsedAgentConfig> {
|
||||
let config_yaml_value = parse_from_message_to_yaml_value(text);
|
||||
|
||||
let config_yaml_value = match config_yaml_value {
|
||||
Ok(config_yaml_value) => config_yaml_value,
|
||||
Err(err) => {
|
||||
bot.messaging()
|
||||
.send_error_markdown_no_fail(
|
||||
message_context.room(),
|
||||
&strings::agent::configuration_not_a_valid_yaml_hashmap(err),
|
||||
MessageResponseType::InThread(message_context.thread_info().clone()),
|
||||
)
|
||||
.await;
|
||||
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let agent = create_from_provider_and_yaml_value_config(
|
||||
provider,
|
||||
agent_identifier,
|
||||
config_yaml_value.clone(),
|
||||
);
|
||||
|
||||
let agent = match agent {
|
||||
Ok(agent) => ParsedAgentConfig {
|
||||
agent,
|
||||
config: config_yaml_value,
|
||||
},
|
||||
Err(err) => {
|
||||
bot.messaging()
|
||||
.send_error_markdown_no_fail(
|
||||
message_context.room(),
|
||||
&strings::provider::invalid_configuration_for_provider(provider, err),
|
||||
MessageResponseType::InThread(message_context.thread_info().clone()),
|
||||
)
|
||||
.await;
|
||||
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
Some(agent)
|
||||
}
|
||||
|
||||
async fn try_to_ping_agent_or_complain(
|
||||
bot: &Bot,
|
||||
message_context: &MessageContext,
|
||||
agent_instance: &AgentInstance,
|
||||
) -> bool {
|
||||
bot.messaging()
|
||||
.send_notice_markdown_no_fail(
|
||||
message_context.room(),
|
||||
format!("⏳ {}", strings::agent::configuration_agent_will_ping()),
|
||||
MessageResponseType::InThread(message_context.thread_info().clone()),
|
||||
)
|
||||
.await;
|
||||
|
||||
match agent_instance.controller().ping().await {
|
||||
Ok(ping_result) => {
|
||||
let message = match ping_result {
|
||||
PingResult::Inconclusive => format!(
|
||||
"❓ {}",
|
||||
strings::agent::configuration_agent_ping_inconclusive()
|
||||
),
|
||||
PingResult::Successful => {
|
||||
format!("✅ {}", strings::agent::configuration_agent_ping_ok())
|
||||
}
|
||||
};
|
||||
|
||||
bot.messaging()
|
||||
.send_notice_markdown_no_fail(
|
||||
message_context.room(),
|
||||
message,
|
||||
MessageResponseType::InThread(message_context.thread_info().clone()),
|
||||
)
|
||||
.await;
|
||||
|
||||
true
|
||||
}
|
||||
Err(err) => {
|
||||
bot.messaging()
|
||||
.send_error_markdown_no_fail(
|
||||
message_context.room(),
|
||||
&strings::agent::configuration_does_not_result_in_a_working_agent(err),
|
||||
MessageResponseType::InThread(message_context.thread_info().clone()),
|
||||
)
|
||||
.await;
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_completion_wrap_up(
|
||||
bot: &Bot,
|
||||
message_context: &MessageContext,
|
||||
agent_identifier: &PublicIdentifier,
|
||||
agent_instance: &AgentInstance,
|
||||
) {
|
||||
bot.messaging()
|
||||
.send_success_markdown_no_fail(
|
||||
message_context.room(),
|
||||
&strings::agent::created(agent_identifier),
|
||||
MessageResponseType::InThread(message_context.thread_info().clone()),
|
||||
)
|
||||
.await;
|
||||
|
||||
bot.messaging()
|
||||
.send_tooltip_markdown_no_fail(
|
||||
message_context.room(),
|
||||
&strings::agent::post_creation_helpful_commands(
|
||||
agent_identifier,
|
||||
agent_instance,
|
||||
bot.command_prefix(),
|
||||
),
|
||||
MessageResponseType::InThread(message_context.thread_info().clone()),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
61
src/controller/agent/create/tests.rs
Normal file
61
src/controller/agent/create/tests.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
#[test]
|
||||
fn agent_config_parsing_works() {
|
||||
struct TestCase {
|
||||
input: String,
|
||||
expected: Option<serde_yaml::Value>,
|
||||
}
|
||||
|
||||
let provider = crate::agent::AgentProvider::OpenAI;
|
||||
let sample_config = crate::agent::default_config_for_provider(&provider);
|
||||
let sample_config_pretty_yaml = serde_yaml::to_string(&sample_config).unwrap();
|
||||
|
||||
let test_cases = vec![
|
||||
// Invalid input
|
||||
TestCase {
|
||||
input: r#"Hello"#.to_owned(),
|
||||
expected: None,
|
||||
},
|
||||
// Plain text
|
||||
TestCase {
|
||||
input: sample_config_pretty_yaml.clone(),
|
||||
expected: Some(sample_config.clone()),
|
||||
},
|
||||
// Generic code block
|
||||
TestCase {
|
||||
input: format!("```\n{}```", sample_config_pretty_yaml),
|
||||
expected: Some(sample_config.clone()),
|
||||
},
|
||||
// YAML code block (yaml)
|
||||
TestCase {
|
||||
input: format!("```yaml\n{}```", sample_config_pretty_yaml),
|
||||
expected: Some(sample_config.clone()),
|
||||
},
|
||||
// YAML code block (yml)
|
||||
TestCase {
|
||||
input: format!("```yml\n{}```", sample_config_pretty_yaml),
|
||||
expected: Some(sample_config.clone()),
|
||||
},
|
||||
// JSON code block
|
||||
TestCase {
|
||||
input: format!("```json\n{}```", sample_config_pretty_yaml),
|
||||
expected: None,
|
||||
},
|
||||
];
|
||||
|
||||
for (i, test_case) in test_cases.iter().enumerate() {
|
||||
let result = super::parse_from_message_to_yaml_value(&test_case.input);
|
||||
match result {
|
||||
Ok(config) => {
|
||||
assert_eq!(
|
||||
config,
|
||||
test_case.expected.clone().unwrap(),
|
||||
"Test case {} failed",
|
||||
i
|
||||
);
|
||||
}
|
||||
Err(_) => {
|
||||
assert_eq!(test_case.expected, None, "Test case {} failed", i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
195
src/controller/agent/delete/mod.rs
Normal file
195
src/controller/agent/delete/mod.rs
Normal file
@@ -0,0 +1,195 @@
|
||||
use mxlink::MessageResponseType;
|
||||
|
||||
use crate::entity::{
|
||||
globalconfig::GlobalConfigurationManager, roomconfig::RoomConfigurationManager, MessageContext,
|
||||
};
|
||||
use crate::{agent::PublicIdentifier, strings, Bot};
|
||||
|
||||
pub async fn handle(
|
||||
bot: &Bot,
|
||||
room_config_manager: &tokio::sync::Mutex<RoomConfigurationManager>,
|
||||
global_config_manager: &tokio::sync::Mutex<GlobalConfigurationManager>,
|
||||
message_context: &MessageContext,
|
||||
agent_identifier: &PublicIdentifier,
|
||||
) -> anyhow::Result<()> {
|
||||
let agents = bot
|
||||
.agent_manager()
|
||||
.available_room_agents_by_room_config_context(message_context.room_config_context());
|
||||
|
||||
let agent = agents.iter().find(|a| a.identifier() == agent_identifier);
|
||||
|
||||
let Some(_) = agent else {
|
||||
bot.messaging()
|
||||
.send_error_markdown_no_fail(
|
||||
message_context.room(),
|
||||
&strings::agent::agent_with_given_identifier_missing(agent_identifier),
|
||||
MessageResponseType::Reply(message_context.thread_info().root_event_id.clone()),
|
||||
)
|
||||
.await;
|
||||
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
match &agent_identifier {
|
||||
PublicIdentifier::DynamicRoomLocal(_) => {
|
||||
if !message_context.sender_can_manage_room_local_agents()? {
|
||||
bot.messaging()
|
||||
.send_error_markdown_no_fail(
|
||||
message_context.room(),
|
||||
&strings::agent::not_allowed_to_manage_room_local_agents_in_room(),
|
||||
MessageResponseType::Reply(
|
||||
message_context.thread_info().root_event_id.clone(),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
delete_room_local_agent(bot, room_config_manager, message_context, agent_identifier)
|
||||
.await
|
||||
}
|
||||
PublicIdentifier::DynamicGlobal(_) => {
|
||||
if !message_context.sender_can_manage_global_config()? {
|
||||
bot.messaging()
|
||||
.send_error_markdown_no_fail(
|
||||
message_context.room(),
|
||||
strings::global_config::no_permissions_to_administrate(),
|
||||
MessageResponseType::Reply(
|
||||
message_context.thread_info().root_event_id.clone(),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
delete_global_agent(
|
||||
bot,
|
||||
global_config_manager,
|
||||
message_context,
|
||||
agent_identifier,
|
||||
)
|
||||
.await
|
||||
}
|
||||
PublicIdentifier::Static(_) => {
|
||||
bot.messaging()
|
||||
.send_error_markdown_no_fail(
|
||||
message_context.room(),
|
||||
&strings::agent::not_allowed_to_manage_static_agents(),
|
||||
MessageResponseType::Reply(message_context.thread_info().root_event_id.clone()),
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn delete_room_local_agent(
|
||||
bot: &Bot,
|
||||
room_config_manager: &tokio::sync::Mutex<RoomConfigurationManager>,
|
||||
message_context: &MessageContext,
|
||||
agent_id: &PublicIdentifier,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut room_config = message_context.room_config().clone();
|
||||
|
||||
let mut was_deleted = false;
|
||||
|
||||
let agent_id_prefixless = agent_id.prefixless();
|
||||
|
||||
let mut agents = Vec::new();
|
||||
for agent_config in room_config.agents {
|
||||
if agent_config.id == agent_id_prefixless {
|
||||
was_deleted = true;
|
||||
} else {
|
||||
agents.push(agent_config.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if !was_deleted {
|
||||
bot.messaging()
|
||||
.send_error_markdown_no_fail(
|
||||
message_context.room(),
|
||||
&strings::agent::agent_with_given_identifier_missing(agent_id),
|
||||
MessageResponseType::Reply(message_context.thread_info().root_event_id.clone()),
|
||||
)
|
||||
.await;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
room_config.agents = agents;
|
||||
|
||||
let room_config_manager = room_config_manager.lock().await;
|
||||
|
||||
// We may unset all handlers in the room config which refer to this agent.
|
||||
// We intentionally do not do this, because we do not support "agent edit" yet and ask people to do "agent delete" and "agent create" instead.
|
||||
// We'd rather not magically reconfigure the room on agent deletion and obstruct this use case.
|
||||
|
||||
room_config_manager
|
||||
.persist(message_context.room(), &room_config)
|
||||
.await?;
|
||||
|
||||
bot.messaging()
|
||||
.send_success_markdown_no_fail(
|
||||
message_context.room(),
|
||||
&strings::agent::removed_room_local(agent_id, bot.command_prefix()),
|
||||
MessageResponseType::Reply(message_context.thread_info().root_event_id.clone()),
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_global_agent(
|
||||
bot: &Bot,
|
||||
global_config_manager: &tokio::sync::Mutex<GlobalConfigurationManager>,
|
||||
message_context: &MessageContext,
|
||||
agent_id: &PublicIdentifier,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut global_config = message_context.global_config().clone();
|
||||
|
||||
let mut was_deleted = false;
|
||||
|
||||
let agent_id_prefixless = agent_id.prefixless();
|
||||
|
||||
let mut agents = Vec::new();
|
||||
for agent_config in global_config.agents {
|
||||
if agent_config.id == agent_id_prefixless {
|
||||
was_deleted = true;
|
||||
} else {
|
||||
agents.push(agent_config.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if !was_deleted {
|
||||
bot.messaging()
|
||||
.send_error_markdown_no_fail(
|
||||
message_context.room(),
|
||||
&strings::agent::agent_with_given_identifier_missing(agent_id),
|
||||
MessageResponseType::Reply(message_context.thread_info().root_event_id.clone()),
|
||||
)
|
||||
.await;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
global_config.agents = agents;
|
||||
|
||||
global_config_manager
|
||||
.lock()
|
||||
.await
|
||||
.persist(&global_config)
|
||||
.await?;
|
||||
|
||||
bot.messaging()
|
||||
.send_success_markdown_no_fail(
|
||||
message_context.room(),
|
||||
&strings::agent::removed_global(agent_id, bot.command_prefix()),
|
||||
MessageResponseType::Reply(message_context.thread_info().root_event_id.clone()),
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
84
src/controller/agent/details/mod.rs
Normal file
84
src/controller/agent/details/mod.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
use mxlink::MessageResponseType;
|
||||
|
||||
use crate::{agent::PublicIdentifier, entity::MessageContext, strings, Bot};
|
||||
|
||||
pub async fn handle(
|
||||
bot: &Bot,
|
||||
message_context: &MessageContext,
|
||||
agent_identifier: &PublicIdentifier,
|
||||
) -> anyhow::Result<()> {
|
||||
let agents = bot
|
||||
.agent_manager()
|
||||
.available_room_agents_by_room_config_context(message_context.room_config_context());
|
||||
|
||||
let agent = agents.iter().find(|a| a.identifier() == agent_identifier);
|
||||
|
||||
let agent = match agent {
|
||||
Some(agent) => agent,
|
||||
None => {
|
||||
bot.messaging()
|
||||
.send_error_markdown_no_fail(
|
||||
message_context.room(),
|
||||
&strings::agent::agent_with_given_identifier_missing(agent_identifier),
|
||||
MessageResponseType::Reply(message_context.thread_info().root_event_id.clone()),
|
||||
)
|
||||
.await;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
// Access checks
|
||||
|
||||
match &agent_identifier {
|
||||
PublicIdentifier::DynamicRoomLocal(_) => {
|
||||
if !message_context.sender_can_manage_room_local_agents()? {
|
||||
bot.messaging()
|
||||
.send_error_markdown_no_fail(
|
||||
message_context.room(),
|
||||
&strings::agent::not_allowed_to_manage_room_local_agents_in_room(),
|
||||
MessageResponseType::Reply(
|
||||
message_context.thread_info().root_event_id.clone(),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
PublicIdentifier::DynamicGlobal(_) => {
|
||||
if !message_context.sender_can_manage_global_config()? {
|
||||
bot.messaging()
|
||||
.send_error_markdown_no_fail(
|
||||
message_context.room(),
|
||||
strings::global_config::no_permissions_to_administrate(),
|
||||
MessageResponseType::Reply(
|
||||
message_context.thread_info().root_event_id.clone(),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
PublicIdentifier::Static(_) => {}
|
||||
};
|
||||
|
||||
let config_yaml_pretty = serde_yaml::to_string(&agent.definition().config)?;
|
||||
|
||||
bot.messaging()
|
||||
.send_text_markdown_no_fail(
|
||||
message_context.room(),
|
||||
format!(
|
||||
"Configuration for agent `{}` (powered by the `{}` provider):\n```yml\n{}\n```",
|
||||
agent_identifier,
|
||||
agent.definition().provider.to_static_str(),
|
||||
config_yaml_pretty.trim(),
|
||||
)
|
||||
.to_owned(),
|
||||
MessageResponseType::Reply(message_context.thread_info().root_event_id.clone()),
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
99
src/controller/agent/determination/mod.rs
Normal file
99
src/controller/agent/determination/mod.rs
Normal file
@@ -0,0 +1,99 @@
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use crate::{agent::PublicIdentifier, controller::ControllerType, strings};
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum AgentControllerType {
|
||||
List,
|
||||
Details(PublicIdentifier),
|
||||
CreateRoomLocal { provider: String, agent_id: String },
|
||||
CreateGlobal { provider: String, agent_id: String },
|
||||
Delete(PublicIdentifier),
|
||||
Help,
|
||||
}
|
||||
|
||||
pub fn determine_controller(command_prefix: &str, text: &str) -> ControllerType {
|
||||
if text.starts_with("list") {
|
||||
return ControllerType::Agent(AgentControllerType::List);
|
||||
}
|
||||
if let Some(agent_id_string) = text.strip_prefix("details") {
|
||||
let agent_id_string = agent_id_string.trim();
|
||||
|
||||
if agent_id_string.is_empty() || agent_id_string.contains(" ") {
|
||||
return ControllerType::Error(
|
||||
strings::agent::incorrect_invocation_expects_agent_id_arg(command_prefix),
|
||||
);
|
||||
}
|
||||
|
||||
let Some(agent_identifier) = PublicIdentifier::from_str(agent_id_string) else {
|
||||
return ControllerType::Error(strings::agent::invalid_id_generic());
|
||||
};
|
||||
|
||||
return ControllerType::Agent(AgentControllerType::Details(agent_identifier));
|
||||
}
|
||||
|
||||
if let Some(remaining_text) = text.strip_prefix("create-room-local") {
|
||||
// `remaining_text` should be something like: `PROVIDER ID`
|
||||
let remaining_text = remaining_text.trim();
|
||||
|
||||
let parts = remaining_text.split_once(' ');
|
||||
let Some((provider, agent_id_string)) = parts else {
|
||||
return ControllerType::Error(strings::agent::incorrect_creation_invocation(
|
||||
command_prefix,
|
||||
));
|
||||
};
|
||||
|
||||
if agent_id_string.contains(" ") {
|
||||
return ControllerType::Error(strings::agent::incorrect_creation_invocation(
|
||||
command_prefix,
|
||||
));
|
||||
}
|
||||
|
||||
return ControllerType::Agent(AgentControllerType::CreateRoomLocal {
|
||||
provider: provider.to_owned(),
|
||||
agent_id: agent_id_string.trim().to_owned(),
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(remaining_text) = text.strip_prefix("create-global") {
|
||||
// `remaining_text` should be something like: `PROVIDER ID`
|
||||
let remaining_text = remaining_text.trim();
|
||||
|
||||
let parts = remaining_text.split_once(' ');
|
||||
let Some((provider, agent_id_string)) = parts else {
|
||||
return ControllerType::Error(strings::agent::incorrect_creation_invocation(
|
||||
command_prefix,
|
||||
));
|
||||
};
|
||||
|
||||
if agent_id_string.contains(" ") {
|
||||
return ControllerType::Error(strings::agent::incorrect_creation_invocation(
|
||||
command_prefix,
|
||||
));
|
||||
}
|
||||
|
||||
return ControllerType::Agent(AgentControllerType::CreateGlobal {
|
||||
provider: provider.to_owned(),
|
||||
agent_id: agent_id_string.trim().to_owned(),
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(agent_id_string) = text.strip_prefix("delete") {
|
||||
let agent_id_string = agent_id_string.trim();
|
||||
|
||||
if agent_id_string.is_empty() || agent_id_string.contains(" ") {
|
||||
return ControllerType::Error(
|
||||
strings::agent::incorrect_invocation_expects_agent_id_arg(command_prefix),
|
||||
);
|
||||
}
|
||||
|
||||
let Some(agent_identifier) = PublicIdentifier::from_str(agent_id_string) else {
|
||||
return ControllerType::Error(strings::agent::invalid_id_generic());
|
||||
};
|
||||
|
||||
return ControllerType::Agent(AgentControllerType::Delete(agent_identifier));
|
||||
}
|
||||
|
||||
ControllerType::Agent(AgentControllerType::Help)
|
||||
}
|
||||
131
src/controller/agent/determination/tests.rs
Normal file
131
src/controller/agent/determination/tests.rs
Normal file
@@ -0,0 +1,131 @@
|
||||
#[test]
|
||||
fn determine_controller() {
|
||||
use crate::agent::PublicIdentifier;
|
||||
|
||||
struct TestCase {
|
||||
name: &'static str,
|
||||
input: &'static str,
|
||||
expected: super::ControllerType,
|
||||
}
|
||||
|
||||
let command_prefix = "!bai";
|
||||
|
||||
let test_cases = vec![
|
||||
TestCase {
|
||||
name: "Top-level is help",
|
||||
input: "",
|
||||
expected: super::ControllerType::Agent(super::AgentControllerType::Help),
|
||||
},
|
||||
TestCase {
|
||||
name: "Anything else goes to top-level",
|
||||
input: "whatever",
|
||||
expected: super::ControllerType::Agent(super::AgentControllerType::Help),
|
||||
},
|
||||
TestCase {
|
||||
name: "List",
|
||||
input: "list",
|
||||
expected: super::ControllerType::Agent(super::AgentControllerType::List),
|
||||
},
|
||||
TestCase {
|
||||
name: "details",
|
||||
input: "details static/agent-id",
|
||||
expected: super::ControllerType::Agent(super::AgentControllerType::Details(
|
||||
PublicIdentifier::Static("agent-id".to_owned()),
|
||||
)),
|
||||
},
|
||||
TestCase {
|
||||
name: "details with invalid agent identifier",
|
||||
input: "details agent-id",
|
||||
expected: super::ControllerType::Error(crate::strings::agent::invalid_id_generic()),
|
||||
},
|
||||
TestCase {
|
||||
name: "create-room-local no arguments",
|
||||
input: "create-room-local",
|
||||
expected: super::ControllerType::Error(
|
||||
crate::strings::agent::incorrect_creation_invocation(command_prefix),
|
||||
),
|
||||
},
|
||||
TestCase {
|
||||
name: "create-room-local only with provider",
|
||||
input: "create-room-local openai",
|
||||
expected: super::ControllerType::Error(
|
||||
crate::strings::agent::incorrect_creation_invocation(command_prefix),
|
||||
),
|
||||
},
|
||||
TestCase {
|
||||
name: "create-room-local correct",
|
||||
input: "create-room-local openai my-agent-id",
|
||||
expected: super::ControllerType::Agent(super::AgentControllerType::CreateRoomLocal {
|
||||
provider: "openai".to_owned(),
|
||||
agent_id: "my-agent-id".trim().to_owned(),
|
||||
}),
|
||||
},
|
||||
TestCase {
|
||||
name: "create-global extra arguments",
|
||||
input: "create-global openai my-agent-id more arguments here",
|
||||
expected: super::ControllerType::Error(
|
||||
crate::strings::agent::incorrect_creation_invocation(command_prefix),
|
||||
),
|
||||
},
|
||||
TestCase {
|
||||
name: "create-global no arguments",
|
||||
input: "create-global",
|
||||
expected: super::ControllerType::Error(
|
||||
crate::strings::agent::incorrect_creation_invocation(command_prefix),
|
||||
),
|
||||
},
|
||||
TestCase {
|
||||
name: "create-global only with provider",
|
||||
input: "create-global openai",
|
||||
expected: super::ControllerType::Error(
|
||||
crate::strings::agent::incorrect_creation_invocation(command_prefix),
|
||||
),
|
||||
},
|
||||
TestCase {
|
||||
name: "create-global correct",
|
||||
input: "create-global openai my-agent-id",
|
||||
expected: super::ControllerType::Agent(super::AgentControllerType::CreateGlobal {
|
||||
provider: "openai".to_owned(),
|
||||
agent_id: "my-agent-id".trim().to_owned(),
|
||||
}),
|
||||
},
|
||||
TestCase {
|
||||
name: "create-global extra arguments",
|
||||
input: "create-global openai my-agent-id more arguments here",
|
||||
expected: super::ControllerType::Error(
|
||||
crate::strings::agent::incorrect_creation_invocation(command_prefix),
|
||||
),
|
||||
},
|
||||
TestCase {
|
||||
name: "delete no arguments",
|
||||
input: "delete",
|
||||
expected: super::ControllerType::Error(
|
||||
crate::strings::agent::incorrect_invocation_expects_agent_id_arg(command_prefix),
|
||||
),
|
||||
},
|
||||
TestCase {
|
||||
name: "delete too many arguments",
|
||||
input: "delete agent-id extra arguments",
|
||||
expected: super::ControllerType::Error(
|
||||
crate::strings::agent::incorrect_invocation_expects_agent_id_arg(command_prefix),
|
||||
),
|
||||
},
|
||||
TestCase {
|
||||
name: "delete",
|
||||
input: "delete static/agent-id",
|
||||
expected: super::ControllerType::Agent(super::AgentControllerType::Delete(
|
||||
PublicIdentifier::Static("agent-id".to_owned()),
|
||||
)),
|
||||
},
|
||||
TestCase {
|
||||
name: "delete with invalid agent identifier",
|
||||
input: "delete agent-id",
|
||||
expected: super::ControllerType::Error(crate::strings::agent::invalid_id_generic()),
|
||||
},
|
||||
];
|
||||
|
||||
for test_case in test_cases {
|
||||
let result = super::determine_controller(command_prefix, test_case.input);
|
||||
assert_eq!(result, test_case.expected, "Test case: {}", test_case.name);
|
||||
}
|
||||
}
|
||||
79
src/controller/agent/help/mod.rs
Normal file
79
src/controller/agent/help/mod.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
use mxlink::MessageResponseType;
|
||||
|
||||
use crate::{entity::MessageContext, strings, Bot};
|
||||
|
||||
pub async fn handle(bot: &Bot, message_context: &MessageContext) -> anyhow::Result<()> {
|
||||
// Anyone can access this help command, because certain subcommands ("list")
|
||||
// are also useful to regular users and it'd be great for them to learn about them.
|
||||
|
||||
let mut message = String::new();
|
||||
|
||||
let can_manage_agents = message_context.sender_can_manage_room_local_agents()?;
|
||||
|
||||
message.push_str(&format!("## {}", strings::help::agent::heading()));
|
||||
message.push_str("\n\n");
|
||||
message.push_str(&strings::help::agent::intro(
|
||||
bot.command_prefix(),
|
||||
can_manage_agents,
|
||||
));
|
||||
message.push('\n');
|
||||
message.push_str(&strings::help::agent::intro_capabilities());
|
||||
message.push_str("\n\n");
|
||||
message.push_str(&strings::help::agent::intro_handler_relation(
|
||||
bot.command_prefix(),
|
||||
));
|
||||
|
||||
if can_manage_agents {
|
||||
message.push_str("\n\n");
|
||||
|
||||
message.push_str(strings::help::available_commands_intro());
|
||||
message.push('\n');
|
||||
|
||||
message.push_str(&strings::help::agent::list_agents(bot.command_prefix()));
|
||||
message.push('\n');
|
||||
|
||||
message.push_str(strings::help::agent::create_agent_intro());
|
||||
message.push('\n');
|
||||
message.push_str(&strings::help::agent::create_agent_room_local(
|
||||
bot.command_prefix(),
|
||||
));
|
||||
message.push('\n');
|
||||
|
||||
if message_context.sender_can_manage_global_config()? {
|
||||
message.push_str(&strings::help::agent::create_agent_global(
|
||||
bot.command_prefix(),
|
||||
));
|
||||
message.push('\n');
|
||||
}
|
||||
|
||||
message.push_str(&strings::help::agent::create_agent_example(
|
||||
bot.command_prefix(),
|
||||
));
|
||||
message.push('\n');
|
||||
|
||||
message.push_str(&strings::help::agent::show_agent_details(
|
||||
bot.command_prefix(),
|
||||
));
|
||||
message.push('\n');
|
||||
|
||||
message.push_str(&strings::help::agent::delete_agent(bot.command_prefix()));
|
||||
|
||||
message.push_str("\n\n");
|
||||
|
||||
message.push_str(strings::help::agent::available_commands_outro_update_note());
|
||||
} else {
|
||||
message.push_str("\n\n");
|
||||
|
||||
message.push_str(strings::help::agent::no_permission_to_create_agents());
|
||||
}
|
||||
|
||||
bot.messaging()
|
||||
.send_text_markdown_no_fail(
|
||||
message_context.room(),
|
||||
message,
|
||||
MessageResponseType::Reply(message_context.thread_info().root_event_id.clone()),
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
39
src/controller/agent/list/mod.rs
Normal file
39
src/controller/agent/list/mod.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use mxlink::MessageResponseType;
|
||||
|
||||
use crate::agent::AgentPurpose;
|
||||
use crate::strings;
|
||||
use crate::{entity::MessageContext, Bot};
|
||||
|
||||
pub async fn handle(bot: &Bot, message_context: &MessageContext) -> anyhow::Result<()> {
|
||||
let agents = bot
|
||||
.agent_manager()
|
||||
.available_room_agents_by_room_config_context(message_context.room_config_context());
|
||||
|
||||
let mut message = String::new();
|
||||
if agents.is_empty() {
|
||||
message.push_str(strings::agent::agent_list_empty().as_str());
|
||||
} else {
|
||||
message.push_str(&strings::agent::non_empty_agent_list_block(&agents));
|
||||
message.push_str("\n\n");
|
||||
|
||||
message.push_str(strings::agent::agent_list_legend_intro().as_str());
|
||||
for purpose in AgentPurpose::choices() {
|
||||
message.push_str(&format!(
|
||||
"\n- {} `{}` ({})",
|
||||
purpose.emoji(),
|
||||
purpose.as_str(),
|
||||
strings::agent::purpose_howto(purpose),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
bot.messaging()
|
||||
.send_text_markdown_no_fail(
|
||||
message_context.room(),
|
||||
message,
|
||||
MessageResponseType::Reply(message_context.thread_info().root_event_id.clone()),
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
54
src/controller/agent/mod.rs
Normal file
54
src/controller/agent/mod.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use crate::{entity::MessageContext, Bot};
|
||||
|
||||
pub mod create;
|
||||
pub mod delete;
|
||||
pub mod details;
|
||||
pub mod determination;
|
||||
pub mod help;
|
||||
pub mod list;
|
||||
|
||||
pub use determination::{determine_controller, AgentControllerType};
|
||||
|
||||
pub async fn dispatch_controller(
|
||||
handler: &AgentControllerType,
|
||||
message_context: &MessageContext,
|
||||
bot: &Bot,
|
||||
) -> anyhow::Result<()> {
|
||||
match handler {
|
||||
AgentControllerType::CreateRoomLocal { provider, agent_id } => {
|
||||
create::handle_room_local(
|
||||
bot,
|
||||
bot.room_config_manager(),
|
||||
message_context,
|
||||
provider,
|
||||
agent_id,
|
||||
)
|
||||
.await
|
||||
}
|
||||
AgentControllerType::CreateGlobal { provider, agent_id } => {
|
||||
create::handle_global(
|
||||
bot,
|
||||
bot.global_config_manager(),
|
||||
message_context,
|
||||
provider,
|
||||
agent_id,
|
||||
)
|
||||
.await
|
||||
}
|
||||
AgentControllerType::List => list::handle(bot, message_context).await,
|
||||
AgentControllerType::Details(agent_identifier) => {
|
||||
details::handle(bot, message_context, agent_identifier).await
|
||||
}
|
||||
AgentControllerType::Delete(agent_identifier) => {
|
||||
delete::handle(
|
||||
bot,
|
||||
bot.room_config_manager(),
|
||||
bot.global_config_manager(),
|
||||
message_context,
|
||||
agent_identifier,
|
||||
)
|
||||
.await
|
||||
}
|
||||
AgentControllerType::Help => help::handle(bot, message_context).await,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user