using System.Diagnostics; using System.Globalization; using System.Text.Json; using AIStudio.Settings.DataModel; using AIStudio.Tools.Databases.IndexStore; using AIStudio.Tools.Mail; namespace AIStudio.Tools.ToolCallingSystem.ToolCallingImplementations.Mailboxes; /// /// The arguments with which the mail tools narrow down the mails they search or count. /// /// /// Searching and counting take the same conditions, so that a model which counted the unread mails /// of a sender finds exactly those when it lists them. The schema and the readers share the names /// of the arguments, and a wrong value is refused with what would have been right, like every other /// argument, see ToolArgumentReader. /// internal static class MailToolArguments { public const string MAILBOX_IDS_ARGUMENT = "mailbox_ids"; public const string FROM_ARGUMENT = "from"; public const string TO_ARGUMENT = "to"; public const string AFTER_ARGUMENT = "after"; public const string BEFORE_ARGUMENT = "before"; public const string IS_UNREAD_ARGUMENT = "is_unread"; public const string IS_FLAGGED_ARGUMENT = "is_flagged"; public const string IS_ENCRYPTED_ARGUMENT = "is_encrypted"; public const string IMPORTANCE_ARGUMENT = "importance"; public const string HAS_ATTACHMENTS_ARGUMENT = "has_attachments"; public const string FOLDER_ARGUMENT = "folder"; /// /// How long a part of an address or a name may be. Longer than any address, shorter than a sentence. /// private const int MAX_ADDRESS_CHARACTERS = 200; /// /// How long the path of a folder may be. Servers allow deep hierarchies, but rarely this deep. /// private const int MAX_FOLDER_CHARACTERS = 500; private static readonly string[] IMPORTANCE_VALUES = ["low", "normal", "high"]; /// /// Adds the conditions to the arguments a mail tool describes. /// /// The schema of the tool. /// What the tool does with the mails, e.g., "search", completing "the mailboxes to ...". /// The ids of the mailboxes the tool offers, or none while no mailboxes are known. /// The schema, for further arguments. public static ToolParameterSchemaBuilder AddMailConditions(this ToolParameterSchemaBuilder builder, string toolAction, params string[] mailboxIds) => builder .OptionalStringArray(MAILBOX_IDS_ARGUMENT, $"Optional IDs of the mailboxes to {toolAction}, out of those listed in the description of this tool. Leave it out to {toolAction} all of them.", mailboxIds) .OptionalString(FROM_ARGUMENT, $"Optional part of the address or the name of the sender, such as 'alice@example.org', 'example.org', or 'Alice'. At most {MAX_ADDRESS_CHARACTERS} characters.") .OptionalString(TO_ARGUMENT, $"Optional part of the address or the name of a recipient in To, Cc, or Bcc. At most {MAX_ADDRESS_CHARACTERS} characters.") .OptionalString(AFTER_ARGUMENT, "Optional: only mails received at this point in time or later. A date such as 2026-09-01 stands for the beginning of that day in the time zone of the user. A date with a time of day such as 2026-09-01T14:30 is read in that time zone as well, unless it ends with an offset such as +02:00 or with Z.") .OptionalString(BEFORE_ARGUMENT, "Optional: only mails received before this point in time, in the same forms as the argument after. A date stands for the beginning of that day, so before 2026-09-30 leaves that day out.") .OptionalBoolean(IS_UNREAD_ARGUMENT, "Optional: true for unread mails only, false for read ones only.") .OptionalBoolean(IS_FLAGGED_ARGUMENT, "Optional: true for flagged mails only, false for unflagged ones only.") .OptionalBoolean(IS_ENCRYPTED_ARGUMENT, "Optional: true for encrypted mails only, false for unencrypted ones only. AI Studio cannot read the content of encrypted mails, only their header.") .OptionalEnum(IMPORTANCE_ARGUMENT, "Optional importance the sender marked the mails with. Mails without such a mark count as normal.", IMPORTANCE_VALUES) .OptionalBoolean(HAS_ATTACHMENTS_ARGUMENT, "Optional: true for mails with attachments only, false for mails without any.") .OptionalString(FOLDER_ARGUMENT, "Optional full path of the folder the mails lie in, exactly as results show it, such as 'INBOX' or 'Archive/2026'. Subfolders are not included."); /// /// The value of the importance argument which stands for the given importance. /// /// /// Results name the importance of a mail the same way, so a model can take it over as a condition. /// public static string ToArgumentValue(MailImportance importance) => importance switch { MailImportance.LOW => "low", MailImportance.HIGH => "high", _ => "normal", }; /// /// Reads which of the offered mailboxes the model asked for. /// /// The arguments the model passed. /// The mailboxes the tool offers, in the order it offers them. /// What the tool does with the mails, as for AddMailConditions. /// The mailboxes, in the order they are offered; all of them when the model named none. /// The model named a mailbox the tool does not offer, with a message for the model to correct it by. public static IReadOnlyList ReadMailboxes(JsonElement arguments, IReadOnlyList offeredMailboxes, string toolAction) { var offeredIds = offeredMailboxes.Select(mailbox => mailbox.Id).ToList(); var requestedIds = ToolArgumentReader.ReadOptionalChoices(arguments, MAILBOX_IDS_ARGUMENT, offeredIds, $"to {toolAction} all listed mailboxes"); return requestedIds is null ? offeredMailboxes : offeredMailboxes.Where(mailbox => requestedIds.Contains(mailbox.Id, StringComparer.Ordinal)).ToList(); } /// /// Reads the conditions the mails have to meet. /// /// The arguments the model passed. /// The time zone of the user, in which dates without an offset are read. /// The conditions; without any condition when the model set none. /// A condition is wrong, with a message for the model to correct it by. public static MailConditions ReadConditions(JsonElement arguments, TimeZoneInfo timeZone) { var from = ToolArgumentReader.ReadOptionalLine(arguments, FROM_ARGUMENT, MAX_ADDRESS_CHARACTERS, "for mails from any sender"); var to = ToolArgumentReader.ReadOptionalLine(arguments, TO_ARGUMENT, MAX_ADDRESS_CHARACTERS, "for mails to any recipient"); var after = ToolArgumentReader.ReadOptionalDateTime(arguments, AFTER_ARGUMENT, timeZone, "for mails of any age"); var before = ToolArgumentReader.ReadOptionalDateTime(arguments, BEFORE_ARGUMENT, timeZone, "for mails up to now"); if (after is { } since && before is { } until && since >= until) throw new ArgumentException(string.Create(CultureInfo.InvariantCulture, $"Argument '{AFTER_ARGUMENT}' must lie before argument '{BEFORE_ARGUMENT}', but no mail can arrive at {since:yyyy-MM-dd'T'HH:mmzzz} or later and before {until:yyyy-MM-dd'T'HH:mmzzz}. Swap the two, or leave one of them out.")); var importance = ToolArgumentReader.ReadOptionalChoice(arguments, IMPORTANCE_ARGUMENT, IMPORTANCE_VALUES, "for mails of any importance") switch { null => (MailImportance?)null, "low" => MailImportance.LOW, "normal" => MailImportance.NORMAL, "high" => MailImportance.HIGH, var other => throw new UnreachableException($"The importance '{other}' was offered, but has no meaning."), }; var filter = new MailFilter { From = from, To = to, ReceivedSinceUtc = after?.ToUniversalTime(), ReceivedBeforeUtc = before?.ToUniversalTime(), IsUnread = ToolArgumentReader.ReadOptionalBoolean(arguments, IS_UNREAD_ARGUMENT, "for read and unread mails alike"), IsFlagged = ToolArgumentReader.ReadOptionalBoolean(arguments, IS_FLAGGED_ARGUMENT, "for flagged and unflagged mails alike"), IsEncrypted = ToolArgumentReader.ReadOptionalBoolean(arguments, IS_ENCRYPTED_ARGUMENT, "for encrypted and unencrypted mails alike"), Importance = importance, HasAttachments = ToolArgumentReader.ReadOptionalBoolean(arguments, HAS_ATTACHMENTS_ARGUMENT, "for mails with and without attachments alike"), }; return new(filter, ToolArgumentReader.ReadOptionalLine(arguments, FOLDER_ARGUMENT, MAX_FOLDER_CHARACTERS, "for mails in any folder")); } }