Add sender context mode for text generation (#104)
Add a per-room/global `sender_context_mode` setting that optionally
prefixes conversation messages with sender metadata before sending them
to the model provider.
This helps models distinguish between participants in multi-user rooms.
Three modes are supported:
- `disabled` (default, no change)
- `matrix_user_id` (prefixes with `[sender=@user:server]`)
- `matrix_user_id_and_timestamp` (adds `send_at`; Example: `[sender=@user:server sent_at=<ISO 8601>]`)
Sender context is applied to user and assistant text messages only,
skipping system prompts and non-text content.
Mixed-sender merged turns (something we intentionally do for Anthropic)
have their `sender_id` cleared to avoid misattribution.
This commit is contained in:
@@ -57,6 +57,25 @@ This feature relies on [tokenization](https://en.wikipedia.org/wiki/Large_langua
|
|||||||
This setting is **disabled by default**, but can be enabled via `!bai config room text-generation set-context-management-enabled true` (this can also be set globally, see [🛠️ Room Settings](./README.md#room-settings)).
|
This setting is **disabled by default**, but can be enabled via `!bai config room text-generation set-context-management-enabled true` (this can also be set globally, see [🛠️ Room Settings](./README.md#room-settings)).
|
||||||
|
|
||||||
|
|
||||||
|
### 👤 Sender Context Mode
|
||||||
|
|
||||||
|
In multi-user rooms, it may be useful for the model to know which participant sent each message in the conversation context.
|
||||||
|
|
||||||
|
To support this, the bot has a `text-generation sender-context-mode` setting, which can be set to:
|
||||||
|
|
||||||
|
- (default) `disabled`: do not attach sender metadata to messages before sending them to the model
|
||||||
|
|
||||||
|
- `matrix_user_id`: prefix text messages with the sender's Matrix user ID, for example: `[sender=@alice:example.com] Hello bot`
|
||||||
|
|
||||||
|
- `matrix_user_id_and_timestamp`: prefix text messages with the sender's Matrix user ID and the message timestamp, for example: `[sender=@alice:example.com sent_at=2026-03-23T14:30:00Z] Hello bot`
|
||||||
|
|
||||||
|
This sender metadata is attached to conversation messages before they are sent to the model provider. It applies to user and assistant text messages, but not to system prompts or non-text content.
|
||||||
|
|
||||||
|
⚠️ Enabling this sends Matrix user IDs, and optionally timestamps, to the model provider.
|
||||||
|
|
||||||
|
Example: `!bai config room text-generation set-sender-context-mode matrix_user_id` (this can also be set globally, see [🛠️ Room Settings](./README.md#room-settings))
|
||||||
|
|
||||||
|
|
||||||
### ⌨️ Prompt Override
|
### ⌨️ Prompt Override
|
||||||
|
|
||||||
You can override the [system prompt](https://huggingface.co/docs/transformers/en/tasks/prompting) configured at the [🤖 agent](../agents.md) level.
|
You can override the [system prompt](https://huggingface.co/docs/transformers/en/tasks/prompting) configured at the [🤖 agent](../agents.md) level.
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ In multi-user (group) rooms, to avoid disturbing the normal conversation between
|
|||||||
|
|
||||||
Normally, the bot only responds to allowed [👥 Users](./access.md#-users). In certain cases, it's useful for an allowed user to provoke the bot to respond even in foreign threads or reply chains. You can learn more about this feature in the [On-demand involvement](./features.md#on-demand-involvement) section below.
|
Normally, the bot only responds to allowed [👥 Users](./access.md#-users). In certain cases, it's useful for an allowed user to provoke the bot to respond even in foreign threads or reply chains. You can learn more about this feature in the [On-demand involvement](./features.md#on-demand-involvement) section below.
|
||||||
|
|
||||||
|
If needed, the bot can also attach sender metadata to conversation messages before sending them to the model, which can help the model distinguish between participants in multi-user rooms. See [🛠️ Configuration / 💬 Text Generation / 👤 Sender Context Mode](./configuration/text-generation.md#-sender-context-mode).
|
||||||
|
|
||||||
A few other features (like [🗣️ Text-to-Speech](#️-text-to-speech) and [🦻 Speech-to-Text](#-speech-to-text)) combine well with Text Generation, so you **don't necessarily need to communicate with the bot via text** (with [Seamless voice interaction](#seamless-voice-interaction), you can communicate only with voice).
|
A few other features (like [🗣️ Text-to-Speech](#️-text-to-speech) and [🦻 Speech-to-Text](#-speech-to-text)) combine well with Text Generation, so you **don't necessarily need to communicate with the bot via text** (with [Seamless voice interaction](#seamless-voice-interaction), you can communicate only with voice).
|
||||||
|
|
||||||
You may also wish to see:
|
You may also wish to see:
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ impl ControllerTrait for Controller {
|
|||||||
|
|
||||||
let messages = vec![LLMMessage {
|
let messages = vec![LLMMessage {
|
||||||
author: LLMAuthor::User,
|
author: LLMAuthor::User,
|
||||||
|
sender_id: None,
|
||||||
content: LLMMessageContent::Text("Hello!".to_string()),
|
content: LLMMessageContent::Text("Hello!".to_string()),
|
||||||
timestamp: chrono::Utc::now(),
|
timestamp: chrono::Utc::now(),
|
||||||
}];
|
}];
|
||||||
@@ -108,6 +109,7 @@ impl ControllerTrait for Controller {
|
|||||||
} else {
|
} else {
|
||||||
Some(LLMMessage {
|
Some(LLMMessage {
|
||||||
author: LLMAuthor::Prompt,
|
author: LLMAuthor::Prompt,
|
||||||
|
sender_id: None,
|
||||||
content: LLMMessageContent::Text(prompt_text),
|
content: LLMMessageContent::Text(prompt_text),
|
||||||
timestamp: chrono::Utc::now(),
|
timestamp: chrono::Utc::now(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ impl ControllerTrait for Controller {
|
|||||||
|
|
||||||
let messages = vec![LLMMessage {
|
let messages = vec![LLMMessage {
|
||||||
author: LLMAuthor::User,
|
author: LLMAuthor::User,
|
||||||
|
sender_id: None,
|
||||||
content: LLMMessageContent::Text("Hello!".to_string()),
|
content: LLMMessageContent::Text("Hello!".to_string()),
|
||||||
timestamp: chrono::Utc::now(),
|
timestamp: chrono::Utc::now(),
|
||||||
}];
|
}];
|
||||||
@@ -104,6 +105,7 @@ impl ControllerTrait for Controller {
|
|||||||
} else {
|
} else {
|
||||||
Some(LLMMessage {
|
Some(LLMMessage {
|
||||||
author: LLMAuthor::Prompt,
|
author: LLMAuthor::Prompt,
|
||||||
|
sender_id: None,
|
||||||
content: LLMMessageContent::Text(prompt_text),
|
content: LLMMessageContent::Text(prompt_text),
|
||||||
timestamp: chrono::Utc::now(),
|
timestamp: chrono::Utc::now(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ impl ControllerTrait for Controller {
|
|||||||
|
|
||||||
let messages = vec![LLMMessage {
|
let messages = vec![LLMMessage {
|
||||||
author: LLMAuthor::User,
|
author: LLMAuthor::User,
|
||||||
|
sender_id: None,
|
||||||
content: LLMMessageContent::Text("Hello!".to_string()),
|
content: LLMMessageContent::Text("Hello!".to_string()),
|
||||||
timestamp: chrono::Utc::now(),
|
timestamp: chrono::Utc::now(),
|
||||||
}];
|
}];
|
||||||
@@ -101,6 +102,7 @@ impl ControllerTrait for Controller {
|
|||||||
} else {
|
} else {
|
||||||
Some(LLMMessage {
|
Some(LLMMessage {
|
||||||
author: LLMAuthor::Prompt,
|
author: LLMAuthor::Prompt,
|
||||||
|
sender_id: None,
|
||||||
content: LLMMessageContent::Text(prompt_text),
|
content: LLMMessageContent::Text(prompt_text),
|
||||||
timestamp: chrono::Utc::now(),
|
timestamp: chrono::Utc::now(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ use crate::{
|
|||||||
entity::roomconfig::{
|
entity::roomconfig::{
|
||||||
SpeechToTextFlowType, SpeechToTextMessageTypeForNonThreadedOnlyTranscribedMessages,
|
SpeechToTextFlowType, SpeechToTextMessageTypeForNonThreadedOnlyTranscribedMessages,
|
||||||
TextGenerationAutoUsage, TextGenerationPrefixRequirementType,
|
TextGenerationAutoUsage, TextGenerationPrefixRequirementType,
|
||||||
TextToSpeechBotMessagesFlowType, TextToSpeechUserMessagesFlowType,
|
TextGenerationSenderContextMode, TextToSpeechBotMessagesFlowType,
|
||||||
|
TextToSpeechUserMessagesFlowType,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -48,6 +49,9 @@ pub enum ConfigTextGenerationSettingRelatedControllerType {
|
|||||||
|
|
||||||
GetTemperatureOverride,
|
GetTemperatureOverride,
|
||||||
SetTemperatureOverride(Option<f32>),
|
SetTemperatureOverride(Option<f32>),
|
||||||
|
|
||||||
|
GetSenderContextMode,
|
||||||
|
SetSenderContextMode(Option<TextGenerationSenderContextMode>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
|
|||||||
@@ -163,6 +163,26 @@ fn determine_controller() {
|
|||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
},
|
},
|
||||||
|
TestCase {
|
||||||
|
name: "per-room text-generation/sender-context-mode getter",
|
||||||
|
input: "room text-generation sender-context-mode",
|
||||||
|
expected: super::ControllerType::Config(controller_type::ConfigControllerType::SettingsRelated(
|
||||||
|
controller_type::SettingsStorageSource::Room,
|
||||||
|
controller_type::ConfigSettingRelatedControllerType::TextGeneration(
|
||||||
|
controller_type::ConfigTextGenerationSettingRelatedControllerType::GetSenderContextMode,
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
name: "global text-generation/sender-context-mode getter",
|
||||||
|
input: "global text-generation sender-context-mode",
|
||||||
|
expected: super::ControllerType::Config(controller_type::ConfigControllerType::SettingsRelated(
|
||||||
|
controller_type::SettingsStorageSource::Global,
|
||||||
|
controller_type::ConfigSettingRelatedControllerType::TextGeneration(
|
||||||
|
controller_type::ConfigTextGenerationSettingRelatedControllerType::GetSenderContextMode,
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
},
|
||||||
TestCase {
|
TestCase {
|
||||||
name: "per-room text-to-speech/speed-override getter",
|
name: "per-room text-to-speech/speed-override getter",
|
||||||
input: "room text-to-speech speed-override",
|
input: "room text-to-speech speed-override",
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ mod tests;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
controller::ControllerType,
|
controller::ControllerType,
|
||||||
entity::roomconfig::{TextGenerationAutoUsage, TextGenerationPrefixRequirementType},
|
entity::roomconfig::{
|
||||||
|
TextGenerationAutoUsage, TextGenerationPrefixRequirementType,
|
||||||
|
TextGenerationSenderContextMode,
|
||||||
|
},
|
||||||
strings,
|
strings,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -197,5 +200,43 @@ pub(super) fn determine(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(remaining_text) = text.strip_prefix("sender-context-mode") {
|
||||||
|
let remaining_text = remaining_text.trim();
|
||||||
|
|
||||||
|
if !remaining_text.is_empty() {
|
||||||
|
return Err(ControllerType::Error(
|
||||||
|
strings::cfg::configuration_getter_used_with_extra_text(
|
||||||
|
"sender-context-mode",
|
||||||
|
remaining_text,
|
||||||
|
)
|
||||||
|
.to_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(ConfigTextGenerationSettingRelatedControllerType::GetSenderContextMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(value_string) = text.strip_prefix("set-sender-context-mode") {
|
||||||
|
let value_string = value_string.trim().to_owned();
|
||||||
|
let value_choice = if value_string.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let value_choice =
|
||||||
|
TextGenerationSenderContextMode::from_str(&value_string.to_lowercase());
|
||||||
|
|
||||||
|
if value_choice.is_none() {
|
||||||
|
return Err(ControllerType::Error(
|
||||||
|
strings::cfg::configuration_value_unrecognized(&value_string).to_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
value_choice
|
||||||
|
};
|
||||||
|
|
||||||
|
return Ok(
|
||||||
|
ConfigTextGenerationSettingRelatedControllerType::SetSenderContextMode(value_choice),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Err(ControllerType::Unknown)
|
Err(ControllerType::Unknown)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,6 +90,74 @@ fn determine_controller_context_management() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn determine_controller_sender_context() {
|
||||||
|
use super::ConfigTextGenerationSettingRelatedControllerType;
|
||||||
|
use super::ControllerType;
|
||||||
|
use crate::entity::roomconfig::TextGenerationSenderContextMode;
|
||||||
|
|
||||||
|
struct TestCase {
|
||||||
|
name: &'static str,
|
||||||
|
input: &'static str,
|
||||||
|
expected: Result<ConfigTextGenerationSettingRelatedControllerType, ControllerType>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let test_cases = vec![
|
||||||
|
TestCase {
|
||||||
|
name: "sender-context-mode getter ok",
|
||||||
|
input: "sender-context-mode",
|
||||||
|
expected: Ok(ConfigTextGenerationSettingRelatedControllerType::GetSenderContextMode),
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
name: "sender-context-mode getter extra args",
|
||||||
|
input: "sender-context-mode some values here",
|
||||||
|
expected: Err(ControllerType::Error(
|
||||||
|
crate::strings::cfg::configuration_getter_used_with_extra_text(
|
||||||
|
"sender-context-mode",
|
||||||
|
"some values here",
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
name: "sender-context-mode setter matrix_user_id",
|
||||||
|
input: "set-sender-context-mode matrix_user_id",
|
||||||
|
expected: Ok(
|
||||||
|
ConfigTextGenerationSettingRelatedControllerType::SetSenderContextMode(Some(
|
||||||
|
TextGenerationSenderContextMode::MatrixUserId,
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
name: "sender-context-mode setter uppercase",
|
||||||
|
input: "set-sender-context-mode MATRIX_USER_ID_AND_TIMESTAMP",
|
||||||
|
expected: Ok(
|
||||||
|
ConfigTextGenerationSettingRelatedControllerType::SetSenderContextMode(Some(
|
||||||
|
TextGenerationSenderContextMode::MatrixUserIdAndTimestamp,
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
name: "sender-context-mode setter invalid",
|
||||||
|
input: "set-sender-context-mode non-Enum-Value",
|
||||||
|
expected: Err(ControllerType::Error(
|
||||||
|
crate::strings::cfg::configuration_value_unrecognized("non-Enum-Value"),
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
name: "sender-context-mode unsetter",
|
||||||
|
input: "set-sender-context-mode",
|
||||||
|
expected: Ok(
|
||||||
|
ConfigTextGenerationSettingRelatedControllerType::SetSenderContextMode(None),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for test_case in test_cases {
|
||||||
|
let result = super::determine(test_case.input);
|
||||||
|
assert_eq!(result, test_case.expected, "Test case: {}", test_case.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn determine_controller_prefix_requirement_type() {
|
fn determine_controller_prefix_requirement_type() {
|
||||||
use super::ConfigTextGenerationSettingRelatedControllerType;
|
use super::ConfigTextGenerationSettingRelatedControllerType;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::entity::roomconfig::{
|
use crate::entity::roomconfig::{
|
||||||
RoomSettings, TextGenerationAutoUsage, TextGenerationPrefixRequirementType,
|
RoomSettings, TextGenerationAutoUsage, TextGenerationPrefixRequirementType,
|
||||||
|
TextGenerationSenderContextMode,
|
||||||
};
|
};
|
||||||
use crate::{Bot, entity::MessageContext};
|
use crate::{Bot, entity::MessageContext};
|
||||||
|
|
||||||
@@ -151,5 +152,38 @@ pub(super) async fn dispatch(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ConfigTextGenerationSettingRelatedControllerType::GetSenderContextMode => {
|
||||||
|
let value = &room_settings.text_generation.sender_context_mode;
|
||||||
|
setting_get::<TextGenerationSenderContextMode>(bot, message_context, value).await
|
||||||
|
}
|
||||||
|
ConfigTextGenerationSettingRelatedControllerType::SetSenderContextMode(value) => {
|
||||||
|
let value = value.to_owned();
|
||||||
|
|
||||||
|
let setter_callback = Box::new(move |room_settings: &mut RoomSettings| {
|
||||||
|
room_settings.text_generation.sender_context_mode = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
match config_type {
|
||||||
|
SettingsStorageSource::Room => {
|
||||||
|
room_setting_set::<TextGenerationSenderContextMode>(
|
||||||
|
bot,
|
||||||
|
message_context,
|
||||||
|
&value,
|
||||||
|
setter_callback,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
SettingsStorageSource::Global => {
|
||||||
|
global_setting_set::<TextGenerationSenderContextMode>(
|
||||||
|
bot,
|
||||||
|
message_context,
|
||||||
|
&value,
|
||||||
|
setter_callback,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ use crate::{
|
|||||||
roomconfig::{
|
roomconfig::{
|
||||||
SpeechToTextFlowType, SpeechToTextMessageTypeForNonThreadedOnlyTranscribedMessages,
|
SpeechToTextFlowType, SpeechToTextMessageTypeForNonThreadedOnlyTranscribedMessages,
|
||||||
TextGenerationAutoUsage, TextGenerationPrefixRequirementType,
|
TextGenerationAutoUsage, TextGenerationPrefixRequirementType,
|
||||||
TextToSpeechBotMessagesFlowType, TextToSpeechUserMessagesFlowType,
|
TextGenerationSenderContextMode, TextToSpeechBotMessagesFlowType,
|
||||||
|
TextToSpeechUserMessagesFlowType,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
strings,
|
strings,
|
||||||
@@ -233,6 +234,46 @@ fn build_section_text_generation(command_prefix: &str, bot_username: &str) -> St
|
|||||||
));
|
));
|
||||||
message.push_str("\n\n");
|
message.push_str("\n\n");
|
||||||
|
|
||||||
|
// Sender Context
|
||||||
|
|
||||||
|
message.push_str(&format!(
|
||||||
|
"#### {}",
|
||||||
|
strings::help::cfg::text_generation_sender_context_heading()
|
||||||
|
));
|
||||||
|
message.push_str("\n\n");
|
||||||
|
message.push_str(&strings::help::cfg::text_generation_sender_context_intro());
|
||||||
|
message.push('\n');
|
||||||
|
message.push_str(
|
||||||
|
&strings::help::cfg::the_following_configuration_values_are_recognized(
|
||||||
|
TextGenerationSenderContextMode::choices(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
message.push_str("\n\n");
|
||||||
|
message.push_str(&format!(
|
||||||
|
"- {}",
|
||||||
|
&strings::help::cfg::current_setting_show(
|
||||||
|
command_prefix,
|
||||||
|
"text-generation sender-context-mode"
|
||||||
|
)
|
||||||
|
));
|
||||||
|
message.push('\n');
|
||||||
|
message.push_str(&format!(
|
||||||
|
"- {}",
|
||||||
|
&strings::help::cfg::current_setting_set(
|
||||||
|
command_prefix,
|
||||||
|
"text-generation set-sender-context-mode VALUE"
|
||||||
|
)
|
||||||
|
));
|
||||||
|
message.push('\n');
|
||||||
|
message.push_str(&format!(
|
||||||
|
"- {}",
|
||||||
|
&strings::help::cfg::current_setting_unset(
|
||||||
|
command_prefix,
|
||||||
|
"text-generation set-sender-context-mode"
|
||||||
|
)
|
||||||
|
));
|
||||||
|
message.push_str("\n\n");
|
||||||
|
|
||||||
// Prompt override
|
// Prompt override
|
||||||
|
|
||||||
message.push_str(&format!(
|
message.push_str(&format!(
|
||||||
|
|||||||
@@ -359,6 +359,33 @@ async fn generate_text_generation_section(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Sender Context
|
||||||
|
|
||||||
|
let effective_sender_context = room_config_context.text_generation_sender_context_mode();
|
||||||
|
let room_config_sender_context = room_config_context
|
||||||
|
.room_config
|
||||||
|
.settings
|
||||||
|
.text_generation
|
||||||
|
.sender_context_mode;
|
||||||
|
let global_config_sender_context = room_config_context
|
||||||
|
.global_config
|
||||||
|
.fallback_room_settings
|
||||||
|
.text_generation
|
||||||
|
.sender_context_mode;
|
||||||
|
|
||||||
|
let sender_context_set_where = if room_config_sender_context.is_some() {
|
||||||
|
strings::cfg::status_badge_set_in_room_config()
|
||||||
|
} else if global_config_sender_context.is_some() {
|
||||||
|
strings::cfg::status_badge_set_in_global_config()
|
||||||
|
} else {
|
||||||
|
strings::cfg::status_badge_using_hardcoded_default()
|
||||||
|
};
|
||||||
|
|
||||||
|
message.push_str(&strings::cfg::status_text_generation_entry_sender_context(
|
||||||
|
effective_sender_context,
|
||||||
|
sender_context_set_where,
|
||||||
|
));
|
||||||
|
|
||||||
// Prompt override
|
// Prompt override
|
||||||
|
|
||||||
let text_agent_prompt = if let Some(text_generation_agent) = &text_generation_agent {
|
let text_agent_prompt = if let Some(text_generation_agent) = &text_generation_agent {
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ use crate::conversation::matrix::MatrixMessageProcessingParams;
|
|||||||
use crate::entity::MessagePayload;
|
use crate::entity::MessagePayload;
|
||||||
use crate::entity::roomconfig::{
|
use crate::entity::roomconfig::{
|
||||||
SpeechToTextFlowType, SpeechToTextMessageTypeForNonThreadedOnlyTranscribedMessages,
|
SpeechToTextFlowType, SpeechToTextMessageTypeForNonThreadedOnlyTranscribedMessages,
|
||||||
TextToSpeechBotMessagesFlowType, TextToSpeechUserMessagesFlowType,
|
TextGenerationSenderContextMode, TextToSpeechBotMessagesFlowType,
|
||||||
|
TextToSpeechUserMessagesFlowType,
|
||||||
};
|
};
|
||||||
use crate::strings;
|
use crate::strings;
|
||||||
use crate::utils::text_to_speech::create_transcribed_message_text;
|
use crate::utils::text_to_speech::create_transcribed_message_text;
|
||||||
@@ -23,6 +24,7 @@ use crate::{
|
|||||||
Bot,
|
Bot,
|
||||||
conversation::{
|
conversation::{
|
||||||
create_llm_conversation_for_matrix_reply_chain, create_llm_conversation_for_matrix_thread,
|
create_llm_conversation_for_matrix_reply_chain, create_llm_conversation_for_matrix_thread,
|
||||||
|
llm::{Author, Conversation, MessageContent},
|
||||||
matrix::create_list_of_bot_user_prefixes_to_strip,
|
matrix::create_list_of_bot_user_prefixes_to_strip,
|
||||||
},
|
},
|
||||||
entity::MessageContext,
|
entity::MessageContext,
|
||||||
@@ -486,6 +488,13 @@ async fn handle_stage_text_generation(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let conversation = inject_sender_context(
|
||||||
|
conversation,
|
||||||
|
message_context
|
||||||
|
.room_config_context()
|
||||||
|
.text_generation_sender_context_mode(),
|
||||||
|
);
|
||||||
|
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
agent_id = agent.identifier().as_string(),
|
agent_id = agent.identifier().as_string(),
|
||||||
provider = format!("{}", agent.definition().provider.clone()),
|
provider = format!("{}", agent.definition().provider.clone()),
|
||||||
@@ -761,3 +770,238 @@ async fn generate_and_send_tts_for_message(
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn inject_sender_context(
|
||||||
|
conversation: Conversation,
|
||||||
|
sender_context_mode: TextGenerationSenderContextMode,
|
||||||
|
) -> Conversation {
|
||||||
|
if sender_context_mode == TextGenerationSenderContextMode::Disabled {
|
||||||
|
return conversation;
|
||||||
|
}
|
||||||
|
|
||||||
|
let include_timestamp =
|
||||||
|
sender_context_mode == TextGenerationSenderContextMode::MatrixUserIdAndTimestamp;
|
||||||
|
|
||||||
|
let messages = conversation
|
||||||
|
.messages
|
||||||
|
.into_iter()
|
||||||
|
.map(|mut message| {
|
||||||
|
if message.author == Author::Prompt {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(sender_id) = &message.sender_id else {
|
||||||
|
return message;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let MessageContent::Text(ref mut text) = message.content {
|
||||||
|
*text = if include_timestamp {
|
||||||
|
let timestamp = message.timestamp.format("%Y-%m-%dT%H:%M:%SZ");
|
||||||
|
format!("[sender={} sent_at={}] {}", sender_id, timestamp, text)
|
||||||
|
} else {
|
||||||
|
format!("[sender={}] {}", sender_id, text)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
message
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Conversation { messages }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod sender_context_tests {
|
||||||
|
use super::inject_sender_context;
|
||||||
|
use crate::conversation::llm::{Author, Conversation, ImageDetails, Message, MessageContent};
|
||||||
|
use crate::entity::roomconfig::TextGenerationSenderContextMode;
|
||||||
|
use chrono::{TimeZone, Utc};
|
||||||
|
use mxlink::matrix_sdk::ruma::events::room::message::ImageMessageEventContent;
|
||||||
|
use mxlink::matrix_sdk::ruma::{OwnedMxcUri, OwnedUserId};
|
||||||
|
use mxlink::mime;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_inject_sender_context_prefixes_text_messages() {
|
||||||
|
let timestamp = Utc.with_ymd_and_hms(2026, 3, 23, 14, 30, 0).unwrap();
|
||||||
|
let user_id = OwnedUserId::try_from("@alice:example.com").unwrap();
|
||||||
|
|
||||||
|
let conversation = Conversation {
|
||||||
|
messages: vec![Message {
|
||||||
|
author: Author::User,
|
||||||
|
sender_id: Some(user_id),
|
||||||
|
timestamp,
|
||||||
|
content: MessageContent::Text("Hello bot".to_string()),
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = inject_sender_context(
|
||||||
|
conversation,
|
||||||
|
TextGenerationSenderContextMode::MatrixUserIdAndTimestamp,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(result.messages.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
result.messages[0].content,
|
||||||
|
MessageContent::Text(
|
||||||
|
"[sender=@alice:example.com sent_at=2026-03-23T14:30:00Z] Hello bot".to_string()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_inject_sender_context_can_prefix_without_timestamp() {
|
||||||
|
let timestamp = Utc.with_ymd_and_hms(2026, 3, 23, 14, 30, 0).unwrap();
|
||||||
|
let user_id = OwnedUserId::try_from("@alice:example.com").unwrap();
|
||||||
|
|
||||||
|
let conversation = Conversation {
|
||||||
|
messages: vec![Message {
|
||||||
|
author: Author::User,
|
||||||
|
sender_id: Some(user_id),
|
||||||
|
timestamp,
|
||||||
|
content: MessageContent::Text("Hello bot".to_string()),
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
let result =
|
||||||
|
inject_sender_context(conversation, TextGenerationSenderContextMode::MatrixUserId);
|
||||||
|
|
||||||
|
assert_eq!(result.messages.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
result.messages[0].content,
|
||||||
|
MessageContent::Text("[sender=@alice:example.com] Hello bot".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_inject_sender_context_prefixes_assistant_messages() {
|
||||||
|
let timestamp = Utc.with_ymd_and_hms(2026, 3, 23, 14, 30, 0).unwrap();
|
||||||
|
let user_id = OwnedUserId::try_from("@baibot:example.com").unwrap();
|
||||||
|
|
||||||
|
let conversation = Conversation {
|
||||||
|
messages: vec![Message {
|
||||||
|
author: Author::Assistant,
|
||||||
|
sender_id: Some(user_id),
|
||||||
|
timestamp,
|
||||||
|
content: MessageContent::Text("Hello human".to_string()),
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = inject_sender_context(
|
||||||
|
conversation,
|
||||||
|
TextGenerationSenderContextMode::MatrixUserIdAndTimestamp,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(result.messages.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
result.messages[0].content,
|
||||||
|
MessageContent::Text(
|
||||||
|
"[sender=@baibot:example.com sent_at=2026-03-23T14:30:00Z] Hello human".to_string()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_inject_sender_context_skips_prompt_messages() {
|
||||||
|
let timestamp = Utc.with_ymd_and_hms(2026, 3, 23, 14, 30, 0).unwrap();
|
||||||
|
|
||||||
|
let conversation = Conversation {
|
||||||
|
messages: vec![Message {
|
||||||
|
author: Author::Prompt,
|
||||||
|
sender_id: None,
|
||||||
|
timestamp,
|
||||||
|
content: MessageContent::Text("You are a bot".to_string()),
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
let result =
|
||||||
|
inject_sender_context(conversation, TextGenerationSenderContextMode::MatrixUserId);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result.messages[0].content,
|
||||||
|
MessageContent::Text("You are a bot".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_inject_sender_context_skips_messages_without_sender_id() {
|
||||||
|
let timestamp = Utc.with_ymd_and_hms(2026, 3, 23, 14, 30, 0).unwrap();
|
||||||
|
|
||||||
|
let conversation = Conversation {
|
||||||
|
messages: vec![Message {
|
||||||
|
author: Author::User,
|
||||||
|
sender_id: None,
|
||||||
|
timestamp,
|
||||||
|
content: MessageContent::Text("Transcribed text".to_string()),
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = inject_sender_context(
|
||||||
|
conversation,
|
||||||
|
TextGenerationSenderContextMode::MatrixUserIdAndTimestamp,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result.messages[0].content,
|
||||||
|
MessageContent::Text("Transcribed text".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_inject_sender_context_leaves_non_text_content_unchanged() {
|
||||||
|
let timestamp = Utc.with_ymd_and_hms(2026, 3, 23, 14, 30, 0).unwrap();
|
||||||
|
let user_id = OwnedUserId::try_from("@alice:example.com").unwrap();
|
||||||
|
let image_event_content = ImageMessageEventContent::plain(
|
||||||
|
"image.png".to_string(),
|
||||||
|
OwnedMxcUri::from("mxc://example.com/1234567890"),
|
||||||
|
);
|
||||||
|
|
||||||
|
let conversation = Conversation {
|
||||||
|
messages: vec![Message {
|
||||||
|
author: Author::User,
|
||||||
|
sender_id: Some(user_id),
|
||||||
|
timestamp,
|
||||||
|
content: MessageContent::Image(ImageDetails::new(
|
||||||
|
image_event_content.clone(),
|
||||||
|
mime::IMAGE_PNG,
|
||||||
|
vec![],
|
||||||
|
)),
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = inject_sender_context(
|
||||||
|
conversation,
|
||||||
|
TextGenerationSenderContextMode::MatrixUserIdAndTimestamp,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result.messages[0].content,
|
||||||
|
MessageContent::Image(ImageDetails::new(
|
||||||
|
image_event_content,
|
||||||
|
mime::IMAGE_PNG,
|
||||||
|
vec![]
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_inject_sender_context_none_leaves_text_unchanged() {
|
||||||
|
let timestamp = Utc.with_ymd_and_hms(2026, 3, 23, 14, 30, 0).unwrap();
|
||||||
|
let user_id = OwnedUserId::try_from("@alice:example.com").unwrap();
|
||||||
|
|
||||||
|
let conversation = Conversation {
|
||||||
|
messages: vec![Message {
|
||||||
|
author: Author::User,
|
||||||
|
sender_id: Some(user_id),
|
||||||
|
timestamp,
|
||||||
|
content: MessageContent::Text("Hello bot".to_string()),
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = inject_sender_context(conversation, TextGenerationSenderContextMode::Disabled);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result.messages[0].content,
|
||||||
|
MessageContent::Text("Hello bot".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ mod tests {
|
|||||||
original_prompt: "Generate a picture of a dog",
|
original_prompt: "Generate a picture of a dog",
|
||||||
messages: vec![Message {
|
messages: vec![Message {
|
||||||
author: Author::User,
|
author: Author::User,
|
||||||
|
sender_id: None,
|
||||||
content: MessageContent::Text("Must be blue".to_owned()),
|
content: MessageContent::Text("Must be blue".to_owned()),
|
||||||
timestamp,
|
timestamp,
|
||||||
}],
|
}],
|
||||||
@@ -75,16 +76,19 @@ mod tests {
|
|||||||
messages: vec![
|
messages: vec![
|
||||||
Message {
|
Message {
|
||||||
author: Author::User,
|
author: Author::User,
|
||||||
|
sender_id: None,
|
||||||
content: MessageContent::Text("Must be blue".to_owned()),
|
content: MessageContent::Text("Must be blue".to_owned()),
|
||||||
timestamp,
|
timestamp,
|
||||||
},
|
},
|
||||||
Message {
|
Message {
|
||||||
author: Author::Assistant,
|
author: Author::Assistant,
|
||||||
|
sender_id: None,
|
||||||
content: MessageContent::Text("Whatever".to_owned()),
|
content: MessageContent::Text("Whatever".to_owned()),
|
||||||
timestamp,
|
timestamp,
|
||||||
},
|
},
|
||||||
Message {
|
Message {
|
||||||
author: Author::User,
|
author: Author::User,
|
||||||
|
sender_id: None,
|
||||||
content: MessageContent::Text(
|
content: MessageContent::Text(
|
||||||
"Must be 3-legged.\nMust be flying.".to_owned(),
|
"Must be 3-legged.\nMust be flying.".to_owned(),
|
||||||
),
|
),
|
||||||
@@ -99,21 +103,25 @@ mod tests {
|
|||||||
messages: vec![
|
messages: vec![
|
||||||
Message {
|
Message {
|
||||||
author: Author::User,
|
author: Author::User,
|
||||||
|
sender_id: None,
|
||||||
content: MessageContent::Text("Must be blue".to_owned()),
|
content: MessageContent::Text("Must be blue".to_owned()),
|
||||||
timestamp,
|
timestamp,
|
||||||
},
|
},
|
||||||
Message {
|
Message {
|
||||||
author: Author::Assistant,
|
author: Author::Assistant,
|
||||||
|
sender_id: None,
|
||||||
content: MessageContent::Text("Whatever".to_owned()),
|
content: MessageContent::Text("Whatever".to_owned()),
|
||||||
timestamp,
|
timestamp,
|
||||||
},
|
},
|
||||||
Message {
|
Message {
|
||||||
author: Author::User,
|
author: Author::User,
|
||||||
|
sender_id: None,
|
||||||
content: MessageContent::Text("Again".to_owned()),
|
content: MessageContent::Text("Again".to_owned()),
|
||||||
timestamp,
|
timestamp,
|
||||||
},
|
},
|
||||||
Message {
|
Message {
|
||||||
author: Author::User,
|
author: Author::User,
|
||||||
|
sender_id: None,
|
||||||
content: MessageContent::Text("again".to_owned()),
|
content: MessageContent::Text("again".to_owned()),
|
||||||
timestamp,
|
timestamp,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
use mxlink::matrix_sdk::ruma::OwnedUserId;
|
||||||
use mxlink::matrix_sdk::ruma::events::room::message::{
|
use mxlink::matrix_sdk::ruma::events::room::message::{
|
||||||
FileMessageEventContent, ImageMessageEventContent,
|
FileMessageEventContent, ImageMessageEventContent,
|
||||||
};
|
};
|
||||||
@@ -16,6 +17,7 @@ pub enum Author {
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Message {
|
pub struct Message {
|
||||||
pub author: Author,
|
pub author: Author,
|
||||||
|
pub sender_id: Option<OwnedUserId>,
|
||||||
pub timestamp: DateTime<Utc>,
|
pub timestamp: DateTime<Utc>,
|
||||||
pub content: MessageContent,
|
pub content: MessageContent,
|
||||||
}
|
}
|
||||||
@@ -104,6 +106,11 @@ impl Conversation {
|
|||||||
///
|
///
|
||||||
/// Certain models (like Anthropic) cannot tolerate consecutive messages by the same author,
|
/// Certain models (like Anthropic) cannot tolerate consecutive messages by the same author,
|
||||||
/// so combining them helps avoid issues.
|
/// so combining them helps avoid issues.
|
||||||
|
///
|
||||||
|
/// When multiple text messages by the same author are merged, the resulting message keeps a
|
||||||
|
/// `sender_id` only if all merged messages came from the same sender. Mixed-sender merges are
|
||||||
|
/// possible for user turns in multi-user rooms, so `sender_id` is cleared in that case to
|
||||||
|
/// avoid incorrectly attributing the whole merged turn to the first sender.
|
||||||
/// See: https://github.com/etkecc/baibot/issues/13
|
/// See: https://github.com/etkecc/baibot/issues/13
|
||||||
pub fn combine_consecutive_messages(&self) -> Conversation {
|
pub fn combine_consecutive_messages(&self) -> Conversation {
|
||||||
// We'll likely get fewer messages, but let's reserve the maximum we expect.
|
// We'll likely get fewer messages, but let's reserve the maximum we expect.
|
||||||
@@ -134,6 +141,10 @@ impl Conversation {
|
|||||||
text.push('\n');
|
text.push('\n');
|
||||||
text.push_str(message_text_content);
|
text.push_str(message_text_content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if last_message.sender_id != message.sender_id {
|
||||||
|
last_message.sender_id = None;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Conversation {
|
Conversation {
|
||||||
@@ -150,7 +161,7 @@ impl Conversation {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use chrono::{TimeZone, Utc};
|
use chrono::{TimeZone, Utc};
|
||||||
use mxlink::matrix_sdk::ruma::OwnedMxcUri;
|
use mxlink::matrix_sdk::ruma::{OwnedMxcUri, OwnedUserId};
|
||||||
use mxlink::mime;
|
use mxlink::mime;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -173,21 +184,25 @@ mod tests {
|
|||||||
// User's turn
|
// User's turn
|
||||||
Message {
|
Message {
|
||||||
author: Author::User,
|
author: Author::User,
|
||||||
|
sender_id: None,
|
||||||
content: MessageContent::Text("Hello".to_string()),
|
content: MessageContent::Text("Hello".to_string()),
|
||||||
timestamp: timestamp_1,
|
timestamp: timestamp_1,
|
||||||
},
|
},
|
||||||
Message {
|
Message {
|
||||||
author: Author::User,
|
author: Author::User,
|
||||||
|
sender_id: None,
|
||||||
content: MessageContent::Text("How are you?".to_string()),
|
content: MessageContent::Text("How are you?".to_string()),
|
||||||
timestamp: timestamp_2,
|
timestamp: timestamp_2,
|
||||||
},
|
},
|
||||||
Message {
|
Message {
|
||||||
author: Author::User,
|
author: Author::User,
|
||||||
|
sender_id: None,
|
||||||
content: MessageContent::Text("I'm OK, btw.".to_string()),
|
content: MessageContent::Text("I'm OK, btw.".to_string()),
|
||||||
timestamp: timestamp_3,
|
timestamp: timestamp_3,
|
||||||
},
|
},
|
||||||
Message {
|
Message {
|
||||||
author: Author::User,
|
author: Author::User,
|
||||||
|
sender_id: None,
|
||||||
content: MessageContent::Image(ImageDetails::new(
|
content: MessageContent::Image(ImageDetails::new(
|
||||||
image_event_content.clone(),
|
image_event_content.clone(),
|
||||||
mime::IMAGE_PNG,
|
mime::IMAGE_PNG,
|
||||||
@@ -197,28 +212,33 @@ mod tests {
|
|||||||
},
|
},
|
||||||
Message {
|
Message {
|
||||||
author: Author::User,
|
author: Author::User,
|
||||||
|
sender_id: None,
|
||||||
content: MessageContent::Text("Above is an image.".to_string()),
|
content: MessageContent::Text("Above is an image.".to_string()),
|
||||||
timestamp: timestamp_4,
|
timestamp: timestamp_4,
|
||||||
},
|
},
|
||||||
Message {
|
Message {
|
||||||
author: Author::User,
|
author: Author::User,
|
||||||
|
sender_id: None,
|
||||||
content: MessageContent::Text("Would you take a look at it?".to_string()),
|
content: MessageContent::Text("Would you take a look at it?".to_string()),
|
||||||
timestamp: timestamp_4,
|
timestamp: timestamp_4,
|
||||||
},
|
},
|
||||||
// Assistant's turn
|
// Assistant's turn
|
||||||
Message {
|
Message {
|
||||||
author: Author::Assistant,
|
author: Author::Assistant,
|
||||||
|
sender_id: None,
|
||||||
content: MessageContent::Text("Hi there!".to_string()),
|
content: MessageContent::Text("Hi there!".to_string()),
|
||||||
timestamp: timestamp_2,
|
timestamp: timestamp_2,
|
||||||
},
|
},
|
||||||
Message {
|
Message {
|
||||||
author: Author::Assistant,
|
author: Author::Assistant,
|
||||||
|
sender_id: None,
|
||||||
content: MessageContent::Text("I'm doing well, thank you.".to_string()),
|
content: MessageContent::Text("I'm doing well, thank you.".to_string()),
|
||||||
timestamp: timestamp_3,
|
timestamp: timestamp_3,
|
||||||
},
|
},
|
||||||
// User's turn
|
// User's turn
|
||||||
Message {
|
Message {
|
||||||
author: Author::User,
|
author: Author::User,
|
||||||
|
sender_id: None,
|
||||||
content: MessageContent::Text("That's great!".to_string()),
|
content: MessageContent::Text("That's great!".to_string()),
|
||||||
timestamp: timestamp_3,
|
timestamp: timestamp_3,
|
||||||
},
|
},
|
||||||
@@ -267,4 +287,39 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(conversation.messages[4].timestamp, timestamp_3);
|
assert_eq!(conversation.messages[4].timestamp, timestamp_3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn combine_consecutive_messages_clears_sender_id_for_mixed_sender_turns() {
|
||||||
|
let timestamp_1 = Utc.with_ymd_and_hms(2024, 9, 20, 18, 34, 15).unwrap();
|
||||||
|
let timestamp_2 = Utc.with_ymd_and_hms(2024, 9, 20, 18, 34, 16).unwrap();
|
||||||
|
let sender_1 = OwnedUserId::try_from("@alice:example.com").unwrap();
|
||||||
|
let sender_2 = OwnedUserId::try_from("@bob:example.com").unwrap();
|
||||||
|
|
||||||
|
let conversation = Conversation {
|
||||||
|
messages: vec![
|
||||||
|
Message {
|
||||||
|
author: Author::User,
|
||||||
|
sender_id: Some(sender_1),
|
||||||
|
content: MessageContent::Text("Hello".to_string()),
|
||||||
|
timestamp: timestamp_1,
|
||||||
|
},
|
||||||
|
Message {
|
||||||
|
author: Author::User,
|
||||||
|
sender_id: Some(sender_2),
|
||||||
|
content: MessageContent::Text("Hi there".to_string()),
|
||||||
|
timestamp: timestamp_2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
let conversation = conversation.combine_consecutive_messages();
|
||||||
|
|
||||||
|
assert_eq!(conversation.messages.len(), 1);
|
||||||
|
assert_eq!(conversation.messages[0].sender_id, None);
|
||||||
|
assert_eq!(
|
||||||
|
conversation.messages[0].content,
|
||||||
|
MessageContent::Text("Hello\nHi there".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(conversation.messages[0].timestamp, timestamp_1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ fn test_messages_by_the_bot_are_identified_correctly() {
|
|||||||
let llm_message = convert_matrix_message_to_llm_message(&matrix_message, &bot_user_id).unwrap();
|
let llm_message = convert_matrix_message_to_llm_message(&matrix_message, &bot_user_id).unwrap();
|
||||||
|
|
||||||
assert_eq!(llm_message.author, Author::Assistant);
|
assert_eq!(llm_message.author, Author::Assistant);
|
||||||
|
assert_eq!(llm_message.sender_id, Some(bot_user_id.clone()));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
llm_message.content,
|
llm_message.content,
|
||||||
MessageContent::Text("Hello!".to_string())
|
MessageContent::Text("Hello!".to_string())
|
||||||
@@ -47,6 +48,7 @@ fn test_notice_messages_by_bot_with_speech_to_text_prefix_are_cleaned_up_and_con
|
|||||||
let llm_message = convert_matrix_message_to_llm_message(&matrix_message, &bot_user_id).unwrap();
|
let llm_message = convert_matrix_message_to_llm_message(&matrix_message, &bot_user_id).unwrap();
|
||||||
|
|
||||||
assert_eq!(llm_message.author, Author::User);
|
assert_eq!(llm_message.author, Author::User);
|
||||||
|
assert_eq!(llm_message.sender_id, None);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
llm_message.content,
|
llm_message.content,
|
||||||
MessageContent::Text(source_message_text.to_string())
|
MessageContent::Text(source_message_text.to_string())
|
||||||
@@ -75,6 +77,30 @@ fn test_notice_error_messages_by_bot_are_ignored() {
|
|||||||
assert!(llm_message.is_none());
|
assert!(llm_message.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_user_messages_preserve_sender_id() {
|
||||||
|
let bot_user_id =
|
||||||
|
OwnedUserId::try_from("@bot:example.com").expect("Failed to parse bot user ID");
|
||||||
|
|
||||||
|
let user_id = OwnedUserId::try_from("@alice:example.com").expect("Failed to parse user ID");
|
||||||
|
|
||||||
|
let matrix_message = super::super::matrix::MatrixMessage {
|
||||||
|
sender_id: user_id.clone(),
|
||||||
|
content: super::super::matrix::MatrixMessageContent::Text("Hello!".to_owned()),
|
||||||
|
mentioned_users: vec![],
|
||||||
|
timestamp: chrono::Utc::now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let llm_message = convert_matrix_message_to_llm_message(&matrix_message, &bot_user_id).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(llm_message.author, Author::User);
|
||||||
|
assert_eq!(llm_message.sender_id, Some(user_id));
|
||||||
|
assert_eq!(
|
||||||
|
llm_message.content,
|
||||||
|
MessageContent::Text("Hello!".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_other_notice_messages_by_the_bot_are_ignored() {
|
fn test_other_notice_messages_by_the_bot_are_ignored() {
|
||||||
// Also see `test_notice_error_messages_by_bot_are_ignored()`.
|
// Also see `test_notice_error_messages_by_bot_are_ignored()`.
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ pub mod test {
|
|||||||
|
|
||||||
let message = super::Message {
|
let message = super::Message {
|
||||||
author: super::Author::User,
|
author: super::Author::User,
|
||||||
|
sender_id: None,
|
||||||
content: super::MessageContent::Text("Hello there!".to_string()),
|
content: super::MessageContent::Text("Hello there!".to_string()),
|
||||||
timestamp: chrono::Utc::now(),
|
timestamp: chrono::Utc::now(),
|
||||||
};
|
};
|
||||||
@@ -108,6 +109,7 @@ pub mod test {
|
|||||||
|
|
||||||
let prompt = super::Message {
|
let prompt = super::Message {
|
||||||
author: super::Author::Prompt,
|
author: super::Author::Prompt,
|
||||||
|
sender_id: None,
|
||||||
content: super::MessageContent::Text("You are a bot!".to_string()),
|
content: super::MessageContent::Text("You are a bot!".to_string()),
|
||||||
timestamp: chrono::Utc::now(),
|
timestamp: chrono::Utc::now(),
|
||||||
};
|
};
|
||||||
@@ -122,6 +124,7 @@ pub mod test {
|
|||||||
|
|
||||||
let first = super::Message {
|
let first = super::Message {
|
||||||
author: super::Author::User,
|
author: super::Author::User,
|
||||||
|
sender_id: None,
|
||||||
content: super::MessageContent::Text("Hello there!".to_string()),
|
content: super::MessageContent::Text("Hello there!".to_string()),
|
||||||
timestamp: chrono::Utc::now(),
|
timestamp: chrono::Utc::now(),
|
||||||
};
|
};
|
||||||
@@ -136,6 +139,7 @@ pub mod test {
|
|||||||
|
|
||||||
let second = super::Message {
|
let second = super::Message {
|
||||||
author: super::Author::Assistant,
|
author: super::Author::Assistant,
|
||||||
|
sender_id: None,
|
||||||
content: super::MessageContent::Text("Hello!".to_string()),
|
content: super::MessageContent::Text("Hello!".to_string()),
|
||||||
timestamp: chrono::Utc::now(),
|
timestamp: chrono::Utc::now(),
|
||||||
};
|
};
|
||||||
@@ -150,6 +154,7 @@ pub mod test {
|
|||||||
|
|
||||||
let third = super::Message {
|
let third = super::Message {
|
||||||
author: super::Author::User,
|
author: super::Author::User,
|
||||||
|
sender_id: None,
|
||||||
content: super::MessageContent::Text(
|
content: super::MessageContent::Text(
|
||||||
"This is the 3rd message in this conversation. It shall be preserved.".to_owned(),
|
"This is the 3rd message in this conversation. It shall be preserved.".to_owned(),
|
||||||
),
|
),
|
||||||
@@ -166,6 +171,7 @@ pub mod test {
|
|||||||
|
|
||||||
let forth = super::Message {
|
let forth = super::Message {
|
||||||
author: super::Author::Assistant,
|
author: super::Author::Assistant,
|
||||||
|
sender_id: None,
|
||||||
content: super::MessageContent::Text(
|
content: super::MessageContent::Text(
|
||||||
"This is yet another message that shall be preserved.".to_owned(),
|
"This is yet another message that shall be preserved.".to_owned(),
|
||||||
),
|
),
|
||||||
@@ -213,6 +219,7 @@ pub mod test {
|
|||||||
|
|
||||||
let prompt = super::Message {
|
let prompt = super::Message {
|
||||||
author: super::Author::User,
|
author: super::Author::User,
|
||||||
|
sender_id: None,
|
||||||
content: super::MessageContent::Text("あなたはボットです。".to_string()),
|
content: super::MessageContent::Text("あなたはボットです。".to_string()),
|
||||||
timestamp: chrono::Utc::now(),
|
timestamp: chrono::Utc::now(),
|
||||||
};
|
};
|
||||||
@@ -227,6 +234,7 @@ pub mod test {
|
|||||||
|
|
||||||
let first = super::Message {
|
let first = super::Message {
|
||||||
author: super::Author::User,
|
author: super::Author::User,
|
||||||
|
sender_id: None,
|
||||||
content: super::MessageContent::Text("こんにちは!".to_string()),
|
content: super::MessageContent::Text("こんにちは!".to_string()),
|
||||||
timestamp: chrono::Utc::now(),
|
timestamp: chrono::Utc::now(),
|
||||||
};
|
};
|
||||||
@@ -241,6 +249,7 @@ pub mod test {
|
|||||||
|
|
||||||
let second = super::Message {
|
let second = super::Message {
|
||||||
author: super::Author::Assistant,
|
author: super::Author::Assistant,
|
||||||
|
sender_id: None,
|
||||||
content: super::MessageContent::Text("こんにちは。今日は元気ですか。".to_string()),
|
content: super::MessageContent::Text("こんにちは。今日は元気ですか。".to_string()),
|
||||||
timestamp: chrono::Utc::now(),
|
timestamp: chrono::Utc::now(),
|
||||||
};
|
};
|
||||||
@@ -255,6 +264,7 @@ pub mod test {
|
|||||||
|
|
||||||
let third = super::Message {
|
let third = super::Message {
|
||||||
author: super::Author::User,
|
author: super::Author::User,
|
||||||
|
sender_id: None,
|
||||||
content: super::MessageContent::Text(
|
content: super::MessageContent::Text(
|
||||||
"これは第3のメッセージなので、保存されます。".to_string(),
|
"これは第3のメッセージなので、保存されます。".to_string(),
|
||||||
),
|
),
|
||||||
@@ -271,6 +281,7 @@ pub mod test {
|
|||||||
|
|
||||||
let forth = super::Message {
|
let forth = super::Message {
|
||||||
author: super::Author::Assistant,
|
author: super::Author::Assistant,
|
||||||
|
sender_id: None,
|
||||||
content: super::MessageContent::Text(
|
content: super::MessageContent::Text(
|
||||||
"これはもう一つの保存されますメッセージです。".to_string(),
|
"これはもう一つの保存されますメッセージです。".to_string(),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -17,14 +17,17 @@ pub fn convert_matrix_message_to_llm_message(
|
|||||||
|
|
||||||
fn convert_bot_message(matrix_message: &MatrixMessage) -> Option<Message> {
|
fn convert_bot_message(matrix_message: &MatrixMessage) -> Option<Message> {
|
||||||
match &matrix_message.content {
|
match &matrix_message.content {
|
||||||
MatrixMessageContent::Text(text) => {
|
MatrixMessageContent::Text(text) => convert_bot_text_message(
|
||||||
convert_bot_text_message(text, &matrix_message.timestamp)
|
text,
|
||||||
}
|
&matrix_message.timestamp,
|
||||||
|
matrix_message.sender_id.clone(),
|
||||||
|
),
|
||||||
MatrixMessageContent::Notice(text) => {
|
MatrixMessageContent::Notice(text) => {
|
||||||
convert_bot_notice_message(text, &matrix_message.timestamp)
|
convert_bot_notice_message(text, &matrix_message.timestamp)
|
||||||
}
|
}
|
||||||
MatrixMessageContent::Image(image_content, mime_type, media_bytes) => Some(Message {
|
MatrixMessageContent::Image(image_content, mime_type, media_bytes) => Some(Message {
|
||||||
author: Author::Assistant,
|
author: Author::Assistant,
|
||||||
|
sender_id: Some(matrix_message.sender_id.clone()),
|
||||||
content: MessageContent::Image(ImageDetails::new(
|
content: MessageContent::Image(ImageDetails::new(
|
||||||
image_content.clone(),
|
image_content.clone(),
|
||||||
mime_type.clone(),
|
mime_type.clone(),
|
||||||
@@ -34,6 +37,7 @@ fn convert_bot_message(matrix_message: &MatrixMessage) -> Option<Message> {
|
|||||||
}),
|
}),
|
||||||
MatrixMessageContent::File(file_content, mime_type, media_bytes) => Some(Message {
|
MatrixMessageContent::File(file_content, mime_type, media_bytes) => Some(Message {
|
||||||
author: Author::Assistant,
|
author: Author::Assistant,
|
||||||
|
sender_id: Some(matrix_message.sender_id.clone()),
|
||||||
content: MessageContent::File(FileDetails::new(
|
content: MessageContent::File(FileDetails::new(
|
||||||
file_content.clone(),
|
file_content.clone(),
|
||||||
mime_type.clone(),
|
mime_type.clone(),
|
||||||
@@ -47,9 +51,11 @@ fn convert_bot_message(matrix_message: &MatrixMessage) -> Option<Message> {
|
|||||||
fn convert_bot_text_message(
|
fn convert_bot_text_message(
|
||||||
text: &str,
|
text: &str,
|
||||||
timestamp: &chrono::DateTime<chrono::Utc>,
|
timestamp: &chrono::DateTime<chrono::Utc>,
|
||||||
|
sender_id: OwnedUserId,
|
||||||
) -> Option<Message> {
|
) -> Option<Message> {
|
||||||
Some(Message {
|
Some(Message {
|
||||||
author: Author::Assistant,
|
author: Author::Assistant,
|
||||||
|
sender_id: Some(sender_id),
|
||||||
content: MessageContent::Text(text.to_owned()),
|
content: MessageContent::Text(text.to_owned()),
|
||||||
timestamp: timestamp.to_owned(),
|
timestamp: timestamp.to_owned(),
|
||||||
})
|
})
|
||||||
@@ -68,8 +74,10 @@ fn convert_bot_notice_message(
|
|||||||
|
|
||||||
if let Some(text) = text_to_speech_utils::parse_transcribed_message_text(text) {
|
if let Some(text) = text_to_speech_utils::parse_transcribed_message_text(text) {
|
||||||
// This is a transcription message. We remove the prefix and consider it as a message sent by the user.
|
// This is a transcription message. We remove the prefix and consider it as a message sent by the user.
|
||||||
|
// sender_id is None because the original speaker is unknown.
|
||||||
return Some(Message {
|
return Some(Message {
|
||||||
author: Author::User,
|
author: Author::User,
|
||||||
|
sender_id: None,
|
||||||
content: MessageContent::Text(text.to_owned()),
|
content: MessageContent::Text(text.to_owned()),
|
||||||
timestamp: timestamp.to_owned(),
|
timestamp: timestamp.to_owned(),
|
||||||
});
|
});
|
||||||
@@ -82,16 +90,19 @@ fn convert_user_message(matrix_message: &MatrixMessage) -> Option<Message> {
|
|||||||
match &matrix_message.content {
|
match &matrix_message.content {
|
||||||
MatrixMessageContent::Text(text) => Some(Message {
|
MatrixMessageContent::Text(text) => Some(Message {
|
||||||
author: Author::User,
|
author: Author::User,
|
||||||
|
sender_id: Some(matrix_message.sender_id.clone()),
|
||||||
content: MessageContent::Text(text.clone()),
|
content: MessageContent::Text(text.clone()),
|
||||||
timestamp: matrix_message.timestamp.to_owned(),
|
timestamp: matrix_message.timestamp.to_owned(),
|
||||||
}),
|
}),
|
||||||
MatrixMessageContent::Notice(text) => Some(Message {
|
MatrixMessageContent::Notice(text) => Some(Message {
|
||||||
author: Author::User,
|
author: Author::User,
|
||||||
|
sender_id: Some(matrix_message.sender_id.clone()),
|
||||||
content: MessageContent::Text(text.clone()),
|
content: MessageContent::Text(text.clone()),
|
||||||
timestamp: matrix_message.timestamp.to_owned(),
|
timestamp: matrix_message.timestamp.to_owned(),
|
||||||
}),
|
}),
|
||||||
MatrixMessageContent::Image(image_content, mime_type, media_bytes) => Some(Message {
|
MatrixMessageContent::Image(image_content, mime_type, media_bytes) => Some(Message {
|
||||||
author: Author::User,
|
author: Author::User,
|
||||||
|
sender_id: Some(matrix_message.sender_id.clone()),
|
||||||
content: MessageContent::Image(ImageDetails::new(
|
content: MessageContent::Image(ImageDetails::new(
|
||||||
image_content.clone(),
|
image_content.clone(),
|
||||||
mime_type.clone(),
|
mime_type.clone(),
|
||||||
@@ -101,6 +112,7 @@ fn convert_user_message(matrix_message: &MatrixMessage) -> Option<Message> {
|
|||||||
}),
|
}),
|
||||||
MatrixMessageContent::File(file_content, mime_type, media_bytes) => Some(Message {
|
MatrixMessageContent::File(file_content, mime_type, media_bytes) => Some(Message {
|
||||||
author: Author::User,
|
author: Author::User,
|
||||||
|
sender_id: Some(matrix_message.sender_id.clone()),
|
||||||
content: MessageContent::File(FileDetails::new(
|
content: MessageContent::File(FileDetails::new(
|
||||||
file_content.clone(),
|
file_content.clone(),
|
||||||
mime_type.clone(),
|
mime_type.clone(),
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ use super::roomconfig::RoomConfig;
|
|||||||
|
|
||||||
use crate::entity::roomconfig::{
|
use crate::entity::roomconfig::{
|
||||||
SpeechToTextFlowType, SpeechToTextMessageTypeForNonThreadedOnlyTranscribedMessages,
|
SpeechToTextFlowType, SpeechToTextMessageTypeForNonThreadedOnlyTranscribedMessages,
|
||||||
TextGenerationAutoUsage, TextGenerationPrefixRequirementType, TextToSpeechBotMessagesFlowType,
|
TextGenerationAutoUsage, TextGenerationPrefixRequirementType, TextGenerationSenderContextMode,
|
||||||
TextToSpeechUserMessagesFlowType, defaults as roomconfig_defaults,
|
TextToSpeechBotMessagesFlowType, TextToSpeechUserMessagesFlowType,
|
||||||
|
defaults as roomconfig_defaults,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -135,6 +136,20 @@ impl RoomConfigContext {
|
|||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn text_generation_sender_context_mode(&self) -> TextGenerationSenderContextMode {
|
||||||
|
self.room_config
|
||||||
|
.settings
|
||||||
|
.text_generation
|
||||||
|
.sender_context_mode
|
||||||
|
.or({
|
||||||
|
self.global_config
|
||||||
|
.fallback_room_settings
|
||||||
|
.text_generation
|
||||||
|
.sender_context_mode
|
||||||
|
})
|
||||||
|
.unwrap_or(roomconfig_defaults::TEXT_GENERATION_SENDER_CONTEXT_MODE)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn text_generation_prefix_requirement_type(&self) -> TextGenerationPrefixRequirementType {
|
pub fn text_generation_prefix_requirement_type(&self) -> TextGenerationPrefixRequirementType {
|
||||||
self.room_config
|
self.room_config
|
||||||
.settings
|
.settings
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
use super::{SpeechToTextFlowType, SpeechToTextMessageTypeForNonThreadedOnlyTranscribedMessages};
|
use super::{SpeechToTextFlowType, SpeechToTextMessageTypeForNonThreadedOnlyTranscribedMessages};
|
||||||
use super::{TextGenerationAutoUsage, TextGenerationPrefixRequirementType};
|
use super::{
|
||||||
|
TextGenerationAutoUsage, TextGenerationPrefixRequirementType, TextGenerationSenderContextMode,
|
||||||
|
};
|
||||||
use super::{TextToSpeechBotMessagesFlowType, TextToSpeechUserMessagesFlowType};
|
use super::{TextToSpeechBotMessagesFlowType, TextToSpeechUserMessagesFlowType};
|
||||||
|
|
||||||
pub const TEXT_GENERATION_PREFIX_REQUIREMENT_TYPE: TextGenerationPrefixRequirementType =
|
pub const TEXT_GENERATION_PREFIX_REQUIREMENT_TYPE: TextGenerationPrefixRequirementType =
|
||||||
@@ -7,6 +9,9 @@ pub const TEXT_GENERATION_PREFIX_REQUIREMENT_TYPE: TextGenerationPrefixRequireme
|
|||||||
|
|
||||||
pub const TEXT_GENERATION_AUTO_USAGE: TextGenerationAutoUsage = TextGenerationAutoUsage::Always;
|
pub const TEXT_GENERATION_AUTO_USAGE: TextGenerationAutoUsage = TextGenerationAutoUsage::Always;
|
||||||
|
|
||||||
|
pub const TEXT_GENERATION_SENDER_CONTEXT_MODE: TextGenerationSenderContextMode =
|
||||||
|
TextGenerationSenderContextMode::Disabled;
|
||||||
|
|
||||||
pub const TEXT_TO_SPEECH_BOT_MESSAGES_FLOW_TYPE: TextToSpeechBotMessagesFlowType =
|
pub const TEXT_TO_SPEECH_BOT_MESSAGES_FLOW_TYPE: TextToSpeechBotMessagesFlowType =
|
||||||
TextToSpeechBotMessagesFlowType::OnDemandForVoice;
|
TextToSpeechBotMessagesFlowType::OnDemandForVoice;
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ pub use handler::RoomSettingsHandler;
|
|||||||
pub use speech_to_text::{
|
pub use speech_to_text::{
|
||||||
SpeechToTextFlowType, SpeechToTextMessageTypeForNonThreadedOnlyTranscribedMessages,
|
SpeechToTextFlowType, SpeechToTextMessageTypeForNonThreadedOnlyTranscribedMessages,
|
||||||
};
|
};
|
||||||
pub use text_generation::{TextGenerationAutoUsage, TextGenerationPrefixRequirementType};
|
pub use text_generation::{
|
||||||
|
TextGenerationAutoUsage, TextGenerationPrefixRequirementType, TextGenerationSenderContextMode,
|
||||||
|
};
|
||||||
pub use text_to_speech::{TextToSpeechBotMessagesFlowType, TextToSpeechUserMessagesFlowType};
|
pub use text_to_speech::{TextToSpeechBotMessagesFlowType, TextToSpeechUserMessagesFlowType};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize, EventContent)]
|
#[derive(Clone, Debug, Deserialize, Serialize, EventContent)]
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ pub struct RoomSettingsTextGeneration {
|
|||||||
/// When enabled, the bot will automatically tokenize messages and try to shorten the message context intelligently.
|
/// When enabled, the bot will automatically tokenize messages and try to shorten the message context intelligently.
|
||||||
pub context_management_enabled: Option<bool>,
|
pub context_management_enabled: Option<bool>,
|
||||||
|
|
||||||
|
/// Controls how each message in the conversation context is annotated with sender metadata.
|
||||||
|
pub sender_context_mode: Option<TextGenerationSenderContextMode>,
|
||||||
|
|
||||||
/// Allows customizing the system prompt that the agent would use
|
/// Allows customizing the system prompt that the agent would use
|
||||||
pub prompt_override: Option<String>,
|
pub prompt_override: Option<String>,
|
||||||
|
|
||||||
@@ -111,3 +114,46 @@ impl std::fmt::Display for TextGenerationAutoUsage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq)]
|
||||||
|
pub enum TextGenerationSenderContextMode {
|
||||||
|
#[serde(rename = "disabled")]
|
||||||
|
Disabled,
|
||||||
|
|
||||||
|
#[serde(rename = "matrix_user_id")]
|
||||||
|
MatrixUserId,
|
||||||
|
|
||||||
|
#[serde(rename = "matrix_user_id_and_timestamp")]
|
||||||
|
MatrixUserIdAndTimestamp,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextGenerationSenderContextMode {
|
||||||
|
pub fn choices() -> Vec<Self> {
|
||||||
|
vec![
|
||||||
|
Self::Disabled,
|
||||||
|
Self::MatrixUserId,
|
||||||
|
Self::MatrixUserIdAndTimestamp,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_str(s: &str) -> Option<Self> {
|
||||||
|
match s {
|
||||||
|
"disabled" => Some(Self::Disabled),
|
||||||
|
"matrix_user_id" => Some(Self::MatrixUserId),
|
||||||
|
"matrix_user_id_and_timestamp" => Some(Self::MatrixUserIdAndTimestamp),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for TextGenerationSenderContextMode {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
TextGenerationSenderContextMode::Disabled => write!(f, "disabled"),
|
||||||
|
TextGenerationSenderContextMode::MatrixUserId => write!(f, "matrix_user_id"),
|
||||||
|
TextGenerationSenderContextMode::MatrixUserIdAndTimestamp => {
|
||||||
|
write!(f, "matrix_user_id_and_timestamp")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ use mxlink::helpers::account_data_config::RoomConfigManager as AccountDataRoomCo
|
|||||||
pub use entity::{RoomConfig, RoomConfigCarrierContent, RoomSettings, RoomSettingsHandler};
|
pub use entity::{RoomConfig, RoomConfigCarrierContent, RoomSettings, RoomSettingsHandler};
|
||||||
pub use entity::{
|
pub use entity::{
|
||||||
SpeechToTextFlowType, SpeechToTextMessageTypeForNonThreadedOnlyTranscribedMessages,
|
SpeechToTextFlowType, SpeechToTextMessageTypeForNonThreadedOnlyTranscribedMessages,
|
||||||
TextGenerationAutoUsage, TextGenerationPrefixRequirementType, TextToSpeechBotMessagesFlowType,
|
TextGenerationAutoUsage, TextGenerationPrefixRequirementType, TextGenerationSenderContextMode,
|
||||||
TextToSpeechUserMessagesFlowType,
|
TextToSpeechBotMessagesFlowType, TextToSpeechUserMessagesFlowType,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub type RoomConfigurationManager =
|
pub type RoomConfigurationManager =
|
||||||
|
|||||||
@@ -249,6 +249,13 @@ pub fn status_text_generation_entry_context_management(value: bool, set_where: &
|
|||||||
format!("- ♻️ Context management: `{}` ({})\n", value, set_where)
|
format!("- ♻️ Context management: `{}` ({})\n", value, set_where)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn status_text_generation_entry_sender_context(
|
||||||
|
value: impl std::fmt::Display,
|
||||||
|
set_where: &str,
|
||||||
|
) -> String {
|
||||||
|
format!("- 👤 Sender context mode: `{}` ({})\n", value, set_where)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn status_text_generation_entry_prompt(value: &str, set_where: &str) -> String {
|
pub fn status_text_generation_entry_prompt(value: &str, set_where: &str) -> String {
|
||||||
let value = value.trim();
|
let value = value.trim();
|
||||||
|
|
||||||
|
|||||||
@@ -132,6 +132,18 @@ pub fn text_generation_context_management_intro() -> String {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn text_generation_sender_context_heading() -> &'static str {
|
||||||
|
"👤 Sender Context Mode"
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn text_generation_sender_context_intro() -> String {
|
||||||
|
format!(
|
||||||
|
"{}\n{}",
|
||||||
|
"Controls whether the bot attaches sender information to conversation messages before sending them to the model.",
|
||||||
|
"`disabled` leaves messages unchanged, `matrix_user_id` adds `[sender=@alice:example.com]`, and `matrix_user_id_and_timestamp` adds `[sender=@alice:example.com sent_at=2026-03-23T14:30:00Z]`. Enabling this sends Matrix user IDs, and optionally timestamps, to the model provider.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn text_generation_prompt_override_heading() -> &'static str {
|
pub fn text_generation_prompt_override_heading() -> &'static str {
|
||||||
"⌨️ Prompt Override"
|
"⌨️ Prompt Override"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user