Initial commit

This commit is contained in:
Slavi Pantaleev
2024-09-12 13:44:06 +03:00
commit 946aa9d9e9
220 changed files with 26033 additions and 0 deletions

View 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;
}

View 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);
}
}
}
}

View 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(())
}

View 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(())
}

View 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)
}

View 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);
}
}

View 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(())
}

View 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(())
}

View 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,
}
}