2024-09-12 13:44:06 +03:00
|
|
|
use mxlink::matrix_sdk::ruma::events::room::message::AudioMessageEventContent;
|
|
|
|
|
use mxlink::matrix_sdk::ruma::OwnedEventId;
|
|
|
|
|
use mxlink::{MatrixLink, MessageResponseType};
|
|
|
|
|
|
|
|
|
|
use tracing::Instrument;
|
|
|
|
|
|
2024-09-21 14:05:19 +00:00
|
|
|
use crate::agent::provider::{
|
|
|
|
|
SpeechToTextParams, TextGenerationParams, TextGenerationPromptVariables,
|
|
|
|
|
};
|
2024-09-12 13:44:06 +03:00
|
|
|
use crate::agent::AgentInstance;
|
|
|
|
|
use crate::agent::AgentPurpose;
|
|
|
|
|
use crate::agent::ControllerTrait;
|
|
|
|
|
use crate::controller::utils::agent::get_effective_agent_for_purpose_or_complain;
|
|
|
|
|
use crate::conversation::matrix::MatrixMessageProcessingParams;
|
|
|
|
|
use crate::entity::roomconfig::{
|
|
|
|
|
SpeechToTextFlowType, TextToSpeechBotMessagesFlowType, TextToSpeechUserMessagesFlowType,
|
|
|
|
|
};
|
|
|
|
|
use crate::entity::MessagePayload;
|
|
|
|
|
use crate::strings;
|
|
|
|
|
use crate::utils::text_to_speech::create_transcribed_message_text;
|
2024-10-01 21:06:54 +03:00
|
|
|
use crate::{
|
|
|
|
|
conversation::{
|
|
|
|
|
create_llm_conversation_for_matrix_reply_chain, create_llm_conversation_for_matrix_thread,
|
|
|
|
|
matrix::create_list_of_bot_user_prefixes_to_strip,
|
|
|
|
|
},
|
|
|
|
|
entity::MessageContext,
|
|
|
|
|
Bot,
|
|
|
|
|
};
|
2024-09-12 13:44:06 +03:00
|
|
|
|
|
|
|
|
#[derive(Debug, PartialEq)]
|
|
|
|
|
pub enum ChatCompletionControllerType {
|
2024-10-01 21:06:54 +03:00
|
|
|
// Invoked via a command prefix (e.g. `!bai Hello!`)
|
|
|
|
|
TextCommand,
|
|
|
|
|
// Invoked via a mention (e.g. `@baibot Hello!`)
|
|
|
|
|
TextMention,
|
|
|
|
|
// Invoked via a direct message (e.g. `Hello!`)
|
|
|
|
|
TextDirect,
|
|
|
|
|
|
|
|
|
|
Audio,
|
2024-09-12 13:44:06 +03:00
|
|
|
|
2024-10-01 21:06:54 +03:00
|
|
|
ThreadMention,
|
|
|
|
|
ReplyMention,
|
2024-09-12 13:44:06 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct TextToSpeechEligiblePayload {
|
|
|
|
|
text: String,
|
|
|
|
|
event_id: OwnedEventId,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
enum TextToSpeechParams {
|
|
|
|
|
Perform(TextToSpeechEligiblePayload, MessageResponseType),
|
|
|
|
|
Offer(TextToSpeechEligiblePayload, MessageResponseType),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn handle(
|
|
|
|
|
bot: &Bot,
|
|
|
|
|
matrix_link: MatrixLink,
|
|
|
|
|
message_context: &MessageContext,
|
|
|
|
|
controller_type: &ChatCompletionControllerType,
|
|
|
|
|
) -> anyhow::Result<()> {
|
|
|
|
|
let mut original_message_is_audio = false;
|
|
|
|
|
|
2024-09-14 10:39:20 +03:00
|
|
|
let mut _typing_notice_guard: Option<mxlink::TypingNoticeGuard> = None;
|
|
|
|
|
|
2024-09-12 13:44:06 +03:00
|
|
|
let speech_to_text_flow_type = message_context
|
|
|
|
|
.room_config_context()
|
|
|
|
|
.speech_to_text_flow_type();
|
|
|
|
|
|
|
|
|
|
let mut speech_to_text_created_event_id: Option<OwnedEventId> = None;
|
|
|
|
|
|
|
|
|
|
if let MessagePayload::Audio(audio_content) = &message_context.payload() {
|
|
|
|
|
original_message_is_audio = true;
|
|
|
|
|
|
|
|
|
|
let response_type = match speech_to_text_flow_type {
|
|
|
|
|
SpeechToTextFlowType::Ignore => {
|
|
|
|
|
tracing::debug!("Intentionally ignoring audio message");
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
SpeechToTextFlowType::TranscribeAndGenerateText => {
|
2024-09-14 09:30:58 +03:00
|
|
|
tracing::debug!("Will be transcribing and possibly generating text..");
|
2024-09-12 13:44:06 +03:00
|
|
|
MessageResponseType::InThread(message_context.thread_info().clone())
|
|
|
|
|
}
|
|
|
|
|
SpeechToTextFlowType::OnlyTranscribe => {
|
2024-09-14 09:30:58 +03:00
|
|
|
tracing::debug!("Will only be transcribing audio to text..");
|
2024-09-12 13:44:06 +03:00
|
|
|
if message_context.thread_info().is_thread_root_only() {
|
|
|
|
|
MessageResponseType::Reply(message_context.thread_info().root_event_id.clone())
|
|
|
|
|
} else {
|
|
|
|
|
MessageResponseType::InThread(message_context.thread_info().clone())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2024-09-14 10:39:20 +03:00
|
|
|
if _typing_notice_guard.is_none() {
|
|
|
|
|
_typing_notice_guard = Some(bot.start_typing_notice(message_context.room()).await);
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-12 13:44:06 +03:00
|
|
|
let Some(speech_to_text_created_event_id_result) =
|
|
|
|
|
handle_stage_speech_to_text(bot, message_context, audio_content, response_type).await
|
|
|
|
|
else {
|
|
|
|
|
return Ok(());
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
speech_to_text_created_event_id = Some(speech_to_text_created_event_id_result);
|
|
|
|
|
|
|
|
|
|
if speech_to_text_flow_type == SpeechToTextFlowType::OnlyTranscribe {
|
|
|
|
|
tracing::debug!(
|
|
|
|
|
"Intentionally not continuing with text generation after transcription"
|
|
|
|
|
);
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// We've pushed a transcription to the room.
|
|
|
|
|
// Let's proceed below where we potentially handle text-generation.
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let text_to_speech_stage_params: Option<TextToSpeechParams>;
|
|
|
|
|
|
|
|
|
|
if message_context
|
|
|
|
|
.room_config_context()
|
|
|
|
|
.should_auto_text_generate(original_message_is_audio)
|
|
|
|
|
{
|
2024-09-14 10:39:20 +03:00
|
|
|
if _typing_notice_guard.is_none() {
|
|
|
|
|
_typing_notice_guard = Some(bot.start_typing_notice(message_context.room()).await);
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-12 13:44:06 +03:00
|
|
|
let speech_to_text_created_event_id_reaction_event_id =
|
|
|
|
|
if let Some(speech_to_text_created_event_id) = speech_to_text_created_event_id {
|
|
|
|
|
let reaction_event_response = bot
|
|
|
|
|
.reacting()
|
|
|
|
|
.react_no_fail(
|
|
|
|
|
message_context.room(),
|
|
|
|
|
speech_to_text_created_event_id.clone(),
|
|
|
|
|
strings::PROGRESS_INDICATOR_EMOJI.to_owned(),
|
|
|
|
|
)
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
reaction_event_response
|
|
|
|
|
.map(|reaction_event_response| reaction_event_response.event_id)
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
};
|
|
|
|
|
|
2024-10-01 21:06:54 +03:00
|
|
|
let response_type = match controller_type {
|
|
|
|
|
// When we're triggered via a reply mention, we reply to the message that triggered us.
|
|
|
|
|
ChatCompletionControllerType::ReplyMention => {
|
|
|
|
|
MessageResponseType::Reply(message_context.thread_info().last_event_id.clone())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// In all other cases, we're dealing with a threaded conversation, so we reply in the thread.
|
|
|
|
|
_ => MessageResponseType::InThread(message_context.thread_info().clone()),
|
|
|
|
|
};
|
2024-09-12 13:44:06 +03:00
|
|
|
|
|
|
|
|
let text_to_speech_eligible_payload = handle_stage_text_generation(
|
|
|
|
|
bot,
|
|
|
|
|
matrix_link.clone(),
|
|
|
|
|
message_context,
|
|
|
|
|
controller_type,
|
|
|
|
|
response_type.clone(),
|
|
|
|
|
)
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
if let Some(speech_to_text_created_event_id_reaction_event_id) =
|
|
|
|
|
speech_to_text_created_event_id_reaction_event_id
|
|
|
|
|
{
|
|
|
|
|
bot.messaging()
|
|
|
|
|
.redact_event_no_fail(
|
|
|
|
|
message_context.room(),
|
|
|
|
|
speech_to_text_created_event_id_reaction_event_id,
|
|
|
|
|
Some("Done".to_owned()),
|
|
|
|
|
)
|
|
|
|
|
.await;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If no text was generated (due to some issue), there's no point in continuing.
|
|
|
|
|
let Some(text_to_speech_eligible_payload) = text_to_speech_eligible_payload else {
|
|
|
|
|
return Ok(());
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
text_to_speech_stage_params = match message_context
|
|
|
|
|
.room_config_context()
|
|
|
|
|
.text_to_speech_bot_messages_flow_type()
|
|
|
|
|
{
|
|
|
|
|
TextToSpeechBotMessagesFlowType::Never => None,
|
|
|
|
|
TextToSpeechBotMessagesFlowType::OnDemandAlways => Some(TextToSpeechParams::Offer(
|
|
|
|
|
text_to_speech_eligible_payload,
|
|
|
|
|
response_type,
|
|
|
|
|
)),
|
|
|
|
|
TextToSpeechBotMessagesFlowType::OnDemandForVoice => {
|
|
|
|
|
if original_message_is_audio {
|
|
|
|
|
Some(TextToSpeechParams::Offer(
|
|
|
|
|
text_to_speech_eligible_payload,
|
|
|
|
|
response_type,
|
|
|
|
|
))
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
TextToSpeechBotMessagesFlowType::OnlyForVoice => {
|
|
|
|
|
if original_message_is_audio {
|
|
|
|
|
Some(TextToSpeechParams::Perform(
|
|
|
|
|
text_to_speech_eligible_payload,
|
|
|
|
|
response_type,
|
|
|
|
|
))
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
TextToSpeechBotMessagesFlowType::Always => Some(TextToSpeechParams::Perform(
|
|
|
|
|
text_to_speech_eligible_payload,
|
|
|
|
|
response_type,
|
|
|
|
|
)),
|
|
|
|
|
};
|
|
|
|
|
} else {
|
|
|
|
|
tracing::debug!("Not generating text due to auto-usage configuration");
|
|
|
|
|
|
|
|
|
|
let response_type = MessageResponseType::Reply(message_context.event_id().clone());
|
|
|
|
|
|
|
|
|
|
// If we got text from the user, perhaps it's eligible for text-to-speech.
|
|
|
|
|
|
|
|
|
|
let MessagePayload::Text(text_payload) = &message_context.payload() else {
|
|
|
|
|
// Audio message, or a notice or something else.
|
|
|
|
|
// We don't wish to proceed with potential TTS for non-text messages.
|
|
|
|
|
return Ok(());
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let text_to_speech_eligible_payload = TextToSpeechEligiblePayload {
|
|
|
|
|
text: text_payload.body.clone(),
|
|
|
|
|
event_id: message_context.event_id().clone(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
text_to_speech_stage_params = match message_context
|
|
|
|
|
.room_config_context()
|
|
|
|
|
.text_to_speech_user_messages_flow_type()
|
|
|
|
|
{
|
|
|
|
|
TextToSpeechUserMessagesFlowType::Never => None,
|
|
|
|
|
TextToSpeechUserMessagesFlowType::OnDemand => Some(TextToSpeechParams::Offer(
|
|
|
|
|
text_to_speech_eligible_payload,
|
|
|
|
|
response_type,
|
|
|
|
|
)),
|
|
|
|
|
TextToSpeechUserMessagesFlowType::Always => Some(TextToSpeechParams::Perform(
|
|
|
|
|
text_to_speech_eligible_payload,
|
|
|
|
|
response_type,
|
|
|
|
|
)),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// We're potentially dealing with some text in text_to_speech_eligible_payload - either coming directly from the user or generated by an agent.
|
|
|
|
|
|
|
|
|
|
match text_to_speech_stage_params {
|
|
|
|
|
Some(TextToSpeechParams::Perform(text_to_speech_eligible_payload, response_type)) => {
|
2024-09-14 10:39:20 +03:00
|
|
|
if _typing_notice_guard.is_none() {
|
|
|
|
|
_typing_notice_guard = Some(bot.start_typing_notice(message_context.room()).await);
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-12 13:44:06 +03:00
|
|
|
let _tts_result = generate_and_send_tts_for_message(
|
|
|
|
|
bot,
|
|
|
|
|
matrix_link.clone(),
|
|
|
|
|
message_context,
|
|
|
|
|
response_type,
|
|
|
|
|
text_to_speech_eligible_payload.event_id,
|
|
|
|
|
&text_to_speech_eligible_payload.text,
|
|
|
|
|
)
|
|
|
|
|
.await;
|
|
|
|
|
}
|
|
|
|
|
Some(TextToSpeechParams::Offer(text_to_speech_eligible_payload, response_type)) => {
|
|
|
|
|
send_tts_offer_for_message(
|
|
|
|
|
bot,
|
|
|
|
|
message_context,
|
|
|
|
|
response_type,
|
|
|
|
|
text_to_speech_eligible_payload.event_id,
|
|
|
|
|
)
|
|
|
|
|
.await;
|
|
|
|
|
}
|
|
|
|
|
None => {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn handle_stage_speech_to_text(
|
|
|
|
|
bot: &Bot,
|
|
|
|
|
message_context: &MessageContext,
|
|
|
|
|
audio_content: &AudioMessageEventContent,
|
|
|
|
|
response_type: MessageResponseType,
|
|
|
|
|
) -> Option<OwnedEventId> {
|
|
|
|
|
let agent = get_effective_agent_for_purpose_or_complain(
|
|
|
|
|
bot,
|
|
|
|
|
message_context,
|
|
|
|
|
AgentPurpose::SpeechToText,
|
|
|
|
|
response_type.clone(),
|
|
|
|
|
true,
|
|
|
|
|
)
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
tracing::debug!(
|
|
|
|
|
agent_id = agent.identifier().as_string(),
|
|
|
|
|
"Handling speech-to-text",
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let reaction_event_response = bot
|
|
|
|
|
.reacting()
|
|
|
|
|
.react_no_fail(
|
|
|
|
|
message_context.room(),
|
|
|
|
|
message_context.event_id().clone(),
|
|
|
|
|
AgentPurpose::SpeechToText.emoji().to_owned(),
|
|
|
|
|
)
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
let speech_to_text_created_event_id = handle_stage_speech_to_text_actual_transcribing(
|
|
|
|
|
bot,
|
|
|
|
|
message_context,
|
|
|
|
|
&agent,
|
|
|
|
|
audio_content,
|
|
|
|
|
response_type.clone(),
|
|
|
|
|
)
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
if let Some(reaction_event_response) = reaction_event_response {
|
|
|
|
|
let redaction_reason = if speech_to_text_created_event_id.is_ok() {
|
|
|
|
|
strings::speech_to_text::redaction_reason_done()
|
|
|
|
|
} else {
|
|
|
|
|
strings::speech_to_text::redaction_reason_failed()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
bot.messaging()
|
|
|
|
|
.redact_event_no_fail(
|
|
|
|
|
message_context.room(),
|
|
|
|
|
reaction_event_response.event_id,
|
|
|
|
|
Some(redaction_reason.to_owned()),
|
|
|
|
|
)
|
|
|
|
|
.await;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let speech_to_text_created_event_id = match speech_to_text_created_event_id {
|
|
|
|
|
Ok(event_id) => event_id,
|
|
|
|
|
Err(err) => {
|
|
|
|
|
tracing::warn!(
|
|
|
|
|
"Error in room {} while trying to transcribe via agent {}: {:?}",
|
|
|
|
|
message_context.room_id(),
|
|
|
|
|
agent.identifier(),
|
|
|
|
|
err,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
bot.messaging()
|
|
|
|
|
.send_error_markdown_no_fail(
|
|
|
|
|
message_context.room(),
|
|
|
|
|
&strings::agent::error_while_serving_purpose(
|
|
|
|
|
agent.identifier(),
|
|
|
|
|
&AgentPurpose::SpeechToText,
|
|
|
|
|
&err,
|
|
|
|
|
),
|
|
|
|
|
response_type,
|
|
|
|
|
)
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Some(speech_to_text_created_event_id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn handle_stage_text_generation(
|
|
|
|
|
bot: &Bot,
|
|
|
|
|
matrix_link: MatrixLink,
|
|
|
|
|
message_context: &MessageContext,
|
|
|
|
|
controller_type: &ChatCompletionControllerType,
|
|
|
|
|
response_type: MessageResponseType,
|
|
|
|
|
) -> Option<TextToSpeechEligiblePayload> {
|
|
|
|
|
let agent = get_effective_agent_for_purpose_or_complain(
|
|
|
|
|
bot,
|
|
|
|
|
message_context,
|
|
|
|
|
AgentPurpose::TextGeneration,
|
|
|
|
|
response_type.clone(),
|
|
|
|
|
true,
|
|
|
|
|
)
|
|
|
|
|
.await?;
|
|
|
|
|
|
2024-10-01 21:06:54 +03:00
|
|
|
// We only strip text from the first message if we're invoked via a command prefix.
|
|
|
|
|
// Otherwise, we do bot-user mentions stripping on all messages below.
|
|
|
|
|
let first_message_prefixes_to_strip = match controller_type {
|
|
|
|
|
ChatCompletionControllerType::TextCommand => vec![bot.command_prefix().to_owned()],
|
|
|
|
|
_ => vec![],
|
2024-09-12 13:44:06 +03:00
|
|
|
};
|
|
|
|
|
|
2024-10-03 16:28:49 +03:00
|
|
|
let bot_user_prefixes_to_strip = create_list_of_bot_user_prefixes_to_strip(
|
|
|
|
|
bot.user_id(),
|
|
|
|
|
message_context.bot_display_name(),
|
|
|
|
|
);
|
2024-10-01 21:06:54 +03:00
|
|
|
|
|
|
|
|
let allowed_users = match controller_type {
|
|
|
|
|
// Regular chat completion only operates on messages from allowed users.
|
|
|
|
|
ChatCompletionControllerType::TextCommand
|
|
|
|
|
| ChatCompletionControllerType::TextMention
|
|
|
|
|
| ChatCompletionControllerType::TextDirect
|
|
|
|
|
| ChatCompletionControllerType::Audio => {
|
|
|
|
|
Some(message_context.combined_admin_and_user_regexes())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// When we're triggered via an explicit mention (thread or reply), we wish to operate against the mention's whole context
|
|
|
|
|
// (the whole thread or the whole reply chain upward of the message that triggered us).
|
|
|
|
|
//
|
|
|
|
|
// This is to allow admins and users to trigger text-generation for other users' messages.
|
|
|
|
|
// When we're dragged into a conversation by a known (to us) user, we'd like to process all messages in the conversation,
|
|
|
|
|
// not just those from allowed users.
|
|
|
|
|
ChatCompletionControllerType::ThreadMention
|
|
|
|
|
| ChatCompletionControllerType::ReplyMention => None,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let params = MatrixMessageProcessingParams::new(bot.user_id().to_owned(), allowed_users)
|
|
|
|
|
.with_first_message_prefixes_to_strip(first_message_prefixes_to_strip)
|
|
|
|
|
.with_bot_user_prefixes_to_strip(bot_user_prefixes_to_strip);
|
|
|
|
|
|
|
|
|
|
let conversation = match controller_type {
|
|
|
|
|
// When we're triggered via a reply mention, the context is the whole reply chain upward of the message that triggered us.
|
|
|
|
|
ChatCompletionControllerType::ReplyMention => {
|
|
|
|
|
create_llm_conversation_for_matrix_reply_chain(
|
|
|
|
|
&bot.room_event_fetcher().clone(),
|
|
|
|
|
message_context.room(),
|
|
|
|
|
message_context.thread_info().last_event_id.clone(),
|
|
|
|
|
¶ms,
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Everything else is happening in a thread, so the context is the whole thread.
|
|
|
|
|
_ => {
|
|
|
|
|
create_llm_conversation_for_matrix_thread(
|
|
|
|
|
matrix_link.clone(),
|
|
|
|
|
message_context.room(),
|
|
|
|
|
message_context.thread_info().root_event_id.clone(),
|
|
|
|
|
¶ms,
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
}
|
|
|
|
|
};
|
2024-09-12 13:44:06 +03:00
|
|
|
|
|
|
|
|
let conversation = match conversation {
|
|
|
|
|
Ok(conversation) => conversation,
|
|
|
|
|
Err(err) => {
|
|
|
|
|
tracing::warn!(?err, "Error while trying to create conversation");
|
|
|
|
|
|
|
|
|
|
bot.messaging()
|
|
|
|
|
.send_error_markdown_no_fail(
|
|
|
|
|
message_context.room(),
|
|
|
|
|
&strings::agent::error_while_serving_purpose(
|
|
|
|
|
agent.identifier(),
|
|
|
|
|
&AgentPurpose::TextGeneration,
|
|
|
|
|
&err,
|
|
|
|
|
),
|
|
|
|
|
response_type,
|
|
|
|
|
)
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
tracing::debug!(
|
|
|
|
|
agent_id = agent.identifier().as_string(),
|
|
|
|
|
provider = format!("{}", agent.definition().provider.clone()),
|
|
|
|
|
"Invoking LLM for text generation with conversation.."
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let span = tracing::debug_span!(
|
|
|
|
|
"text_generation",
|
|
|
|
|
agent_id = agent.identifier().as_string(),
|
|
|
|
|
provider = format!("{}", agent.definition().provider.clone()),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let start_time = std::time::Instant::now();
|
|
|
|
|
|
2024-09-21 14:05:19 +00:00
|
|
|
let controller = agent.controller();
|
|
|
|
|
|
|
|
|
|
let prompt_variables = TextGenerationPromptVariables::new(
|
|
|
|
|
bot.name(),
|
|
|
|
|
&controller
|
|
|
|
|
.text_generation_model_id()
|
|
|
|
|
.unwrap_or("unknown-model".to_owned()),
|
|
|
|
|
chrono::Utc::now(),
|
2024-10-03 11:48:14 +03:00
|
|
|
conversation.start_time(),
|
2024-09-21 14:05:19 +00:00
|
|
|
);
|
|
|
|
|
|
2024-09-12 13:44:06 +03:00
|
|
|
let params = TextGenerationParams {
|
|
|
|
|
context_management_enabled: message_context
|
|
|
|
|
.room_config_context()
|
|
|
|
|
.text_generation_context_management_enabled(),
|
|
|
|
|
|
|
|
|
|
prompt_override: message_context
|
|
|
|
|
.room_config_context()
|
|
|
|
|
.text_generation_prompt_override(),
|
|
|
|
|
|
|
|
|
|
temperature_override: message_context
|
|
|
|
|
.room_config_context()
|
|
|
|
|
.text_generation_temperature_override(),
|
2024-09-21 14:05:19 +00:00
|
|
|
|
|
|
|
|
prompt_variables,
|
2024-09-12 13:44:06 +03:00
|
|
|
};
|
|
|
|
|
|
2024-09-21 14:05:19 +00:00
|
|
|
let result = controller
|
2024-09-12 13:44:06 +03:00
|
|
|
.generate_text(conversation, params)
|
|
|
|
|
.instrument(span)
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
let duration = std::time::Instant::now().duration_since(start_time);
|
|
|
|
|
|
|
|
|
|
tracing::debug!(
|
|
|
|
|
agent_id = agent.identifier().as_string(),
|
|
|
|
|
provider = format!("{}", agent.definition().provider.clone()),
|
|
|
|
|
?duration,
|
|
|
|
|
"Done with LLM text generation"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let result = match result {
|
|
|
|
|
Ok(result) => result,
|
|
|
|
|
Err(err) => {
|
|
|
|
|
tracing::warn!(
|
|
|
|
|
"Error in room {} while trying to generate text via agent {}: {:?}",
|
|
|
|
|
message_context.room_id(),
|
|
|
|
|
agent.identifier(),
|
|
|
|
|
err,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
bot.messaging()
|
|
|
|
|
.send_error_markdown_no_fail(
|
|
|
|
|
message_context.room(),
|
|
|
|
|
&strings::agent::error_while_serving_purpose(
|
|
|
|
|
agent.identifier(),
|
|
|
|
|
&AgentPurpose::TextGeneration,
|
|
|
|
|
&err,
|
|
|
|
|
),
|
|
|
|
|
response_type,
|
|
|
|
|
)
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let text = result.text.clone().trim().to_owned();
|
|
|
|
|
if text.is_empty() {
|
|
|
|
|
tracing::warn!(
|
|
|
|
|
agent_id = agent.identifier().as_string(),
|
|
|
|
|
"Agent returned empty text",
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
bot.messaging()
|
|
|
|
|
.send_error_markdown_no_fail(
|
|
|
|
|
message_context.room(),
|
|
|
|
|
&strings::agent::empty_response_returned(agent.identifier()),
|
|
|
|
|
response_type,
|
|
|
|
|
)
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let send_message_response = bot
|
|
|
|
|
.messaging()
|
|
|
|
|
.send_text_markdown_no_fail(message_context.room(), text.clone(), response_type)
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
Some(TextToSpeechEligiblePayload {
|
|
|
|
|
text,
|
|
|
|
|
event_id: send_message_response.event_id,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn handle_stage_speech_to_text_actual_transcribing(
|
|
|
|
|
bot: &Bot,
|
|
|
|
|
message_context: &MessageContext,
|
|
|
|
|
agent: &AgentInstance,
|
|
|
|
|
audio_content: &AudioMessageEventContent,
|
|
|
|
|
response_type: MessageResponseType,
|
|
|
|
|
) -> anyhow::Result<OwnedEventId> {
|
|
|
|
|
let src = &audio_content.source;
|
|
|
|
|
|
|
|
|
|
let media_request = mxlink::matrix_sdk::media::MediaRequest {
|
|
|
|
|
source: src.to_owned(),
|
|
|
|
|
format: mxlink::matrix_sdk::media::MediaFormat::File,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let media = message_context
|
|
|
|
|
.room()
|
|
|
|
|
.client()
|
|
|
|
|
.media()
|
|
|
|
|
.get_media_content(&media_request, true)
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
let span = tracing::debug_span!(
|
|
|
|
|
"speech_to_text_generation",
|
|
|
|
|
agent_id = agent.identifier().as_string()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let mime_type = audio_content
|
|
|
|
|
.info
|
|
|
|
|
.as_ref()
|
|
|
|
|
.and_then(|info| info.mimetype.clone())
|
|
|
|
|
.unwrap_or_else(|| "audio/ogg".to_string())
|
|
|
|
|
.parse::<mxlink::mime::Mime>()
|
|
|
|
|
.map_err(|err| anyhow::anyhow!("Invalid MIME type: {}", err))?;
|
|
|
|
|
|
|
|
|
|
let params = SpeechToTextParams {
|
|
|
|
|
language_override: message_context
|
|
|
|
|
.room_config_context()
|
|
|
|
|
.speech_to_text_language(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let speech_to_text_result = agent
|
|
|
|
|
.controller()
|
|
|
|
|
.speech_to_text(&mime_type, media, params)
|
|
|
|
|
.instrument(span)
|
|
|
|
|
.await?;
|
|
|
|
|
|
Do not send blockquote-formatted transcription when replying without a thread
This actually fixes 2 issues.
Fixes https://github.com/etkecc/baibot/issues/14
Fixes https://github.com/etkecc/baibot/issues/17
When people enable transcribe-only mode and the bot replies outside of a
thread, messages will no longer look like this: `> 🦻 Transcribed text`.
Instead, they will:
- look like this: `Transcribed text`
- get an emoji reaction (🦻) sent by the bot itself,
to indicate that the message is a transcription
---------------------------------------
As https://github.com/etkecc/baibot/issues/14 discusses,
the `> 🦻` prefixing of messages also served the purpose of indicating
to the bot that this is not its own message, but rather something it
"heard" from a user.
Given that out-of-thread replies no longer include this, they could be
mistaken for bot messages.
Because transcribed messages are posted as notice messages, we can
easily tell them apart from regular text-generated messages by the bot
itself, so we can (and do) treat them differently.
Thankfully, the bot does not yet support building a text-generation
conversation from arbitrary messages (something discussed in
https://github.com/etkecc/baibot/issues/15), so these out-of-thread
replies having the wrong owner are not an issue for now.
If we do land support for this, we'll probably need to make the bot inspect such notice messages
posted by it, inspect their reactons and attribute them properly (🦻 -> user message).
2024-09-30 17:18:44 +03:00
|
|
|
// Only use the `> 🦻 Transcribed text` format if we're posting in a thread.
|
|
|
|
|
//
|
|
|
|
|
// If we're dealing with a regular reply (which would be the case in "Transcribe-only mode" = speech-to-text/flow-type=only_transcribe),
|
|
|
|
|
// we don't want to use the `> 🦻 Transcribed text` format for 2 reasons:
|
|
|
|
|
//
|
|
|
|
|
// 1. This kind of blockquote-formatting can be confused by clients for a fallback-for-rich-replies
|
|
|
|
|
// (see https://spec.matrix.org/v1.11/client-server-api/#fallbacks-for-rich-replies).
|
|
|
|
|
// It makes certain clients render our messages incorrectly.
|
|
|
|
|
//
|
|
|
|
|
// 2. Transcribe-only mode is typically used for memos. Sticking to a plain-text format
|
|
|
|
|
// allows people to copy-paste the text or forward it to another room more easily (without having to strip formatting, etc.)
|
|
|
|
|
//
|
|
|
|
|
// When sending a bare reply, we'd better annotate the message with a 🦻 reaction instead,
|
|
|
|
|
// to make it clear to users that it's a transcription.
|
|
|
|
|
//
|
|
|
|
|
// Regardless of how we post this message, it will be posted as a notice,
|
|
|
|
|
// which can indicate to the bot (for potential future text-generation purposes) that this message is not a bot message.
|
2024-10-01 21:06:54 +03:00
|
|
|
let (transcribed_text, annotate_message_with_reaction) =
|
|
|
|
|
if let MessageResponseType::InThread(_) = response_type {
|
|
|
|
|
(
|
|
|
|
|
create_transcribed_message_text(&speech_to_text_result.text),
|
|
|
|
|
false,
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
(speech_to_text_result.text, true)
|
|
|
|
|
};
|
2024-09-12 13:44:06 +03:00
|
|
|
|
|
|
|
|
let result = bot
|
|
|
|
|
.messaging()
|
|
|
|
|
.send_notice_markdown_no_fail(message_context.room(), transcribed_text, response_type)
|
|
|
|
|
.await;
|
|
|
|
|
|
Do not send blockquote-formatted transcription when replying without a thread
This actually fixes 2 issues.
Fixes https://github.com/etkecc/baibot/issues/14
Fixes https://github.com/etkecc/baibot/issues/17
When people enable transcribe-only mode and the bot replies outside of a
thread, messages will no longer look like this: `> 🦻 Transcribed text`.
Instead, they will:
- look like this: `Transcribed text`
- get an emoji reaction (🦻) sent by the bot itself,
to indicate that the message is a transcription
---------------------------------------
As https://github.com/etkecc/baibot/issues/14 discusses,
the `> 🦻` prefixing of messages also served the purpose of indicating
to the bot that this is not its own message, but rather something it
"heard" from a user.
Given that out-of-thread replies no longer include this, they could be
mistaken for bot messages.
Because transcribed messages are posted as notice messages, we can
easily tell them apart from regular text-generated messages by the bot
itself, so we can (and do) treat them differently.
Thankfully, the bot does not yet support building a text-generation
conversation from arbitrary messages (something discussed in
https://github.com/etkecc/baibot/issues/15), so these out-of-thread
replies having the wrong owner are not an issue for now.
If we do land support for this, we'll probably need to make the bot inspect such notice messages
posted by it, inspect their reactons and attribute them properly (🦻 -> user message).
2024-09-30 17:18:44 +03:00
|
|
|
let event_id = result
|
2024-09-12 13:44:06 +03:00
|
|
|
.map(|result| result.event_id)
|
Do not send blockquote-formatted transcription when replying without a thread
This actually fixes 2 issues.
Fixes https://github.com/etkecc/baibot/issues/14
Fixes https://github.com/etkecc/baibot/issues/17
When people enable transcribe-only mode and the bot replies outside of a
thread, messages will no longer look like this: `> 🦻 Transcribed text`.
Instead, they will:
- look like this: `Transcribed text`
- get an emoji reaction (🦻) sent by the bot itself,
to indicate that the message is a transcription
---------------------------------------
As https://github.com/etkecc/baibot/issues/14 discusses,
the `> 🦻` prefixing of messages also served the purpose of indicating
to the bot that this is not its own message, but rather something it
"heard" from a user.
Given that out-of-thread replies no longer include this, they could be
mistaken for bot messages.
Because transcribed messages are posted as notice messages, we can
easily tell them apart from regular text-generated messages by the bot
itself, so we can (and do) treat them differently.
Thankfully, the bot does not yet support building a text-generation
conversation from arbitrary messages (something discussed in
https://github.com/etkecc/baibot/issues/15), so these out-of-thread
replies having the wrong owner are not an issue for now.
If we do land support for this, we'll probably need to make the bot inspect such notice messages
posted by it, inspect their reactons and attribute them properly (🦻 -> user message).
2024-09-30 17:18:44 +03:00
|
|
|
.ok_or_else(|| anyhow::anyhow!("Failed to send transcribed text"))?;
|
|
|
|
|
|
|
|
|
|
if annotate_message_with_reaction {
|
|
|
|
|
bot.reacting()
|
|
|
|
|
.react_no_fail(
|
|
|
|
|
message_context.room(),
|
|
|
|
|
event_id.clone(),
|
|
|
|
|
AgentPurpose::SpeechToText.emoji().to_owned(),
|
|
|
|
|
)
|
|
|
|
|
.await;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(event_id)
|
2024-09-12 13:44:06 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn send_tts_offer_for_message(
|
|
|
|
|
bot: &Bot,
|
|
|
|
|
message_context: &MessageContext,
|
|
|
|
|
response_type: MessageResponseType,
|
|
|
|
|
event_id: OwnedEventId,
|
|
|
|
|
) {
|
|
|
|
|
// Offers may be enabled, but there's no guarantee that whatever agent is configured can actually do TTS.
|
|
|
|
|
// So.. do not complain if there's no agent available. Just silently ignore it.
|
|
|
|
|
let speech_agent = get_effective_agent_for_purpose_or_complain(
|
|
|
|
|
bot,
|
|
|
|
|
message_context,
|
|
|
|
|
AgentPurpose::TextToSpeech,
|
|
|
|
|
response_type,
|
|
|
|
|
false,
|
|
|
|
|
)
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
if speech_agent.is_some() {
|
|
|
|
|
bot.reacting()
|
|
|
|
|
.react_no_fail(
|
|
|
|
|
message_context.room(),
|
|
|
|
|
event_id,
|
|
|
|
|
AgentPurpose::TextToSpeech.emoji().to_owned(),
|
|
|
|
|
)
|
|
|
|
|
.await;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn generate_and_send_tts_for_message(
|
|
|
|
|
bot: &Bot,
|
|
|
|
|
matrix_link: MatrixLink,
|
|
|
|
|
message_context: &MessageContext,
|
|
|
|
|
response_type: MessageResponseType,
|
|
|
|
|
event_id: OwnedEventId,
|
|
|
|
|
text: &str,
|
|
|
|
|
) -> bool {
|
|
|
|
|
let speech_agent = get_effective_agent_for_purpose_or_complain(
|
|
|
|
|
bot,
|
|
|
|
|
message_context,
|
|
|
|
|
AgentPurpose::TextToSpeech,
|
|
|
|
|
response_type.clone(),
|
|
|
|
|
true,
|
|
|
|
|
)
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
let Some(speech_agent) = speech_agent else {
|
|
|
|
|
return false;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
crate::controller::utils::text_to_speech::generate_and_send_tts_for_message(
|
|
|
|
|
bot,
|
|
|
|
|
matrix_link,
|
|
|
|
|
message_context,
|
|
|
|
|
response_type,
|
|
|
|
|
&speech_agent,
|
|
|
|
|
&event_id,
|
|
|
|
|
text,
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
}
|