diff --git a/src/Orbit.Infrastructure/Services/Prompts/NotificationVoice.cs b/src/Orbit.Infrastructure/Services/Prompts/NotificationVoice.cs index a4dacaaa4..552103f1e 100644 --- a/src/Orbit.Infrastructure/Services/Prompts/NotificationVoice.cs +++ b/src/Orbit.Infrastructure/Services/Prompts/NotificationVoice.cs @@ -2,12 +2,17 @@ namespace Orbit.Infrastructure.Services.Prompts; public static class NotificationVoice { - public const string Rules = """ - Voice, and every line here is absolute: - - Write calmly. Describe what is true. Never sell, never hype, never perform enthusiasm. + public const string CharacterRules = """ + Character bans are absolute in every language: - Use no exclamation mark anywhere, in the title or in the body. - Use no dash character: no em dash, no en dash, and no doubled hyphen. A comma or a full stop does that job. - - Use no emoji, no markdown, no bullet, no heading, no greeting, no sign-off, and no quotation marks around the message. + - Use no emoji in the reply's own words. + """; + + public const string Rules = CharacterRules + "\n" + """ + Voice, and every line here is absolute: + - Write calmly. Describe what is true. Never sell, never hype, never perform enthusiasm. + - Use no markdown, no bullet, no heading, no greeting, no sign-off, and no quotation marks around the message. - Never shame, blame, scold or warn. A missed day is ordinary and is never a failure. - Address the reader directly as you. Never call them the user. - Use plain verbs. Never use these words or their translations: unlock, elevate, empower, supercharge, leverage, harness, delve, seamless, robust, crucial, essential, vital, revolutionary, maximize, optimize. diff --git a/src/Orbit.Infrastructure/Services/Prompts/Sections/Dynamic/ImageInstructionsSection.cs b/src/Orbit.Infrastructure/Services/Prompts/Sections/Dynamic/ImageInstructionsSection.cs index fe4985273..e61d4d438 100644 --- a/src/Orbit.Infrastructure/Services/Prompts/Sections/Dynamic/ImageInstructionsSection.cs +++ b/src/Orbit.Infrastructure/Services/Prompts/Sections/Dynamic/ImageInstructionsSection.cs @@ -25,7 +25,7 @@ When the user uploads an image (photo of schedule, to-do list, calendar, task ap - Top-level groups/categories -> separate CreateHabit actions (parents) - Nested/indented items under a group -> subHabits array on that parent - Deeply nested items (sub-sub-items) -> flatten into the nearest parent's subHabits - 3. Preserve exact titles/names as shown in the image -- do not rename, summarize, or merge items + 3. Preserve exact titles/names as shown in the image. Do not rename, summarize, or merge items 4. If an item appears multiple times (e.g., "Water - 710ml" x4), create each one individually 5. Items with no children that are not nested under anything -> standalone CreateHabit (no subHabits) 6. Infer frequency from visual cues: @@ -45,7 +45,7 @@ 4. If an item appears multiple times (e.g., "Water - 710ml" x4), create each one ### Example: Task app screenshot with groups (user says "create these habits") { - "aiMessage": "Created 5 habit groups with all their sub-habits from your screenshot!", + "aiMessage": "Created 5 habit groups with all their sub-habits from your screenshot.", "actions": [ { "type": "CreateHabit", diff --git a/src/Orbit.Infrastructure/Services/Prompts/Sections/Static/ClarificationGuidanceSection.cs b/src/Orbit.Infrastructure/Services/Prompts/Sections/Static/ClarificationGuidanceSection.cs index 88b8fbf5c..76557f5a1 100644 --- a/src/Orbit.Infrastructure/Services/Prompts/Sections/Static/ClarificationGuidanceSection.cs +++ b/src/Orbit.Infrastructure/Services/Prompts/Sections/Static/ClarificationGuidanceSection.cs @@ -13,20 +13,20 @@ public string Build(PromptContext context) sb.AppendLine(""" ## Clarification Cards (NeedsClarification) - Some tools may return a structured "I need to ask the user a question" result instead of executing. This shows up as a `NeedsClarification` action containing a question + a small set of quick-action buttons (Daily, Weekly, X times per week, One-time task, etc.). The user taps one button and the tool re-runs with the chosen value merged into the original arguments — no follow-up tool call from you. + Some tools may return a structured "I need to ask the user a question" result instead of executing. This shows up as a `NeedsClarification` action containing a question + a small set of quick-action buttons (Daily, Weekly, X times per week, One-time task, etc.). The user taps one button and the tool re-runs with the chosen value merged into the original arguments, with no follow-up tool call from you. ### When this can happen - `create_habit` returns `NeedsClarification` if you call it with no `frequency_unit` AND the title contains "habit" / "rotina" / "hábito". Translation: you guessed it's a one-time task on a recurrence-sounding title. The tool refuses to silently create a one-time task in that case. ### How to behave when a tool returns NeedsClarification - - DO NOT add a plain-text question of your own on top — the card already asks. Replying with extra text duplicates the prompt and confuses the user. - - DO NOT immediately call another tool to "help out" — the user is now interacting with the card, not your text. - - A short acknowledgement like "Sure — what schedule would you like?" is OK but unnecessary; one short line max, or stay silent. + - DO NOT add a plain-text question of your own on top. The card already asks. Replying with extra text duplicates the prompt and confuses the user. + - DO NOT immediately call another tool to "help out". The user is now interacting with the card, not your text. + - A short acknowledgement like "Sure, what schedule would you like?" is OK but unnecessary; one short line max, or stay silent. - The user's button tap triggers the tool to re-run server-side. You will receive the resulting success/failure on the next turn just like any normal tool call. ### How to avoid the clarification in the first place - Prefer to ask the schedule INLINE in your text BEFORE calling `create_habit` when the user describes something as a "habit"/"rotina"/"hábito" without a schedule (see Structuring Strategy). The clarification card is the safety net; the ideal flow is for you to ask first. - - When the user clearly described a one-time task ("just once", "this Friday only", "uma vez"), call `create_habit` with no `frequency_unit` — the tool detects the explicit one-time language and does not request clarification. + - When the user clearly described a one-time task ("just once", "this Friday only", "uma vez"), call `create_habit` with no `frequency_unit`. The tool detects the explicit one-time language and does not request clarification. """); return sb.ToString(); diff --git a/src/Orbit.Infrastructure/Services/Prompts/Sections/Static/EncouragingToneSection.cs b/src/Orbit.Infrastructure/Services/Prompts/Sections/Static/EncouragingToneSection.cs index 5d6ef76e3..36f8a7628 100644 --- a/src/Orbit.Infrastructure/Services/Prompts/Sections/Static/EncouragingToneSection.cs +++ b/src/Orbit.Infrastructure/Services/Prompts/Sections/Static/EncouragingToneSection.cs @@ -13,8 +13,10 @@ public string Build(PromptContext context) sb.AppendLine(""" ## Tone and Encouragement - Keep your voice warm, supportive, and concise. Celebrate progress and streaks with genuine, specific recognition, and give a finished week or a recovered streak a quick word of acknowledgement. Stay non-judgmental about missed days, treat a slip as a normal part of building habits, and point the way back without guilt or pressure. Be encouraging without being saccharine, skip the empty hype and exclamation spam, and just be a steady presence that helps the user keep moving. + Keep your voice warm, supportive, and concise. Write calmly and give progress and streaks a brief, specific acknowledgement. Stay non-judgmental about missed days, treat a slip as a normal part of building habits, and point the way back without guilt or pressure. Be encouraging without being saccharine, avoid empty hype, and be a steady presence that helps the user keep moving. """); + sb.AppendLine(NotificationVoice.CharacterRules); + sb.AppendLine("These bans apply to your prose. You may still set habit emoji fields through tools, and use markdown and bullet points in chat replies."); return sb.ToString(); } } diff --git a/tests/Orbit.Infrastructure.Tests/Services/NotificationVoiceTests.cs b/tests/Orbit.Infrastructure.Tests/Services/NotificationVoiceTests.cs index 90045a4ac..917ec0118 100644 --- a/tests/Orbit.Infrastructure.Tests/Services/NotificationVoiceTests.cs +++ b/tests/Orbit.Infrastructure.Tests/Services/NotificationVoiceTests.cs @@ -37,6 +37,7 @@ public class NotificationVoiceTests { "daily summary, system", AiSummaryService.SystemPrompt }, { "daily summary, en", DailySummaryPrompt("en") }, { "daily summary, pt-BR", DailySummaryPrompt("pt-BR") }, + { "chat, system with image", ChatSystemPrompt() }, }; public static TheoryData EveryGeneratedUserPrompt() => new() @@ -68,6 +69,35 @@ private static string DailySummaryPrompt(string language) new DailySummaryContext(today, today, today, language, null, 0, 0, new Dictionary())); } + private static string ChatSystemPrompt() + { + var builder = new SystemPromptBuilder(); + var request = new PromptBuildRequest( + [], [], HasImage: true, UserToday: new DateOnly(2026, 9, 18)); + + return builder.BuildStatic(request) + builder.BuildDynamic(request); + } + + [Theory] + [InlineData("no exclamation mark")] + [InlineData("no emoji")] + [InlineData("no em dash")] + [InlineData("no en dash")] + [InlineData("no doubled hyphen")] + public void ChatPromptBansCharactersInEveryLanguageAndPreservesFormattingAndHabitEmojiTools(string ban) + { + var prompt = ChatSystemPrompt(); + + prompt.Should().Contain(ban); + prompt.Should().Contain("absolute in every language"); + prompt.Should().Contain("reply's own words"); + prompt.Should().Contain("habit emoji fields through tools"); + prompt.Should().Contain("Use bullet points with newlines"); + prompt.Should().Contain("bulk_update_habit_emojis"); + prompt.Should().NotContain("no markdown"); + prompt.Should().NotContain("no bullet"); + } + [Theory] [MemberData(nameof(EveryPromptSentToAModel))] public void NoPromptDemonstratesACharacterItBans(string label, string prompt) diff --git a/tests/Orbit.Infrastructure.Tests/Services/PromptSectionTests.cs b/tests/Orbit.Infrastructure.Tests/Services/PromptSectionTests.cs index d1255b1b9..487be6b3b 100644 --- a/tests/Orbit.Infrastructure.Tests/Services/PromptSectionTests.cs +++ b/tests/Orbit.Infrastructure.Tests/Services/PromptSectionTests.cs @@ -90,6 +90,18 @@ public void Build_ContainsWarmEncouragingTone() result.Should().Contain("saccharine"); } + [Fact] + public void Build_KeepsAcknowledgementCalmAndSpecific() + { + var ctx = new PromptContext(new List(), new List(), false, null, null, null, null); + var result = new EncouragingToneSection().Build(ctx); + + result.Should().Contain("calmly"); + result.Should().Contain("specific acknowledgement"); + result.Should().NotContain("Celebrate"); + result.Should().NotContain("exclamation spam"); + } + [Fact] public void Build_ContainsNoEmOrEnDashes() {