Compare commits

...

18 Commits

Author SHA1 Message Date
Slavi Pantaleev
cb29419d75 Prepare 1.16.0 release
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 12:25:52 +02:00
Slavi Pantaleev
51ca8c9948 Update dependencies 2026-03-20 12:23:51 +02:00
Slavi Pantaleev
12b938d2d1 Skip files with application/octet-stream MIME type
Files with unrecognized MIME types (application/octet-stream) are
not supported by any LLM provider and would cause errors that
permanently break the conversation thread. Instead, represent them
as a text message describing the attachment so the LLM is still
aware a file was sent without the thread becoming unusable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 12:22:38 +02:00
Slavi Pantaleev
f3d1b32ad7 Use mime_guess for file extension MIME type detection
Replace the hand-maintained extension-to-MIME mapping with the
mime_guess crate, which was already in the dependency tree.
This covers hundreds of file extensions out of the box.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 12:22:38 +02:00
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
renovate[bot]
a25845e89e Update Rust crate quick_cache to v0.6.21 2026-03-19 19:22:00 +02:00
renovate[bot]
479f54b93d Update docker.io/ollama/ollama Docker tag to v0.18.2 2026-03-19 08:15:14 +02:00
renovate[bot]
90ab6807ac Update Rust crate quick_cache to v0.6.20 2026-03-18 09:15:53 +02:00
renovate[bot]
5022f79bf5 Update docker.io/ollama/ollama Docker tag to v0.18.1 2026-03-17 07:25:56 +02:00
renovate[bot]
2ba3b5a437 Update docker.io/ollama/ollama Docker tag to v0.18.0 2026-03-14 22:34:33 +02:00
renovate[bot]
2819011b9d Update Rust crate tracing-subscriber to v0.3.23 2026-03-13 20:44:43 +02:00
renovate[bot]
ede9065f77 Update Rust crate async-openai to v0.33.1 2026-03-13 11:54:12 +02:00
renovate[bot]
5e61f5b3a3 Update ghcr.io/element-hq/synapse Docker tag to v1.149.1 2026-03-11 20:05:48 +02:00
renovate[bot]
8633e82f62 Update Rust crate quick_cache to v0.6.19 2026-03-11 20:05:32 +02:00
Slavi Pantaleev
6aa3d70d57 Bump default OpenAI text-generation model (gpt-5.2 -> gpt-5.4) 2026-03-11 09:53:02 +02:00
renovate[bot]
75e29ee3fb Update Rust crate tempfile to 3.27.* 2026-03-11 08:50:06 +02:00
renovate[bot]
eab7978b9f Update ghcr.io/element-hq/element-web Docker tag to v1.12.12 2026-03-10 22:06:39 +02:00
renovate[bot]
765e7c17b2 Update ghcr.io/element-hq/synapse Docker tag to v1.149.0 2026-03-10 20:19:06 +02:00
24 changed files with 235 additions and 74 deletions

View File

@@ -1,3 +1,11 @@
# (2026-03-20) Version 1.16.0
- (**Feature**) Add support for file attachments (`m.file` Matrix messages) in conversations. Files like PDFs, text documents, spreadsheets, code files, etc. are now downloaded and forwarded to the LLM alongside the conversation context, similar to how images (`m.image`) are already handled. See the [💬 Text Generation](./docs/features.md#-text-generation) documentation for details and known limitations.
- (**Improvement**) Use the [mime_guess](https://crates.io/crates/mime_guess) crate for MIME type detection from file extensions, replacing a hand-maintained mapping. This covers hundreds of file extensions out of the box.
# (2026-03-07) Version 1.15.0
- (**Feature**) Add support for authentication via access tokens (for [Matrix Authentication Service](https://github.com/element-hq/matrix-authentication-service)/OIDC-enabled homeservers) as an alternative to password authentication. See [🔐 Authentication](./docs/configuration/authentication.md) for setup details. Thanks to [Taylor Southwick](https://github.com/twsouthwick) for the contribution in [#83](https://github.com/etkecc/baibot/pull/83)!

88
Cargo.lock generated
View File

@@ -178,9 +178,9 @@ dependencies = [
[[package]]
name = "async-openai"
version = "0.33.0"
version = "0.33.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3ef6d66e53b3ec8ed4bae8e6dcdda75c0f5b4f67faba78fac1b09434cb4f2fc"
checksum = "cc48c3deb4ad9a2ee8c8e364c79eb0f74e69e17ed7e883d55988b90ea44fe986"
dependencies = [
"async-openai-macros",
"backoff",
@@ -288,7 +288,7 @@ dependencies = [
[[package]]
name = "baibot"
version = "1.15.0"
version = "1.16.0"
dependencies = [
"anthropic",
"anyhow",
@@ -460,9 +460,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.56"
version = "1.2.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423"
dependencies = [
"find-msvc-tools",
"shlex",
@@ -1998,9 +1998,9 @@ dependencies = [
[[package]]
name = "itoa"
version = "1.0.17"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "js-sys"
@@ -2070,9 +2070,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "libc"
version = "0.2.182"
version = "0.2.183"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
[[package]]
name = "libsqlite3-sys"
@@ -2673,9 +2673,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.21.3"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "opaque-debug"
@@ -2685,9 +2685,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]]
name = "openssl"
version = "0.10.75"
version = "0.10.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328"
checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf"
dependencies = [
"bitflags 2.11.0",
"cfg-if",
@@ -2723,9 +2723,9 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
[[package]]
name = "openssl-sys"
version = "0.9.111"
version = "0.9.112"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321"
checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb"
dependencies = [
"cc",
"libc",
@@ -2983,9 +2983,9 @@ checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae"
[[package]]
name = "quick_cache"
version = "0.6.18"
version = "0.6.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ada44a88ef953a3294f6eb55d2007ba44646015e18613d2f213016379203ef3"
checksum = "5a70b1b8b47e31d0498ecbc3c5470bb931399a8bfed1fd79d1717a61ce7f96e3"
dependencies = [
"ahash",
"equivalent",
@@ -3015,9 +3015,9 @@ dependencies = [
[[package]]
name = "quinn-proto"
version = "0.11.13"
version = "0.11.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31"
checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
dependencies = [
"bytes",
"getrandom 0.3.4",
@@ -3701,9 +3701,9 @@ checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "schannel"
version = "0.1.28"
version = "0.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1"
checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
dependencies = [
"windows-sys 0.61.2",
]
@@ -4132,9 +4132,9 @@ dependencies = [
[[package]]
name = "tempfile"
version = "3.26.0"
version = "3.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom 0.4.2",
@@ -4261,9 +4261,9 @@ dependencies = [
[[package]]
name = "tinyvec"
version = "1.10.0"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa"
checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
dependencies = [
"tinyvec_macros",
]
@@ -4365,7 +4365,7 @@ dependencies = [
"serde_spanned",
"toml_datetime 0.7.5+spec-1.1.0",
"toml_parser",
"winnow",
"winnow 0.7.15",
]
[[package]]
@@ -4379,32 +4379,32 @@ dependencies = [
[[package]]
name = "toml_datetime"
version = "1.0.0+spec-1.1.0"
version = "1.0.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e"
checksum = "9b320e741db58cac564e26c607d3cc1fdc4a88fd36c879568c07856ed83ff3e9"
dependencies = [
"serde_core",
]
[[package]]
name = "toml_edit"
version = "0.25.4+spec-1.1.0"
version = "0.25.5+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2"
checksum = "8ca1a40644a28bce036923f6a431df0b34236949d111cc07cb6dca830c9ef2e1"
dependencies = [
"indexmap",
"toml_datetime 1.0.0+spec-1.1.0",
"toml_datetime 1.0.1+spec-1.1.0",
"toml_parser",
"winnow",
"winnow 1.0.0",
]
[[package]]
name = "toml_parser"
version = "1.0.9+spec-1.1.0"
version = "1.0.10+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4"
checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420"
dependencies = [
"winnow",
"winnow 1.0.0",
]
[[package]]
@@ -4502,9 +4502,9 @@ dependencies = [
[[package]]
name = "tracing-subscriber"
version = "0.3.22"
version = "0.3.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
dependencies = [
"matchers",
"nu-ansi-term",
@@ -5213,6 +5213,12 @@ name = "winnow"
version = "0.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
[[package]]
name = "winnow"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8"
dependencies = [
"memchr",
]
@@ -5364,18 +5370,18 @@ dependencies = [
[[package]]
name = "zerocopy"
version = "0.8.40"
version = "0.8.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5"
checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.40"
version = "0.8.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953"
checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89"
dependencies = [
"proc-macro2",
"quote",

View File

@@ -7,7 +7,7 @@ license = "AGPL-3.0-or-later"
readme = "README.md"
keywords = ["matrix", "chat", "bot", "AI", "LLM"]
include = ["/etc/assets/baibot-torso-768.png", "/src", "/README.md", "/CHANGELOG.md", "/LICENSE"]
version = "1.15.0"
version = "1.16.0"
edition = "2024"
[lib]
@@ -32,7 +32,7 @@ regex = "1.12.*"
serde = { version = "1.0.*", features = ["derive"], default-features = false }
serde_json = "1.0.*"
serde_yaml_ng = "0.10.*"
tempfile = "3.26.*"
tempfile = "3.27.*"
tiktoken-rs = { version = "0.9.*", default-features = false }
tokio = { version = "1.50.*", features = ["rt", "rt-multi-thread", "macros"] }
tracing = "0.1.*"

View File

@@ -8,7 +8,7 @@ You can also use **different models within the same room** (e.g. [💬 text-gene
The bot supports the following use-purposes:
- [💬 text-generation](../features.md#-text-generation): communicating with you via text (though certain models may "see" images as well)
- [💬 text-generation](../features.md#-text-generation): communicating with you via text (though certain models may also process images and files)
- [🦻 speech-to-text](../features.md#-speech-to-text): turning your voice messages into text
- [🗣️ text-to-speech](../features.md#️-text-to-speech): turning bot or users text messages into voice messages
- [🖌️ image-generation](../features.md#image-generation): generating images based on instructions

View File

@@ -8,7 +8,7 @@ You can also use **different models within the same room** (e.g. [💬 text-gene
The bot supports the following use-purposes:
- [💬 text-generation](#-text-generation): communicating with you via text (though certain models may "see" images as well)
- [💬 text-generation](#-text-generation): communicating with you via text (though certain models may also process images and files)
- [🦻 speech-to-text](#-speech-to-text): turning your voice messages into text
- [🗣️ text-to-speech](#%EF%B8%8F-text-to-speech): turning bot or users text messages into voice messages
- [🖌️ image-generation](#%EF%B8%8F-image-generation): generating images based on instructions
@@ -26,7 +26,7 @@ Text Generation is the bot's ability to **respond to users' messages with text**
![Screenshot of Text Generation - a user sends a message and the bot replies in a new conversation thread](./screenshots/text-generation.webp)
Some models also support vision, so you may be able to mix text and images in the same conversation.
Some models also support vision and document understanding, so you may be able to mix text, images, and files (PDFs, text documents, etc.) in the same conversation. Note that certain providers may not support all file types or may have issues with specific files (e.g. scanned/image-based PDFs). If a file is rejected by the provider, the conversation thread may become unusable — start a new thread to work around this.
In multi-user (group) rooms, to avoid disturbing the normal conversation between people, the bot is auto-configured to only respond to messages starting with the command prefix (`!bai`) or direct mentions via the [💬 Text Generation / 🗟 Prefix Requirement Type](./configuration/text-generation.md#-prefix-requirement-type) setting.

View File

@@ -1,7 +1,7 @@
base_url: https://api.openai.com/v1
api_key: YOUR_API_KEY_HERE
text_generation:
model_id: gpt-5.2
model_id: gpt-5.4
prompt: "You are a brief, but helpful bot called {{ baibot_name }} powered by the {{ baibot_model_id }} model. The date/time of this conversation's start is: {{ baibot_conversation_start_time_utc }}."
temperature: 1.0
# Reasoning models need to use `max_completion_tokens` instead of `max_response_tokens`.

View File

@@ -11,7 +11,7 @@ This is related to the [💬 Text Generation](./features.md#-text-generation) fe
If there's a text-generation handler agent configured, the bot **may** respond to messages sent in the room.
Some models also support vision, so you may be able to mix text and images in the same conversation.
Some models also support vision and document understanding, so you may be able to mix text, images, and files (PDFs, text documents, etc.) in the same conversation.
See screenshots of:

View File

@@ -91,7 +91,7 @@ agents:
# base_url: https://api.openai.com/v1
# api_key: ""
# text_generation:
# model_id: gpt-5.2
# model_id: gpt-5.4
# prompt: "You are a brief, but helpful bot called {{ baibot_name }} powered by the {{ baibot_model_id }} model. The date/time of this conversation's start is: {{ baibot_conversation_start_time_utc }}."
# temperature: 1.0
# # Reasoning models need to use `max_completion_tokens` instead of `max_response_tokens`.

View File

@@ -1,6 +1,6 @@
services:
element-web:
image: ghcr.io/element-hq/element-web:v1.12.11
image: ghcr.io/element-hq/element-web:v1.12.12
user: "${UID}:${GID}"
restart: unless-stopped
environment:

View File

@@ -1,6 +1,6 @@
services:
ollama:
image: docker.io/ollama/ollama:0.17.7
image: docker.io/ollama/ollama:0.18.2
restart: unless-stopped
ports:
- "${SERVICE_OLLAMA_BIND_PORT_HTTP}:11434"

View File

@@ -14,7 +14,7 @@ services:
- /etc/passwd:/etc/passwd:ro
synapse:
image: ghcr.io/element-hq/synapse:v1.148.0
image: ghcr.io/element-hq/synapse:v1.149.1
user: "${UID}:${GID}"
restart: unless-stopped
entrypoint: python

View File

@@ -28,6 +28,13 @@ pub(super) fn create_anthropic_message_request(llm_messages: Vec<LLMMessage>) ->
},
}]
}
LLMMessageContent::File(file_details) => {
tracing::warn!(
"The Anthropic provider's library does not support file/document content. This file message ({}) will be skipped.",
file_details.filename(),
);
continue;
}
};
let message = Message { role, content };

View File

@@ -84,7 +84,7 @@ impl Default for TextGenerationConfig {
}
fn default_text_model_id() -> String {
"gpt-5.2".to_owned()
"gpt-5.4".to_owned()
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]

View File

@@ -1,6 +1,6 @@
use async_openai::types::responses::{
EasyInputContent, EasyInputMessage, ImageDetail, InputContent, InputImageContent, InputItem,
InputParam, MessageType, Role,
EasyInputContent, EasyInputMessage, ImageDetail, InputContent, InputFileArgs,
InputImageContent, InputItem, InputParam, MessageType, Role,
};
use crate::conversation::llm::{
@@ -35,6 +35,21 @@ pub fn convert_llm_messages_to_openai_response_input(
file_id: None,
})])
}
LLMMessageContent::File(file_details) => {
let file_data = format!(
"data:{};base64,{}",
file_details.mime,
base64_encode(&file_details.data)
);
let file_content = InputFileArgs::default()
.file_data(file_data)
.filename(file_details.filename())
.build()
.expect("Failed to build InputFileContent");
EasyInputContent::ContentList(vec![InputContent::InputFile(file_content)])
}
};
items.push(InputItem::EasyMessage(EasyInputMessage {

View File

@@ -40,6 +40,12 @@ fn convert_llm_message_to_openai_message(llm_message: LLMMessage) -> Option<Mess
);
None
}
LLMMessageContent::File(_file_details) => {
tracing::warn!(
"The OpenAI-compat provider's library does not support file content. This file message will be skipped."
);
None
}
}
}

View File

@@ -41,6 +41,8 @@ pub enum ChatCompletionControllerType {
Image,
File,
ThreadMention,
ReplyMention,
}
@@ -419,7 +421,8 @@ async fn handle_stage_text_generation(
| ChatCompletionControllerType::TextMention
| ChatCompletionControllerType::TextDirect
| ChatCompletionControllerType::Audio
| ChatCompletionControllerType::Image => {
| ChatCompletionControllerType::Image
| ChatCompletionControllerType::File => {
Some(message_context.combined_admin_and_user_regexes())
}

View File

@@ -58,6 +58,18 @@ pub fn determine_controller(
)
}
}
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)
}

View File

@@ -1,5 +1,7 @@
use chrono::{DateTime, Utc};
use mxlink::matrix_sdk::ruma::events::room::message::ImageMessageEventContent;
use mxlink::matrix_sdk::ruma::events::room::message::{
FileMessageEventContent, ImageMessageEventContent,
};
use mxlink::mime::Mime;
use crate::agent::provider::ImageSource;
@@ -48,10 +50,35 @@ impl From<ImageDetails> for ImageSource {
}
}
#[derive(Debug, Clone)]
pub struct FileDetails {
pub event_content: FileMessageEventContent,
pub mime: Mime,
pub data: Vec<u8>,
}
impl FileDetails {
pub fn new(event_content: FileMessageEventContent, mime: Mime, data: Vec<u8>) -> Self {
Self {
event_content,
mime,
data,
}
}
pub fn filename(&self) -> String {
self.event_content
.filename
.clone()
.unwrap_or(self.event_content.body.clone())
}
}
#[derive(Debug, Clone)]
pub enum MessageContent {
Text(String),
Image(ImageDetails),
File(FileDetails),
}
impl PartialEq for MessageContent {
@@ -62,6 +89,7 @@ impl PartialEq for MessageContent {
// We can probably do better than this by inspecting `.event_conten1t.source`, but for now this is good enough.
a.filename() == b.filename()
}
(MessageContent::File(a), MessageContent::File(b)) => a.filename() == b.filename(),
_ => false,
}
}

View File

@@ -74,6 +74,7 @@ fn calculate_token_size_for_message(bpe: &CoreBPE, model: &str, message: &Messag
let text_length = match &message.content {
MessageContent::Text(text) => bpe.encode_with_special_tokens(text).len() as i32,
MessageContent::Image(..) => 0,
MessageContent::File(..) => 0,
};
(text_length + role_length + tokens_per_message + tokens_per_name) as u32

View File

@@ -1,6 +1,6 @@
use mxlink::matrix_sdk::ruma::OwnedUserId;
use super::entity::{Author, ImageDetails, Message, MessageContent};
use super::entity::{Author, FileDetails, ImageDetails, Message, MessageContent};
use crate::conversation::matrix::{MatrixMessage, MatrixMessageContent};
use crate::utils::text_to_speech as text_to_speech_utils;
@@ -32,6 +32,15 @@ fn convert_bot_message(matrix_message: &MatrixMessage) -> Option<Message> {
)),
timestamp: matrix_message.timestamp.to_owned(),
}),
MatrixMessageContent::File(file_content, mime_type, media_bytes) => Some(Message {
author: Author::Assistant,
content: MessageContent::File(FileDetails::new(
file_content.clone(),
mime_type.clone(),
media_bytes.clone(),
)),
timestamp: matrix_message.timestamp.to_owned(),
}),
}
}
@@ -90,5 +99,14 @@ fn convert_user_message(matrix_message: &MatrixMessage) -> Option<Message> {
)),
timestamp: matrix_message.timestamp.to_owned(),
}),
MatrixMessageContent::File(file_content, mime_type, media_bytes) => Some(Message {
author: Author::User,
content: MessageContent::File(FileDetails::new(
file_content.clone(),
mime_type.clone(),
media_bytes.clone(),
)),
timestamp: matrix_message.timestamp.to_owned(),
}),
}
}

View File

@@ -2,7 +2,9 @@ use chrono::{DateTime, Utc};
use regex::Regex;
use mxlink::matrix_sdk::ruma::OwnedUserId;
use mxlink::matrix_sdk::ruma::events::room::message::ImageMessageEventContent;
use mxlink::matrix_sdk::ruma::events::room::message::{
FileMessageEventContent, ImageMessageEventContent,
};
use mxlink::mime::Mime;
#[derive(Clone)]
@@ -18,6 +20,7 @@ pub enum MatrixMessageContent {
Text(String),
Notice(String),
Image(ImageMessageEventContent, Mime, Vec<u8>),
File(FileMessageEventContent, Mime, Vec<u8>),
}
#[derive(Clone)]

View File

@@ -234,6 +234,7 @@ pub async fn convert_matrix_native_event_to_matrix_message(
MessageType::Text(text_content) => (text_content.body.clone(), false),
MessageType::Notice(notice_content) => (notice_content.body.clone(), true),
MessageType::Image(image_content) => (image_content.body.clone(), false),
MessageType::File(file_content) => (file_content.body.clone(), false),
_ => return Ok(None),
};
@@ -291,6 +292,67 @@ pub async fn convert_matrix_native_event_to_matrix_message(
}));
}
if let MessageType::File(file_content) = &room_message.msgtype {
let media_request = mxlink::matrix_sdk::media::MediaRequestParameters {
source: file_content.source.to_owned(),
format: mxlink::matrix_sdk::media::MediaFormat::File,
};
let file_name = file_content
.filename
.clone()
.unwrap_or(file_content.body.clone());
let mime_type = file_content
.info
.as_ref()
.and_then(|info| info.mimetype.clone())
.and_then(|mimetype| mimetype.parse::<mxlink::mime::Mime>().ok())
.unwrap_or_else(|| get_mime_type_from_file_name(&file_name));
tracing::debug!("Determined mime type {} for file {}", mime_type, file_name);
if mime_type == mxlink::mime::APPLICATION_OCTET_STREAM {
tracing::debug!(
"Skipping file {} with unsupported MIME type {}. It will be represented as a text message.",
file_name,
mime_type,
);
return Ok(Some(MatrixMessage {
sender_id: matrix_native_event.sender().to_owned(),
content: MatrixMessageContent::Text(format!(
"[A file ({}) was attached but skipped because its content type ({}) is not supported. Let the user know.]",
file_name, mime_type,
)),
mentioned_users,
timestamp,
}));
}
let span = tracing::debug_span!("get_media_content", file_name = %file_name, mime_type = %mime_type);
let media_bytes = matrix_link
.client()
.media()
.get_media_content(&media_request, true)
.instrument(span)
.await?;
tracing::debug!(
"Downloaded {} bytes for file {}",
media_bytes.len(),
file_name
);
return Ok(Some(MatrixMessage {
sender_id: matrix_native_event.sender().to_owned(),
content: MatrixMessageContent::File(file_content.clone(), mime_type, media_bytes),
mentioned_users,
timestamp,
}));
}
Ok(Some(MatrixMessage {
sender_id: matrix_native_event.sender().to_owned(),
content: if is_notice {

View File

@@ -1,5 +1,6 @@
use mxlink::matrix_sdk::ruma::events::room::message::{
AudioMessageEventContent, ImageMessageEventContent, MessageType, TextMessageEventContent,
AudioMessageEventContent, FileMessageEventContent, ImageMessageEventContent, MessageType,
TextMessageEventContent,
};
use mxlink::matrix_sdk::ruma::{OwnedEventId, OwnedUserId};
@@ -30,6 +31,7 @@ pub enum MessagePayload {
Text(TextMessageEventContent),
Audio(AudioMessageEventContent),
Image(ImageMessageEventContent),
File(FileMessageEventContent),
Reaction {
key: String,
@@ -57,6 +59,7 @@ impl TryInto<MessagePayload> for MessageType {
MessagePayload::Audio(audio_content)
}
MessageType::Image(image_content) => MessagePayload::Image(image_content),
MessageType::File(file_content) => MessagePayload::File(file_content),
other => {
return Err(format!("Unsupported message type: {:?}", other));
}

View File

@@ -17,18 +17,7 @@ pub fn get_file_extension(mime_type: &mime::Mime) -> String {
}
pub fn get_mime_type_from_file_name(file_name: &str) -> mime::Mime {
let extension = file_name.rsplit('.').next().unwrap_or("");
match extension.to_lowercase().as_str() {
"jpg" | "jpeg" => mime::IMAGE_JPEG,
"png" => mime::IMAGE_PNG,
"gif" => mime::IMAGE_GIF,
"webp" => "image/webp".parse().unwrap(),
"svg" => mime::IMAGE_SVG,
"tiff" | "tif" => "image/tiff".parse().unwrap(),
"bmp" => "image/bmp".parse().unwrap(),
"heic" | "heif" => "image/heic".parse().unwrap(),
"avif" => "image/avif".parse().unwrap(),
_ => mime::APPLICATION_OCTET_STREAM,
}
mime_guess::from_path(file_name)
.first()
.unwrap_or(mime::APPLICATION_OCTET_STREAM)
}