Files
baibot-withmcp/src/controller/determination/mod.rs
Slavi Pantaleev 3d3bd3c9f9 Add support for file attachments (m.file) in conversations
Files sent as m.file Matrix messages are now downloaded, MIME-detected,
and forwarded to LLM providers alongside the conversation context,
similar to how m.image is already handled.

- OpenAI provider: sends files inline as base64 data URLs
- Anthropic provider: skips files with a warning (library limitation)
- OpenAI-compat provider: skips files with a warning (library limitation)

Controller routing respects the existing prefix requirement setting.
MIME detection expanded to cover PDF, text, code, and document formats.
Docs updated to reflect file support and known limitations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 12:22:38 +02:00

150 lines
5.8 KiB
Rust

#[cfg(test)]
mod tests;
use super::chat_completion::ChatCompletionControllerType;
use crate::{
entity::{
InteractionTrigger, MessageContext, MessagePayload,
roomconfig::TextGenerationPrefixRequirementType,
},
strings,
};
use super::ControllerType;
pub fn determine_controller(
command_prefix: &str,
first_thread_message: &InteractionTrigger,
message_context: &MessageContext,
) -> ControllerType {
match &first_thread_message.payload {
MessagePayload::SynthethicChatCompletionTriggerInThread => {
ControllerType::ChatCompletion(ChatCompletionControllerType::ThreadMention)
}
MessagePayload::SynthethicChatCompletionTriggerForReply => {
ControllerType::ChatCompletion(ChatCompletionControllerType::ReplyMention)
}
MessagePayload::Text(text_message_content) => {
let prefix_requirement_type = message_context
.room_config_context()
.text_generation_prefix_requirement_type();
determine_text_controller(
command_prefix,
&text_message_content.body,
prefix_requirement_type,
first_thread_message.is_mentioning_bot,
)
}
MessagePayload::Image(_image_message_content) => {
let prefix_requirement_type = message_context
.room_config_context()
.text_generation_prefix_requirement_type();
match prefix_requirement_type {
TextGenerationPrefixRequirementType::CommandPrefix => ControllerType::Ignore,
TextGenerationPrefixRequirementType::No => {
ControllerType::ChatCompletion(ChatCompletionControllerType::Image)
}
}
}
MessagePayload::Encrypted(thread_info) => {
if thread_info.is_thread_root_only() {
ControllerType::Error(strings::error::message_is_encrypted().to_owned())
} else {
ControllerType::ErrorInThread(
strings::error::first_message_in_thread_is_encrypted().to_owned(),
thread_info.clone(),
)
}
}
MessagePayload::File(_file_message_content) => {
let prefix_requirement_type = message_context
.room_config_context()
.text_generation_prefix_requirement_type();
match prefix_requirement_type {
TextGenerationPrefixRequirementType::CommandPrefix => ControllerType::Ignore,
TextGenerationPrefixRequirementType::No => {
ControllerType::ChatCompletion(ChatCompletionControllerType::File)
}
}
}
MessagePayload::Audio(_) => {
ControllerType::ChatCompletion(ChatCompletionControllerType::Audio)
}
MessagePayload::Reaction { .. } => {
panic!("Handling reaction as first message in thread does not make sense")
}
}
}
fn determine_text_controller(
command_prefix: &str,
text: &str,
room_text_generation_prefix_requirement_type: TextGenerationPrefixRequirementType,
is_mentioning_bot: bool,
) -> ControllerType {
let text = text.trim();
if text.starts_with(&format!("{command_prefix} help")) || text == command_prefix {
return ControllerType::Help;
}
if let Some(remaining) = text.strip_prefix(&format!("{command_prefix} access")) {
return super::access::determine_controller(remaining.trim());
}
if let Some(remaining) = text.strip_prefix(&format!("{command_prefix} provider")) {
return super::provider::determine_controller(remaining.trim());
}
if let Some(remaining) = text.strip_prefix(&format!("{command_prefix} agent")) {
return super::agent::determine_controller(command_prefix, remaining.trim());
}
if let Some(remaining) = text.strip_prefix(&format!("{command_prefix} config")) {
return super::cfg::determine_controller(remaining.trim());
}
if let Some(prompt) = text.strip_prefix(&format!("{command_prefix} image")) {
return super::image::determine_controller(prompt.trim());
}
if let Some(prompt) = text.strip_prefix(&format!("{command_prefix} sticker")) {
return ControllerType::StickerGeneration(prompt.trim().to_owned());
}
if let Some(remaining) = text.strip_prefix(&format!("{command_prefix} usage")) {
return super::usage::determine_controller(remaining.trim());
}
// Regular text message that does not match any command.
// If it mentions the bot, it's a chat completion.
// Otherwise, it depends on the prefix requirement for text generation - it may be routed for chat completion or ignored.
if is_mentioning_bot {
return ControllerType::ChatCompletion(ChatCompletionControllerType::TextMention);
}
// Regardless of what the prefix requirement is, if we encounter a command prefix, we'll consider it a chat completion via command prefix invokation.
// This is to correctly indicate to the chat completion controller that a command prefix was used,
// so that it can be stripped from the beginning of the message.
if text.starts_with(command_prefix) {
return ControllerType::ChatCompletion(ChatCompletionControllerType::TextCommand);
}
// We're dealing with a regular message that does not start with a command prefix.
match room_text_generation_prefix_requirement_type {
TextGenerationPrefixRequirementType::CommandPrefix => {
// A prefix is required, but we've already checked (above) that the message does not start with a command prefix.
// It's to be ignored.
ControllerType::Ignore
}
TextGenerationPrefixRequirementType::No => {
ControllerType::ChatCompletion(ChatCompletionControllerType::TextDirect)
}
}
}