feat: initial Sol virtual librarian implementation
Matrix bot with E2EE (matrix-sdk 0.9) that passively archives all messages to OpenSearch and responds to queries via Mistral AI with function calling tools. Core systems: - Archive: bulk OpenSearch indexer with batch/flush, edit/redaction handling, embedding pipeline passthrough - Brain: rule-based engagement evaluator (mentions, DMs, name invocations), LLM-powered spontaneous engagement, per-room conversation context windows, response delay simulation - Tools: search_archive, get_room_context, list_rooms, get_room_members registered as Mistral function calling tools with iterative tool loop - Personality: templated system prompt with Sol's librarian persona 47 unit tests covering config, evaluator, conversation windowing, personality templates, schema serialization, and search query building.
This commit is contained in:
77
src/matrix_utils.rs
Normal file
77
src/matrix_utils.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
use matrix_sdk::room::Room;
|
||||
use matrix_sdk::RoomMemberships;
|
||||
use ruma::events::room::message::{
|
||||
MessageType, OriginalSyncRoomMessageEvent, Relation, RoomMessageEventContent,
|
||||
};
|
||||
use ruma::events::relation::InReplyTo;
|
||||
use ruma::OwnedEventId;
|
||||
|
||||
/// Extract the plain-text body from a message event.
|
||||
pub fn extract_body(event: &OriginalSyncRoomMessageEvent) -> Option<String> {
|
||||
match &event.content.msgtype {
|
||||
MessageType::Text(text) => Some(text.body.clone()),
|
||||
MessageType::Notice(notice) => Some(notice.body.clone()),
|
||||
MessageType::Emote(emote) => Some(emote.body.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if this event is an edit (m.replace relation) and return the new body.
|
||||
pub fn extract_edit(event: &OriginalSyncRoomMessageEvent) -> Option<(OwnedEventId, String)> {
|
||||
if let Some(Relation::Replacement(replacement)) = &event.content.relates_to {
|
||||
let new_body = match &replacement.new_content.msgtype {
|
||||
MessageType::Text(text) => text.body.clone(),
|
||||
MessageType::Notice(notice) => notice.body.clone(),
|
||||
_ => return None,
|
||||
};
|
||||
return Some((replacement.event_id.clone(), new_body));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Extract the event ID being replied to, if any.
|
||||
pub fn extract_reply_to(event: &OriginalSyncRoomMessageEvent) -> Option<OwnedEventId> {
|
||||
if let Some(Relation::Reply { in_reply_to }) = &event.content.relates_to {
|
||||
return Some(in_reply_to.event_id.clone());
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Extract thread root event ID, if any.
|
||||
pub fn extract_thread_id(event: &OriginalSyncRoomMessageEvent) -> Option<OwnedEventId> {
|
||||
if let Some(Relation::Thread(thread)) = &event.content.relates_to {
|
||||
return Some(thread.event_id.clone());
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Build a reply message content with m.in_reply_to relation.
|
||||
pub fn make_reply_content(body: &str, reply_to_event_id: OwnedEventId) -> RoomMessageEventContent {
|
||||
let mut content = RoomMessageEventContent::text_plain(body);
|
||||
content.relates_to = Some(Relation::Reply {
|
||||
in_reply_to: InReplyTo::new(reply_to_event_id),
|
||||
});
|
||||
content
|
||||
}
|
||||
|
||||
/// Get the display name for a room.
|
||||
pub fn room_display_name(room: &Room) -> String {
|
||||
room.cached_display_name()
|
||||
.map(|n| n.to_string())
|
||||
.unwrap_or_else(|| room.room_id().to_string())
|
||||
}
|
||||
|
||||
/// Get member display names for a room.
|
||||
pub async fn room_member_names(room: &Room) -> Vec<String> {
|
||||
match room.members(RoomMemberships::JOIN).await {
|
||||
Ok(members) => members
|
||||
.iter()
|
||||
.map(|m| {
|
||||
m.display_name()
|
||||
.unwrap_or_else(|| m.user_id().as_str())
|
||||
.to_string()
|
||||
})
|
||||
.collect(),
|
||||
Err(_) => Vec::new(),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user