Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions src/Orbit.Infrastructure/Services/Prompts/NotificationVoice.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> EveryGeneratedUserPrompt() => new()
Expand Down Expand Up @@ -68,6 +69,35 @@ private static string DailySummaryPrompt(string language)
new DailySummaryContext(today, today, today, language, null, 0, 0, new Dictionary<Guid, DateOnly>()));
}

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)
Expand Down
12 changes: 12 additions & 0 deletions tests/Orbit.Infrastructure.Tests/Services/PromptSectionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,18 @@ public void Build_ContainsWarmEncouragingTone()
result.Should().Contain("saccharine");
}

[Fact]
public void Build_KeepsAcknowledgementCalmAndSpecific()
{
var ctx = new PromptContext(new List<Habit>(), new List<UserFact>(), 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()
{
Expand Down
Loading