episodic 0.2.3

Reusable Observational Memory core models and pure transforms.
Documentation
use crate::OmPendingMessage;
use crate::xml::{escape_xml_attribute, escape_xml_text};

use super::OmObserverThreadMessages;

fn normalize_role(role: &str) -> String {
    let mut chars = role.trim().chars();
    let Some(first) = chars.next() else {
        return "Unknown".to_string();
    };
    let mut normalized = first.to_uppercase().collect::<String>();
    normalized.push_str(chars.as_str());
    normalized
}

fn format_timestamp_for_observer(raw: &str) -> Option<String> {
    chrono::DateTime::parse_from_rfc3339(raw)
        .ok()
        .map(|value| value.with_timezone(&chrono::Utc))
        .map(|value| value.format("%b %-d, %Y, %-I:%M %p").to_string())
}

pub fn format_observer_messages_for_prompt(messages: &[OmPendingMessage]) -> String {
    messages
        .iter()
        .map(|message| {
            let role = normalize_role(&message.role);
            let id = message.id.trim();
            let text = message.text.trim();
            let timestamp = message
                .created_at_rfc3339
                .as_deref()
                .and_then(format_timestamp_for_observer);
            let timestamp_suffix = timestamp
                .map(|value| format!(" ({value})"))
                .unwrap_or_default();
            let id_suffix = if id.is_empty() {
                String::new()
            } else {
                format!(" [id:{id}]")
            };
            format!("**{role}{timestamp_suffix}{id_suffix}:**\n{text}")
        })
        .collect::<Vec<_>>()
        .join("\n\n---\n\n")
}

pub fn format_multi_thread_observer_messages_for_prompt(
    threads: &[OmObserverThreadMessages],
) -> String {
    threads
        .iter()
        .filter_map(|thread| {
            let thread_id = thread.thread_id.trim();
            let message_history = thread.message_history.trim();
            if thread_id.is_empty() || message_history.is_empty() {
                return None;
            }
            let thread_id = escape_xml_attribute(thread_id);
            let message_history = escape_xml_text(message_history);
            Some(format!(
                "<thread id=\"{thread_id}\">\n{message_history}\n</thread>"
            ))
        })
        .collect::<Vec<_>>()
        .join("\n\n")
}