Browse Source

feat: add today's date to AI system turn context and format for display

softword 3 days ago
parent
commit
38b73843b3
2 changed files with 61 additions and 7 deletions
  1. 5 2
      src/app.rs
  2. 56 5
      src/autoreply.rs

+ 5 - 2
src/app.rs

@@ -10,7 +10,7 @@ use tokio::sync::mpsc::UnboundedSender;
 use crate::ai::{AiClient, ChatTurn, ImageInput, Reply};
 use crate::autoreply::{
     CONTEXT_MESSAGES, ReplyGate, TurnContext, build_turns, context_start, is_group_chat,
-    supports_autoreply,
+    supports_autoreply, today_label,
 };
 use crate::config::Config;
 use crate::history::{ChatSummary, Contact, History, StoredMessage};
@@ -1051,7 +1051,8 @@ impl App {
     /// Side effects: reads the chat name and the stored image descriptions (images are given to
     /// the model only as their descriptions); records the last context message as answered (see
     /// `finish_reply`); spawns a task that sends `AiReply`. Mentions of the account owner are
-    /// marked (see `ReplyGate::mark_own_mentions`) before the turns are built.
+    /// marked (see `ReplyGate::mark_own_mentions`) before the turns are built. Today's local
+    /// date (see `today_label`) is passed to the system turn.
     fn start_reply(&mut self, ai: AiClient, prompt: &str, chat_jid: &str, context: &[StoredMessage]) -> Result<()> {
         if let Some(last) = context.last() {
             self.answered_up_to.insert(chat_jid.to_owned(), last.message_id.clone());
@@ -1062,11 +1063,13 @@ impl App {
             .iter()
             .map(|message| StoredMessage { text: self.gate.mark_own_mentions(&message.text), ..message.clone() })
             .collect();
+        let today = today_label(now_unix());
         let chat = TurnContext {
             self_chat: self.gate.is_self_chat(chat_jid),
             group: is_group_chat(chat_jid),
             chat_name: &chat_name,
             image_descriptions: Some(&descriptions),
+            today: &today,
         };
         let turns = build_turns(prompt, &context, chat);
         spawn_ai_request(ai, self.events.clone(), chat_jid.to_owned(), turns);

+ 56 - 5
src/autoreply.rs

@@ -46,6 +46,12 @@ const CHAT_LINE_GROUP: &str = "Chat di gruppo";
 /// Start of the chat description line of a private chat (followed by the contact name).
 const CHAT_LINE_PRIVATE: &str = "Chat privata con";
 
+/// Start of the system turn line giving today's date (followed by the formatted date).
+const DATE_LINE_PREFIX: &str = "Oggi è";
+
+/// `chrono` format of the local date given to the AI (day/month/year).
+const DATE_FORMAT: &str = "%d/%m/%Y";
+
 /// Text replacing a mention of the account owner in message texts.
 const OWN_MENTION: &str = "@[te]";
 
@@ -329,6 +335,8 @@ pub struct TurnContext<'a> {
     pub group: bool,
     /// Display name of the chat: group name or contact name.
     pub chat_name: &'a str,
+    /// Today's local date, already formatted by `today_label`; empty means no date line.
+    pub today: &'a str,
     /// Stored image descriptions keyed by message id.
     pub image_descriptions: Option<&'a HashMap<String, String>>,
 }
@@ -422,15 +430,21 @@ fn message_to_turn(message: &StoredMessage, context: TurnContext) -> ChatTurn {
 /// Inputs: `system_prompt`, the configured prompt; `context`, the chat description.
 /// Output: in the self-chat, the prompt followed by `ROLE_NOTE_SELF_CHAT`; elsewhere the
 /// prompt, a line naming the chat (`Chat di gruppo «name».` or `Chat privata con «name».`)
-/// and `ROLE_NOTE_CHAT`, separated by `ROLE_NOTE_SEPARATOR`.
+/// and `ROLE_NOTE_CHAT`, separated by `ROLE_NOTE_SEPARATOR`. When `context.today` is not
+/// empty, a date line (`Oggi è <date>.`) follows the prompt in both cases.
 /// Side effects: none.
 fn system_turn_content(system_prompt: &str, context: TurnContext) -> String {
+    let prompt = if context.today.is_empty() {
+        system_prompt.to_string()
+    } else {
+        format!("{system_prompt}{ROLE_NOTE_SEPARATOR}{DATE_LINE_PREFIX} {}.", context.today)
+    };
     if context.self_chat {
-        return format!("{system_prompt}{ROLE_NOTE_SEPARATOR}{ROLE_NOTE_SELF_CHAT}");
+        return format!("{prompt}{ROLE_NOTE_SEPARATOR}{ROLE_NOTE_SELF_CHAT}");
     }
     let chat_kind = if context.group { CHAT_LINE_GROUP } else { CHAT_LINE_PRIVATE };
     format!(
-        "{system_prompt}{ROLE_NOTE_SEPARATOR}{chat_kind} «{}».{ROLE_NOTE_SEPARATOR}{ROLE_NOTE_CHAT}",
+        "{prompt}{ROLE_NOTE_SEPARATOR}{chat_kind} «{}».{ROLE_NOTE_SEPARATOR}{ROLE_NOTE_CHAT}",
         context.chat_name
     )
 }
@@ -479,13 +493,23 @@ fn local_yesterday_midnight(now: i64) -> Option<i64> {
     Some(start.timestamp())
 }
 
+/// Formats the local date of `now` for the AI system turn.
+///
+/// Inputs: `now`, the current time in unix seconds.
+/// Output: the local date formatted with `DATE_FORMAT`; empty when the local
+/// time cannot be resolved (invalid timestamp).
+/// Side effects: none.
+pub fn today_label(now: i64) -> String {
+    Local.timestamp_opt(now, 0).single().map(|time| time.format(DATE_FORMAT).to_string()).unwrap_or_default()
+}
+
 /// Builds the ordered list of turns sent to the AI.
 ///
 /// Inputs: `system_prompt`, the instructions for the assistant; `messages`,
 /// the chat history in ascending chronological order; `context`, the chat
 /// description: self-chat (where `from_me` is true for both the human and the
-/// AI, so `is_ai` decides the role), group or private, chat name and the id of
-/// the image attached to the request.
+/// AI, so `is_ai` decides the role), group or private, chat name, today's date
+/// line (see `system_turn_content`) and the stored image descriptions.
 /// Output: a `System` turn first (see `system_turn_content`), then the last
 /// `CONTEXT_MESSAGES` messages mapped to `Assistant` or `User` (see `turn_role`), skipping messages with
 /// blank text. User voice notes and images become factual descriptions (see
@@ -522,6 +546,7 @@ mod tests {
             self_chat: true,
             group: false,
             chat_name: "",
+            today: "",
             image_descriptions: None,
         };
     const VOICE_TEXT: &str = "[messaggio vocale]";
@@ -888,6 +913,32 @@ mod tests {
         );
     }
 
+    #[test]
+    fn build_turns_system_turn_includes_today_when_set() {
+        let private = TurnContext { chat_name: "Anna", today: "25/10/2026", ..TurnContext::default() };
+        let self_chat = TurnContext { today: "25/10/2026", ..SELF_CHAT };
+        assert_eq!(
+            build_turns(SYSTEM_PROMPT, &[], private)[0].content,
+            format!("{SYSTEM_PROMPT}{ROLE_NOTE_SEPARATOR}{DATE_LINE_PREFIX} 25/10/2026.{ROLE_NOTE_SEPARATOR}{CHAT_LINE_PRIVATE} «Anna».{ROLE_NOTE_SEPARATOR}{ROLE_NOTE_CHAT}")
+        );
+        assert_eq!(
+            build_turns(SYSTEM_PROMPT, &[], self_chat)[0].content,
+            format!("{SYSTEM_PROMPT}{ROLE_NOTE_SEPARATOR}{DATE_LINE_PREFIX} 25/10/2026.{ROLE_NOTE_SEPARATOR}{ROLE_NOTE_SELF_CHAT}")
+        );
+    }
+
+    #[test]
+    fn today_label_formats_local_date() {
+        // 25/10/2025 12:00 UTC: the same calendar date in every zone from UTC-11 to UTC+11.
+        const NOON_UTC_2025_10_25: i64 = 1_761_393_600;
+        assert_eq!(today_label(NOON_UTC_2025_10_25), "25/10/2025");
+    }
+
+    #[test]
+    fn today_label_is_empty_for_unresolvable_timestamp() {
+        assert_eq!(today_label(i64::MAX), "");
+    }
+
     #[test]
     fn build_turns_with_no_messages_has_only_system() {
         let turns = build_turns(SYSTEM_PROMPT, &[], TurnContext::default());