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:
@@ -71,6 +71,7 @@ impl ControllerTrait for Controller {
|
||||
|
||||
let messages = vec![LLMMessage {
|
||||
author: LLMAuthor::User,
|
||||
sender_id: None,
|
||||
content: LLMMessageContent::Text("Hello!".to_string()),
|
||||
timestamp: chrono::Utc::now(),
|
||||
}];
|
||||
@@ -108,6 +109,7 @@ impl ControllerTrait for Controller {
|
||||
} else {
|
||||
Some(LLMMessage {
|
||||
author: LLMAuthor::Prompt,
|
||||
sender_id: None,
|
||||
content: LLMMessageContent::Text(prompt_text),
|
||||
timestamp: chrono::Utc::now(),
|
||||
})
|
||||
|
||||
@@ -67,6 +67,7 @@ impl ControllerTrait for Controller {
|
||||
|
||||
let messages = vec![LLMMessage {
|
||||
author: LLMAuthor::User,
|
||||
sender_id: None,
|
||||
content: LLMMessageContent::Text("Hello!".to_string()),
|
||||
timestamp: chrono::Utc::now(),
|
||||
}];
|
||||
@@ -104,6 +105,7 @@ impl ControllerTrait for Controller {
|
||||
} else {
|
||||
Some(LLMMessage {
|
||||
author: LLMAuthor::Prompt,
|
||||
sender_id: None,
|
||||
content: LLMMessageContent::Text(prompt_text),
|
||||
timestamp: chrono::Utc::now(),
|
||||
})
|
||||
|
||||
@@ -64,6 +64,7 @@ impl ControllerTrait for Controller {
|
||||
|
||||
let messages = vec![LLMMessage {
|
||||
author: LLMAuthor::User,
|
||||
sender_id: None,
|
||||
content: LLMMessageContent::Text("Hello!".to_string()),
|
||||
timestamp: chrono::Utc::now(),
|
||||
}];
|
||||
@@ -101,6 +102,7 @@ impl ControllerTrait for Controller {
|
||||
} else {
|
||||
Some(LLMMessage {
|
||||
author: LLMAuthor::Prompt,
|
||||
sender_id: None,
|
||||
content: LLMMessageContent::Text(prompt_text),
|
||||
timestamp: chrono::Utc::now(),
|
||||
})
|
||||
|
||||
@@ -3,7 +3,8 @@ use crate::{
|
||||
entity::roomconfig::{
|
||||
SpeechToTextFlowType, SpeechToTextMessageTypeForNonThreadedOnlyTranscribedMessages,
|
||||
TextGenerationAutoUsage, TextGenerationPrefixRequirementType,
|
||||
TextToSpeechBotMessagesFlowType, TextToSpeechUserMessagesFlowType,
|
||||
TextGenerationSenderContextMode, TextToSpeechBotMessagesFlowType,
|
||||
TextToSpeechUserMessagesFlowType,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -48,6 +49,9 @@ pub enum ConfigTextGenerationSettingRelatedControllerType {
|
||||
|
||||
GetTemperatureOverride,
|
||||
SetTemperatureOverride(Option<f32>),
|
||||
|
||||
GetSenderContextMode,
|
||||
SetSenderContextMode(Option<TextGenerationSenderContextMode>),
|
||||
}
|
||||
|
||||
#[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 {
|
||||
name: "per-room text-to-speech/speed-override getter",
|
||||
input: "room text-to-speech speed-override",
|
||||
|
||||
@@ -3,7 +3,10 @@ mod tests;
|
||||
|
||||
use crate::{
|
||||
controller::ControllerType,
|
||||
entity::roomconfig::{TextGenerationAutoUsage, TextGenerationPrefixRequirementType},
|
||||
entity::roomconfig::{
|
||||
TextGenerationAutoUsage, TextGenerationPrefixRequirementType,
|
||||
TextGenerationSenderContextMode,
|
||||
},
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
fn determine_controller_prefix_requirement_type() {
|
||||
use super::ConfigTextGenerationSettingRelatedControllerType;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::entity::roomconfig::{
|
||||
RoomSettings, TextGenerationAutoUsage, TextGenerationPrefixRequirementType,
|
||||
TextGenerationSenderContextMode,
|
||||
};
|
||||
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::{
|
||||
SpeechToTextFlowType, SpeechToTextMessageTypeForNonThreadedOnlyTranscribedMessages,
|
||||
TextGenerationAutoUsage, TextGenerationPrefixRequirementType,
|
||||
TextToSpeechBotMessagesFlowType, TextToSpeechUserMessagesFlowType,
|
||||
TextGenerationSenderContextMode, TextToSpeechBotMessagesFlowType,
|
||||
TextToSpeechUserMessagesFlowType,
|
||||
},
|
||||
},
|
||||
strings,
|
||||
@@ -233,6 +234,46 @@ fn build_section_text_generation(command_prefix: &str, bot_username: &str) -> St
|
||||
));
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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::roomconfig::{
|
||||
SpeechToTextFlowType, SpeechToTextMessageTypeForNonThreadedOnlyTranscribedMessages,
|
||||
TextToSpeechBotMessagesFlowType, TextToSpeechUserMessagesFlowType,
|
||||
TextGenerationSenderContextMode, TextToSpeechBotMessagesFlowType,
|
||||
TextToSpeechUserMessagesFlowType,
|
||||
};
|
||||
use crate::strings;
|
||||
use crate::utils::text_to_speech::create_transcribed_message_text;
|
||||
@@ -23,6 +24,7 @@ use crate::{
|
||||
Bot,
|
||||
conversation::{
|
||||
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,
|
||||
},
|
||||
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!(
|
||||
agent_id = agent.identifier().as_string(),
|
||||
provider = format!("{}", agent.definition().provider.clone()),
|
||||
@@ -761,3 +770,238 @@ async fn generate_and_send_tts_for_message(
|
||||
)
|
||||
.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",
|
||||
messages: vec![Message {
|
||||
author: Author::User,
|
||||
sender_id: None,
|
||||
content: MessageContent::Text("Must be blue".to_owned()),
|
||||
timestamp,
|
||||
}],
|
||||
@@ -75,16 +76,19 @@ mod tests {
|
||||
messages: vec![
|
||||
Message {
|
||||
author: Author::User,
|
||||
sender_id: None,
|
||||
content: MessageContent::Text("Must be blue".to_owned()),
|
||||
timestamp,
|
||||
},
|
||||
Message {
|
||||
author: Author::Assistant,
|
||||
sender_id: None,
|
||||
content: MessageContent::Text("Whatever".to_owned()),
|
||||
timestamp,
|
||||
},
|
||||
Message {
|
||||
author: Author::User,
|
||||
sender_id: None,
|
||||
content: MessageContent::Text(
|
||||
"Must be 3-legged.\nMust be flying.".to_owned(),
|
||||
),
|
||||
@@ -99,21 +103,25 @@ mod tests {
|
||||
messages: vec![
|
||||
Message {
|
||||
author: Author::User,
|
||||
sender_id: None,
|
||||
content: MessageContent::Text("Must be blue".to_owned()),
|
||||
timestamp,
|
||||
},
|
||||
Message {
|
||||
author: Author::Assistant,
|
||||
sender_id: None,
|
||||
content: MessageContent::Text("Whatever".to_owned()),
|
||||
timestamp,
|
||||
},
|
||||
Message {
|
||||
author: Author::User,
|
||||
sender_id: None,
|
||||
content: MessageContent::Text("Again".to_owned()),
|
||||
timestamp,
|
||||
},
|
||||
Message {
|
||||
author: Author::User,
|
||||
sender_id: None,
|
||||
content: MessageContent::Text("again".to_owned()),
|
||||
timestamp,
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use mxlink::matrix_sdk::ruma::OwnedUserId;
|
||||
use mxlink::matrix_sdk::ruma::events::room::message::{
|
||||
FileMessageEventContent, ImageMessageEventContent,
|
||||
};
|
||||
@@ -16,6 +17,7 @@ pub enum Author {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Message {
|
||||
pub author: Author,
|
||||
pub sender_id: Option<OwnedUserId>,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
pub content: MessageContent,
|
||||
}
|
||||
@@ -104,6 +106,11 @@ impl Conversation {
|
||||
///
|
||||
/// Certain models (like Anthropic) cannot tolerate consecutive messages by the same author,
|
||||
/// 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
|
||||
pub fn combine_consecutive_messages(&self) -> Conversation {
|
||||
// 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_str(message_text_content);
|
||||
}
|
||||
|
||||
if last_message.sender_id != message.sender_id {
|
||||
last_message.sender_id = None;
|
||||
}
|
||||
}
|
||||
|
||||
Conversation {
|
||||
@@ -150,7 +161,7 @@ impl Conversation {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use chrono::{TimeZone, Utc};
|
||||
use mxlink::matrix_sdk::ruma::OwnedMxcUri;
|
||||
use mxlink::matrix_sdk::ruma::{OwnedMxcUri, OwnedUserId};
|
||||
use mxlink::mime;
|
||||
|
||||
#[test]
|
||||
@@ -173,21 +184,25 @@ mod tests {
|
||||
// User's turn
|
||||
Message {
|
||||
author: Author::User,
|
||||
sender_id: None,
|
||||
content: MessageContent::Text("Hello".to_string()),
|
||||
timestamp: timestamp_1,
|
||||
},
|
||||
Message {
|
||||
author: Author::User,
|
||||
sender_id: None,
|
||||
content: MessageContent::Text("How are you?".to_string()),
|
||||
timestamp: timestamp_2,
|
||||
},
|
||||
Message {
|
||||
author: Author::User,
|
||||
sender_id: None,
|
||||
content: MessageContent::Text("I'm OK, btw.".to_string()),
|
||||
timestamp: timestamp_3,
|
||||
},
|
||||
Message {
|
||||
author: Author::User,
|
||||
sender_id: None,
|
||||
content: MessageContent::Image(ImageDetails::new(
|
||||
image_event_content.clone(),
|
||||
mime::IMAGE_PNG,
|
||||
@@ -197,28 +212,33 @@ mod tests {
|
||||
},
|
||||
Message {
|
||||
author: Author::User,
|
||||
sender_id: None,
|
||||
content: MessageContent::Text("Above is an image.".to_string()),
|
||||
timestamp: timestamp_4,
|
||||
},
|
||||
Message {
|
||||
author: Author::User,
|
||||
sender_id: None,
|
||||
content: MessageContent::Text("Would you take a look at it?".to_string()),
|
||||
timestamp: timestamp_4,
|
||||
},
|
||||
// Assistant's turn
|
||||
Message {
|
||||
author: Author::Assistant,
|
||||
sender_id: None,
|
||||
content: MessageContent::Text("Hi there!".to_string()),
|
||||
timestamp: timestamp_2,
|
||||
},
|
||||
Message {
|
||||
author: Author::Assistant,
|
||||
sender_id: None,
|
||||
content: MessageContent::Text("I'm doing well, thank you.".to_string()),
|
||||
timestamp: timestamp_3,
|
||||
},
|
||||
// User's turn
|
||||
Message {
|
||||
author: Author::User,
|
||||
sender_id: None,
|
||||
content: MessageContent::Text("That's great!".to_string()),
|
||||
timestamp: timestamp_3,
|
||||
},
|
||||
@@ -267,4 +287,39 @@ mod tests {
|
||||
);
|
||||
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();
|
||||
|
||||
assert_eq!(llm_message.author, Author::Assistant);
|
||||
assert_eq!(llm_message.sender_id, Some(bot_user_id.clone()));
|
||||
assert_eq!(
|
||||
llm_message.content,
|
||||
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();
|
||||
|
||||
assert_eq!(llm_message.author, Author::User);
|
||||
assert_eq!(llm_message.sender_id, None);
|
||||
assert_eq!(
|
||||
llm_message.content,
|
||||
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());
|
||||
}
|
||||
|
||||
#[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]
|
||||
fn test_other_notice_messages_by_the_bot_are_ignored() {
|
||||
// Also see `test_notice_error_messages_by_bot_are_ignored()`.
|
||||
|
||||
@@ -89,6 +89,7 @@ pub mod test {
|
||||
|
||||
let message = super::Message {
|
||||
author: super::Author::User,
|
||||
sender_id: None,
|
||||
content: super::MessageContent::Text("Hello there!".to_string()),
|
||||
timestamp: chrono::Utc::now(),
|
||||
};
|
||||
@@ -108,6 +109,7 @@ pub mod test {
|
||||
|
||||
let prompt = super::Message {
|
||||
author: super::Author::Prompt,
|
||||
sender_id: None,
|
||||
content: super::MessageContent::Text("You are a bot!".to_string()),
|
||||
timestamp: chrono::Utc::now(),
|
||||
};
|
||||
@@ -122,6 +124,7 @@ pub mod test {
|
||||
|
||||
let first = super::Message {
|
||||
author: super::Author::User,
|
||||
sender_id: None,
|
||||
content: super::MessageContent::Text("Hello there!".to_string()),
|
||||
timestamp: chrono::Utc::now(),
|
||||
};
|
||||
@@ -136,6 +139,7 @@ pub mod test {
|
||||
|
||||
let second = super::Message {
|
||||
author: super::Author::Assistant,
|
||||
sender_id: None,
|
||||
content: super::MessageContent::Text("Hello!".to_string()),
|
||||
timestamp: chrono::Utc::now(),
|
||||
};
|
||||
@@ -150,6 +154,7 @@ pub mod test {
|
||||
|
||||
let third = super::Message {
|
||||
author: super::Author::User,
|
||||
sender_id: None,
|
||||
content: super::MessageContent::Text(
|
||||
"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 {
|
||||
author: super::Author::Assistant,
|
||||
sender_id: None,
|
||||
content: super::MessageContent::Text(
|
||||
"This is yet another message that shall be preserved.".to_owned(),
|
||||
),
|
||||
@@ -213,6 +219,7 @@ pub mod test {
|
||||
|
||||
let prompt = super::Message {
|
||||
author: super::Author::User,
|
||||
sender_id: None,
|
||||
content: super::MessageContent::Text("あなたはボットです。".to_string()),
|
||||
timestamp: chrono::Utc::now(),
|
||||
};
|
||||
@@ -227,6 +234,7 @@ pub mod test {
|
||||
|
||||
let first = super::Message {
|
||||
author: super::Author::User,
|
||||
sender_id: None,
|
||||
content: super::MessageContent::Text("こんにちは!".to_string()),
|
||||
timestamp: chrono::Utc::now(),
|
||||
};
|
||||
@@ -241,6 +249,7 @@ pub mod test {
|
||||
|
||||
let second = super::Message {
|
||||
author: super::Author::Assistant,
|
||||
sender_id: None,
|
||||
content: super::MessageContent::Text("こんにちは。今日は元気ですか。".to_string()),
|
||||
timestamp: chrono::Utc::now(),
|
||||
};
|
||||
@@ -255,6 +264,7 @@ pub mod test {
|
||||
|
||||
let third = super::Message {
|
||||
author: super::Author::User,
|
||||
sender_id: None,
|
||||
content: super::MessageContent::Text(
|
||||
"これは第3のメッセージなので、保存されます。".to_string(),
|
||||
),
|
||||
@@ -271,6 +281,7 @@ pub mod test {
|
||||
|
||||
let forth = super::Message {
|
||||
author: super::Author::Assistant,
|
||||
sender_id: None,
|
||||
content: super::MessageContent::Text(
|
||||
"これはもう一つの保存されますメッセージです。".to_string(),
|
||||
),
|
||||
|
||||
@@ -17,14 +17,17 @@ pub fn convert_matrix_message_to_llm_message(
|
||||
|
||||
fn convert_bot_message(matrix_message: &MatrixMessage) -> Option<Message> {
|
||||
match &matrix_message.content {
|
||||
MatrixMessageContent::Text(text) => {
|
||||
convert_bot_text_message(text, &matrix_message.timestamp)
|
||||
}
|
||||
MatrixMessageContent::Text(text) => convert_bot_text_message(
|
||||
text,
|
||||
&matrix_message.timestamp,
|
||||
matrix_message.sender_id.clone(),
|
||||
),
|
||||
MatrixMessageContent::Notice(text) => {
|
||||
convert_bot_notice_message(text, &matrix_message.timestamp)
|
||||
}
|
||||
MatrixMessageContent::Image(image_content, mime_type, media_bytes) => Some(Message {
|
||||
author: Author::Assistant,
|
||||
sender_id: Some(matrix_message.sender_id.clone()),
|
||||
content: MessageContent::Image(ImageDetails::new(
|
||||
image_content.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 {
|
||||
author: Author::Assistant,
|
||||
sender_id: Some(matrix_message.sender_id.clone()),
|
||||
content: MessageContent::File(FileDetails::new(
|
||||
file_content.clone(),
|
||||
mime_type.clone(),
|
||||
@@ -47,9 +51,11 @@ fn convert_bot_message(matrix_message: &MatrixMessage) -> Option<Message> {
|
||||
fn convert_bot_text_message(
|
||||
text: &str,
|
||||
timestamp: &chrono::DateTime<chrono::Utc>,
|
||||
sender_id: OwnedUserId,
|
||||
) -> Option<Message> {
|
||||
Some(Message {
|
||||
author: Author::Assistant,
|
||||
sender_id: Some(sender_id),
|
||||
content: MessageContent::Text(text.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) {
|
||||
// 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 {
|
||||
author: Author::User,
|
||||
sender_id: None,
|
||||
content: MessageContent::Text(text.to_owned()),
|
||||
timestamp: timestamp.to_owned(),
|
||||
});
|
||||
@@ -82,16 +90,19 @@ fn convert_user_message(matrix_message: &MatrixMessage) -> Option<Message> {
|
||||
match &matrix_message.content {
|
||||
MatrixMessageContent::Text(text) => Some(Message {
|
||||
author: Author::User,
|
||||
sender_id: Some(matrix_message.sender_id.clone()),
|
||||
content: MessageContent::Text(text.clone()),
|
||||
timestamp: matrix_message.timestamp.to_owned(),
|
||||
}),
|
||||
MatrixMessageContent::Notice(text) => Some(Message {
|
||||
author: Author::User,
|
||||
sender_id: Some(matrix_message.sender_id.clone()),
|
||||
content: MessageContent::Text(text.clone()),
|
||||
timestamp: matrix_message.timestamp.to_owned(),
|
||||
}),
|
||||
MatrixMessageContent::Image(image_content, mime_type, media_bytes) => Some(Message {
|
||||
author: Author::User,
|
||||
sender_id: Some(matrix_message.sender_id.clone()),
|
||||
content: MessageContent::Image(ImageDetails::new(
|
||||
image_content.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 {
|
||||
author: Author::User,
|
||||
sender_id: Some(matrix_message.sender_id.clone()),
|
||||
content: MessageContent::File(FileDetails::new(
|
||||
file_content.clone(),
|
||||
mime_type.clone(),
|
||||
|
||||
@@ -5,8 +5,9 @@ use super::roomconfig::RoomConfig;
|
||||
|
||||
use crate::entity::roomconfig::{
|
||||
SpeechToTextFlowType, SpeechToTextMessageTypeForNonThreadedOnlyTranscribedMessages,
|
||||
TextGenerationAutoUsage, TextGenerationPrefixRequirementType, TextToSpeechBotMessagesFlowType,
|
||||
TextToSpeechUserMessagesFlowType, defaults as roomconfig_defaults,
|
||||
TextGenerationAutoUsage, TextGenerationPrefixRequirementType, TextGenerationSenderContextMode,
|
||||
TextToSpeechBotMessagesFlowType, TextToSpeechUserMessagesFlowType,
|
||||
defaults as roomconfig_defaults,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -135,6 +136,20 @@ impl RoomConfigContext {
|
||||
.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 {
|
||||
self.room_config
|
||||
.settings
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use super::{SpeechToTextFlowType, SpeechToTextMessageTypeForNonThreadedOnlyTranscribedMessages};
|
||||
use super::{TextGenerationAutoUsage, TextGenerationPrefixRequirementType};
|
||||
use super::{
|
||||
TextGenerationAutoUsage, TextGenerationPrefixRequirementType, TextGenerationSenderContextMode,
|
||||
};
|
||||
use super::{TextToSpeechBotMessagesFlowType, TextToSpeechUserMessagesFlowType};
|
||||
|
||||
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_SENDER_CONTEXT_MODE: TextGenerationSenderContextMode =
|
||||
TextGenerationSenderContextMode::Disabled;
|
||||
|
||||
pub const TEXT_TO_SPEECH_BOT_MESSAGES_FLOW_TYPE: TextToSpeechBotMessagesFlowType =
|
||||
TextToSpeechBotMessagesFlowType::OnDemandForVoice;
|
||||
|
||||
|
||||
@@ -16,7 +16,9 @@ pub use handler::RoomSettingsHandler;
|
||||
pub use speech_to_text::{
|
||||
SpeechToTextFlowType, SpeechToTextMessageTypeForNonThreadedOnlyTranscribedMessages,
|
||||
};
|
||||
pub use text_generation::{TextGenerationAutoUsage, TextGenerationPrefixRequirementType};
|
||||
pub use text_generation::{
|
||||
TextGenerationAutoUsage, TextGenerationPrefixRequirementType, TextGenerationSenderContextMode,
|
||||
};
|
||||
pub use text_to_speech::{TextToSpeechBotMessagesFlowType, TextToSpeechUserMessagesFlowType};
|
||||
|
||||
#[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.
|
||||
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
|
||||
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::{
|
||||
SpeechToTextFlowType, SpeechToTextMessageTypeForNonThreadedOnlyTranscribedMessages,
|
||||
TextGenerationAutoUsage, TextGenerationPrefixRequirementType, TextToSpeechBotMessagesFlowType,
|
||||
TextToSpeechUserMessagesFlowType,
|
||||
TextGenerationAutoUsage, TextGenerationPrefixRequirementType, TextGenerationSenderContextMode,
|
||||
TextToSpeechBotMessagesFlowType, TextToSpeechUserMessagesFlowType,
|
||||
};
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
"⌨️ Prompt Override"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user