|
|
@@ -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());
|