Skip to content

Commit 0f55410

Browse files
committed
feat: Improve Prompts
1 parent 5fad817 commit 0f55410

File tree

7 files changed

+217
-93
lines changed

7 files changed

+217
-93
lines changed

apple/Clarissa/Sources/Agent/Agent.swift

Lines changed: 51 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -171,46 +171,33 @@ public final class Agent: ObservableObject {
171171
/// - Explicit negative rules to avoid unnecessary tool use
172172
/// Community insights: "Instructions in English work best", "Use CAPS for critical rules"
173173
private func buildSystemPrompt() async -> String {
174-
// Keep prompt concise (~600 chars = ~150 tokens) to maximize context for conversation
174+
// Format current date/time for relative date understanding
175+
let dateFormatter = DateFormatter()
176+
dateFormatter.dateFormat = "EEEE, MMMM d, yyyy 'at' h:mm a"
177+
let currentTime = dateFormatter.string(from: Date())
178+
179+
// Keep prompt concise to maximize context for conversation
180+
// Optimized based on Foundation Models community insights
175181
var prompt = """
176-
You are Clarissa, an iOS assistant.
177-
178-
ALWAYS USE TOOLS FOR:
179-
- Weather/temperature/forecast/rain/hot/cold -> weather tool
180-
- Math/calculate/percent/tip/convert -> calculator tool
181-
- Schedule/meeting/event/calendar/what's on -> calendar tool
182-
- Remind me/task/to-do/don't forget/list reminders -> reminders tool
183-
- Phone number/email/contact/call/text -> contacts tool
184-
- Where am I/my location/current location -> location tool
185-
- Remember that/remember I/my preference/I like -> remember tool
186-
- URL/webpage/fetch/read page/get content -> web_fetch tool
187-
- Image file URL (file://) needs analysis -> image_analysis tool
188-
- PDF file URL (file://) needs reading -> image_analysis tool (pdf_extract_text or pdf_ocr)
189-
190-
ANSWER DIRECTLY (no tools):
191-
- Message contains "[Image Analysis]" -> USE the provided OCR text and classifications to respond
192-
- Date/time/day -> answer from your knowledge
193-
- General knowledge -> answer directly
194-
- Opinions/advice -> respond conversationally
195-
- Greetings/chat -> respond naturally
182+
You are Clarissa, a personal assistant. Be warm but concise.
183+
Current time: \(currentTime)
184+
185+
TOOLS: weather(location?), calculator(expression), calendar(action,title?,date?), reminders(action,title?), contacts(query), location(), remember(content), web_fetch(url), image_analysis(file_url)
186+
187+
USE TOOLS for: weather, math, calendar, reminders, contacts, location, saving facts, fetching URLs, analyzing images/PDFs
188+
ANSWER DIRECTLY for: "[Image Analysis]" in message (use provided OCR/classifications), date/time questions, general knowledge, opinions, greetings
196189
197190
EXAMPLES:
198-
"Weather?" -> weather (no params = current location)
199191
"Weather in Paris" -> weather(location="Paris")
200-
"What's 20% of 85?" -> calculator(expression="85 * 0.20")
201-
"Meeting tomorrow 2pm" -> calendar(action=create, title, startDate)
202-
"What's on my calendar?" -> calendar(action=list)
203-
"Remind me to call Bob" -> reminders(action=create, title="Call Bob")
204-
"Show my reminders" -> reminders(action=list)
205-
"What's John's phone number?" -> contacts(action=search, query="John")
206-
"Fetch example.com" -> web_fetch(url="https://example.com")
207-
User: "Analyze this image [Image Analysis] Text: Hello World Contains: sign" -> "This image shows a sign with the text 'Hello World'."
208-
209-
RESPONSE RULES:
210-
- Be brief (1-2 sentences)
192+
"What's 20% of 85?" -> calculator(expression="85*0.20")
193+
"Meeting tomorrow 2pm" -> calendar(action=create,title,startDate)
194+
195+
RULES:
196+
- Brief responses (1-2 sentences)
211197
- State result, not process
198+
- If request is ambiguous, ask one clarifying question before using tools
212199
- If tool fails, explain and suggest alternative
213-
- If user asks about saved facts (name, preferences), answer from Saved Facts section
200+
- Use saved facts when user asks about their name/preferences
214201
"""
215202

216203
// Add disabled tools section so AI can inform user about features that can be enabled
@@ -380,7 +367,7 @@ public final class Agent: ObservableObject {
380367
// The LLM session has already executed tools and incorporated results
381368
if nativeToolHandling {
382369
ClarissaLogger.agent.info("Agent run completed (native tool handling)")
383-
let finalContent = Self.applyRefusalFallback(fullContent)
370+
let finalContent = Self.applyRefusalFallback(fullContent, userMessage: userMessage)
384371
callbacks?.onResponse(content: finalContent)
385372
return finalContent
386373
}
@@ -413,7 +400,7 @@ public final class Agent: ObservableObject {
413400

414401
// No tool calls - final response
415402
ClarissaLogger.agent.info("Agent run completed with response")
416-
let finalContent = Self.applyRefusalFallback(fullContent)
403+
let finalContent = Self.applyRefusalFallback(fullContent, userMessage: userMessage)
417404
callbacks?.onResponse(content: finalContent)
418405
return finalContent
419406
}
@@ -442,19 +429,39 @@ public final class Agent: ObservableObject {
442429
"i'm sorry, but i can't"
443430
]
444431

445-
/// Friendly redirect message when model refuses
446-
private static let refusalFallback = """
447-
I'm best at helping with tasks like checking your calendar, setting reminders, getting weather updates, and doing calculations. What can I help you with?
448-
"""
432+
/// Context-aware suggestions based on what the user was trying to do
433+
private static func getRefusalSuggestion(for userMessage: String) -> String {
434+
let lowercased = userMessage.lowercased()
435+
436+
// Detect intent and suggest relevant alternatives
437+
if lowercased.contains("weather") || lowercased.contains("temperature") || lowercased.contains("forecast") {
438+
return "I can check the weather for you. Try asking \"What's the weather in [city]?\" or just \"Weather?\""
439+
}
440+
if lowercased.contains("remind") || lowercased.contains("reminder") || lowercased.contains("task") {
441+
return "I can help with reminders. Try \"Remind me to [task]\" or \"Show my reminders\"."
442+
}
443+
if lowercased.contains("calendar") || lowercased.contains("meeting") || lowercased.contains("schedule") || lowercased.contains("event") {
444+
return "I can help with your calendar. Try \"What's on my calendar?\" or \"Schedule a meeting\"."
445+
}
446+
if lowercased.contains("calculate") || lowercased.contains("math") || lowercased.contains("%") || lowercased.contains("tip") {
447+
return "I can do calculations. Try \"What's 20% of 85?\" or \"Calculate 15 + 27\"."
448+
}
449+
if lowercased.contains("contact") || lowercased.contains("phone") || lowercased.contains("email") || lowercased.contains("call") {
450+
return "I can look up contacts. Try \"What's [name]'s phone number?\" or \"Find [name]'s email\"."
451+
}
452+
453+
// Default fallback
454+
return "I'm best at helping with your calendar, reminders, weather, calculations, and contacts. What can I help you with?"
455+
}
449456

450-
/// Check if a response is a refusal and provide a helpful redirect if so
451-
private static func applyRefusalFallback(_ content: String) -> String {
457+
/// Check if a response is a refusal and provide a context-aware redirect if so
458+
private static func applyRefusalFallback(_ content: String, userMessage: String) -> String {
452459
let lowercased = content.lowercased()
453460

454461
for phrase in refusalPhrases {
455462
if lowercased.contains(phrase) {
456-
ClarissaLogger.agent.info("Detected refusal response, applying fallback")
457-
return refusalFallback
463+
ClarissaLogger.agent.info("Detected refusal response, applying context-aware fallback")
464+
return getRefusalSuggestion(for: userMessage)
458465
}
459466
}
460467

apple/Clarissa/Sources/LLM/PromptEnhancer.swift

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,14 @@
11
import Foundation
22

33
/// System prompt for the prompt enhancement feature.
4-
/// Optimized for Apple's on-device model - focuses on clarity for tool execution.
5-
/// Keeps prompts short and direct rather than making them verbose.
4+
/// Optimized for Apple's on-device model - uses examples only, no redundant rules.
5+
/// The instruction and examples are sufficient for the model to understand the task.
66
private let enhancementSystemPrompt = """
7-
Rewrite the user's request to be clearer. Output ONLY the rewritten text.
7+
Rewrite to be clearer. Output ONLY the rewritten text.
88
9-
Example:
10-
User: "weather"
11-
Output: "What's the weather right now?"
12-
13-
Example:
14-
User: "remind me about the thing tomorrow"
15-
Output: "Set a reminder for tomorrow"
16-
17-
Example:
18-
User: "whats 15% of 50 dolars"
19-
Output: "What's 15% of $50?"
20-
21-
Rules:
22-
- Keep it short and direct
23-
- Fix typos and grammar
24-
- Add missing context (like "current" for weather)
25-
- Output ONLY the improved text
9+
"weather" -> "What's the weather right now?"
10+
"remind me about the thing tomorrow" -> "Set a reminder for tomorrow"
11+
"whats 15% of 50 dolars" -> "What's 15% of $50?"
2612
"""
2713

2814
/// Keywords that indicate the prompt is already clear enough for tool use

apple/Clarissa/Sources/Persistence/MemoryManager.swift

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ public actor MemoryManager {
208208

209209
/// Get memories formatted for the system prompt
210210
/// Includes topic tags when available for better context
211+
/// Optimized for token efficiency while maintaining clarity
211212
func getForPrompt() async -> String? {
212213
await ensureLoaded()
213214

@@ -221,21 +222,17 @@ public actor MemoryManager {
221222
let memoryList = recentMemories.map { memory -> String in
222223
if let topics = memory.topics, !topics.isEmpty {
223224
let topicStr = topics.joined(separator: ", ")
224-
return "- \(memory.content) [topics: \(topicStr)]"
225+
return "- \(memory.content) [\(topicStr)]"
225226
}
226227
return "- \(memory.content)"
227228
}.joined(separator: "\n")
228229

229230
logger.info("getForPrompt: Including \(recentMemories.count) memories in system prompt")
230231

232+
// Concise format - the system prompt already instructs to use saved facts
231233
return """
232-
## Saved Facts About This User
233-
234+
USER FACTS:
234235
\(memoryList)
235-
236-
IMPORTANT: When the user asks about their name, preferences, or anything in the saved facts above, respond using this information directly. For example:
237-
- "Say my name" or "What's my name?" -> Answer with their name from saved facts
238-
- "What do you know about me?" -> List the saved facts
239236
"""
240237
}
241238

apple/Clarissa/Sources/Tools/CalculatorTool.swift

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -316,11 +316,16 @@ final class CalculatorTool: ClarissaTool, @unchecked Sendable {
316316
if computedValue.isNaN {
317317
throw ToolError.executionFailed("Expression '\(name)(\(argString))' resulted in an undefined value (NaN).")
318318
}
319+
// Allow infinity as a valid result - expressions like log(0) should return -Infinity
320+
// Format infinity as a string representation to avoid NSExpression issues
321+
let replacement: String
319322
if computedValue.isInfinite {
320-
throw ToolError.executionFailed("Expression '\(name)(\(argString))' resulted in a value too large to represent.")
323+
replacement = computedValue > 0 ? "Infinity" : "-Infinity"
324+
} else {
325+
replacement = "\(computedValue)"
321326
}
322-
323-
result.replaceSubrange(fullRange, with: "\(computedValue)")
327+
328+
result.replaceSubrange(fullRange, with: replacement)
324329
}
325330

326331
return result

apple/Clarissa/Sources/UI/SettingsView.swift

Lines changed: 66 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,10 @@ public struct SettingsView: View {
245245
}
246246
}
247247

248+
if !hasHighQualityVoices {
249+
VoiceDownloadPromptView()
250+
}
251+
248252
VStack(alignment: .leading, spacing: 8) {
249253
HStack {
250254
Text("Speech Rate")
@@ -275,12 +279,7 @@ public struct SettingsView: View {
275279
} header: {
276280
Text("Voice Output")
277281
} footer: {
278-
VStack(alignment: .leading, spacing: 8) {
279-
Text("When enabled, Clarissa will speak responses aloud in voice mode.")
280-
if voiceOutputEnabled {
281-
Text("Premium and Enhanced voices provide the best quality. Download more in System Settings -> Accessibility -> Spoken Content -> System Voices.")
282-
}
283-
}
282+
Text("When enabled, Clarissa will speak responses aloud in voice mode.")
284283
}
285284
}
286285
.formStyle(.grouped)
@@ -495,6 +494,10 @@ public struct SettingsView: View {
495494
}
496495
}
497496

497+
if !hasHighQualityVoices {
498+
VoiceDownloadPromptView()
499+
}
500+
498501
VStack(alignment: .leading, spacing: 8) {
499502
HStack {
500503
Text("Speech Rate")
@@ -525,12 +528,7 @@ public struct SettingsView: View {
525528
} header: {
526529
Text("Voice")
527530
} footer: {
528-
VStack(alignment: .leading, spacing: 8) {
529-
Text("When enabled, Clarissa will speak responses aloud in voice mode.")
530-
if voiceOutputEnabled {
531-
Text("Premium and Enhanced voices provide the best quality. Download more in Settings -> Accessibility -> Spoken Content -> Voices.")
532-
}
533-
}
531+
Text("When enabled, Clarissa will speak responses aloud in voice mode.")
534532
}
535533

536534
Section {
@@ -656,6 +654,13 @@ public struct SettingsView: View {
656654
}
657655
}
658656

657+
/// Check if any Premium or Enhanced voices are available
658+
private var hasHighQualityVoices: Bool {
659+
availableVoices.contains { voice in
660+
voice.quality == .premium || voice.quality == .enhanced
661+
}
662+
}
663+
659664
private func testVoice() {
660665
voiceTester.testVoice(rate: speechRate, voiceIdentifier: selectedVoiceIdentifier)
661666
}
@@ -970,6 +975,55 @@ struct MemorySettingsView: View {
970975
}
971976
}
972977

978+
// MARK: - Voice Download Prompt
979+
980+
/// View shown when no Premium or Enhanced voices are installed
981+
/// Provides helpful guidance and a deep link to system settings
982+
private struct VoiceDownloadPromptView: View {
983+
var body: some View {
984+
VStack(alignment: .leading, spacing: 12) {
985+
HStack(spacing: 8) {
986+
Image(systemName: "exclamationmark.triangle.fill")
987+
.foregroundStyle(.orange)
988+
Text("No High-Quality Voices")
989+
.font(.subheadline.weight(.semibold))
990+
}
991+
992+
Text("For the best experience, download Premium or Enhanced voices from System Settings.")
993+
.font(.caption)
994+
.foregroundStyle(.secondary)
995+
996+
#if os(iOS)
997+
Button {
998+
// Deep link to Accessibility settings
999+
// iOS 26: Settings > Accessibility > Read & Speak > Voices
1000+
if let url = URL(string: "App-prefs:ACCESSIBILITY") {
1001+
UIApplication.shared.open(url)
1002+
}
1003+
} label: {
1004+
HStack(spacing: 4) {
1005+
Text("Open Accessibility Settings")
1006+
Image(systemName: "arrow.up.forward.app")
1007+
}
1008+
.font(.caption.weight(.medium))
1009+
}
1010+
.buttonStyle(.borderedProminent)
1011+
.tint(ClarissaTheme.purple)
1012+
.controlSize(.small)
1013+
1014+
Text("Go to: Read & Speak > Voices")
1015+
.font(.caption2)
1016+
.foregroundStyle(.tertiary)
1017+
#else
1018+
Text("Open System Settings > Accessibility > Spoken Content > System Voices")
1019+
.font(.caption)
1020+
.foregroundStyle(.tertiary)
1021+
#endif
1022+
}
1023+
.padding(.vertical, 8)
1024+
}
1025+
}
1026+
9731027
// MARK: - Voice Tester Helper
9741028

9751029
/// Helper class for testing voice settings with proper delegate handling

0 commit comments

Comments
 (0)