Optimize AI generation speed and add richer insight data
Speed optimizations:
- Add session.prewarm() in InsightsViewModel and ReportsViewModel init
for 40% faster first-token latency
- Cap maximumResponseTokens on all 8 AI respond() calls (100-600 per use case)
- Add prompt brevity constraints ("1-2 sentences", "2 sentences")
- Reduce report batch concurrency from 4 to 2 to prevent device contention
- Pre-fetch health data once and share across all 3 insight periods
Richer insight data in MoodDataSummarizer:
- Tag-mood correlations: overall frequency + good day vs bad day tag breakdown
- Weather-mood correlations: avg mood by condition and temperature range
- Absence pattern detection: logging gap count with pre/post-gap mood averages
- Entry source breakdown: % of entries from App, Widget, Watch, Siri, etc.
- Update insight prompt to leverage tags, weather, and gap data when available
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -49,6 +49,23 @@ struct MoodDataSummary {
|
||||
|
||||
// Health data for AI analysis (optional)
|
||||
let healthAverages: HealthService.HealthAverages?
|
||||
|
||||
// Tag-mood correlations
|
||||
let tagFrequencies: [String: Int]
|
||||
let goodDayTags: [String: Int] // tag counts for entries with mood good/great
|
||||
let badDayTags: [String: Int] // tag counts for entries with mood bad/horrible
|
||||
|
||||
// Weather-mood correlation
|
||||
let weatherMoodAverages: [String: Double] // condition -> avg mood (1-5 scale)
|
||||
let tempRangeMoodAverages: [String: Double] // "Cold"/"Mild"/"Warm"/"Hot" -> avg mood
|
||||
|
||||
// Absence patterns
|
||||
let loggingGapCount: Int // number of 2+ day gaps
|
||||
let preGapMoodAverage: Double // avg mood in 3 days before a gap
|
||||
let postGapMoodAverage: Double // avg mood in 3 days after returning
|
||||
|
||||
// Entry source breakdown
|
||||
let entrySourceBreakdown: [String: Int] // source name -> count
|
||||
}
|
||||
|
||||
/// Transforms raw MoodEntryModel data into AI-optimized summaries
|
||||
@@ -83,6 +100,11 @@ class MoodDataSummarizer {
|
||||
// Format date range
|
||||
let dateRange = formatDateRange(entries: sortedEntries)
|
||||
|
||||
let tagAnalysis = calculateTagAnalysis(entries: validEntries)
|
||||
let weatherAnalysis = calculateWeatherAnalysis(entries: validEntries)
|
||||
let absencePatterns = calculateAbsencePatterns(entries: sortedEntries)
|
||||
let sourceBreakdown = calculateEntrySourceBreakdown(entries: validEntries)
|
||||
|
||||
return MoodDataSummary(
|
||||
periodName: periodName,
|
||||
totalEntries: validEntries.count,
|
||||
@@ -107,7 +129,16 @@ class MoodDataSummarizer {
|
||||
last7DaysMoods: recentContext.moods,
|
||||
hasAllMoodTypes: moodTypes.hasAll,
|
||||
missingMoodTypes: moodTypes.missing,
|
||||
healthAverages: healthAverages
|
||||
healthAverages: healthAverages,
|
||||
tagFrequencies: tagAnalysis.frequencies,
|
||||
goodDayTags: tagAnalysis.goodDayTags,
|
||||
badDayTags: tagAnalysis.badDayTags,
|
||||
weatherMoodAverages: weatherAnalysis.conditionAverages,
|
||||
tempRangeMoodAverages: weatherAnalysis.tempRangeAverages,
|
||||
loggingGapCount: absencePatterns.gapCount,
|
||||
preGapMoodAverage: absencePatterns.preGapAverage,
|
||||
postGapMoodAverage: absencePatterns.postGapAverage,
|
||||
entrySourceBreakdown: sourceBreakdown
|
||||
)
|
||||
}
|
||||
|
||||
@@ -346,6 +377,139 @@ class MoodDataSummarizer {
|
||||
return (hasAll, missing)
|
||||
}
|
||||
|
||||
// MARK: - Tag Analysis
|
||||
|
||||
private func calculateTagAnalysis(entries: [MoodEntryModel]) -> (frequencies: [String: Int], goodDayTags: [String: Int], badDayTags: [String: Int]) {
|
||||
var frequencies: [String: Int] = [:]
|
||||
var goodDayTags: [String: Int] = [:]
|
||||
var badDayTags: [String: Int] = [:]
|
||||
|
||||
for entry in entries {
|
||||
let entryTags = entry.tags
|
||||
guard !entryTags.isEmpty else { continue }
|
||||
|
||||
for tag in entryTags {
|
||||
let normalizedTag = tag.lowercased()
|
||||
frequencies[normalizedTag, default: 0] += 1
|
||||
|
||||
if [.good, .great].contains(entry.mood) {
|
||||
goodDayTags[normalizedTag, default: 0] += 1
|
||||
} else if [.bad, .horrible].contains(entry.mood) {
|
||||
badDayTags[normalizedTag, default: 0] += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (frequencies, goodDayTags, badDayTags)
|
||||
}
|
||||
|
||||
// MARK: - Weather Analysis
|
||||
|
||||
private func calculateWeatherAnalysis(entries: [MoodEntryModel]) -> (conditionAverages: [String: Double], tempRangeAverages: [String: Double]) {
|
||||
var conditionTotals: [String: (total: Int, count: Int)] = [:]
|
||||
var tempRangeTotals: [String: (total: Int, count: Int)] = [:]
|
||||
|
||||
for entry in entries {
|
||||
guard let json = entry.weatherJSON, let weather = WeatherData.decode(from: json) else { continue }
|
||||
|
||||
let moodScore = Int(entry.moodValue) + 1 // 1-5 scale
|
||||
|
||||
// Group by weather condition
|
||||
let condition = weather.condition
|
||||
let current = conditionTotals[condition, default: (0, 0)]
|
||||
conditionTotals[condition] = (current.total + moodScore, current.count + 1)
|
||||
|
||||
// Group by temperature range (convert Celsius to Fahrenheit)
|
||||
let tempF = weather.temperature * 9.0 / 5.0 + 32.0
|
||||
let tempRange: String
|
||||
if tempF < 50 {
|
||||
tempRange = "Cold"
|
||||
} else if tempF <= 70 {
|
||||
tempRange = "Mild"
|
||||
} else if tempF <= 85 {
|
||||
tempRange = "Warm"
|
||||
} else {
|
||||
tempRange = "Hot"
|
||||
}
|
||||
let currentTemp = tempRangeTotals[tempRange, default: (0, 0)]
|
||||
tempRangeTotals[tempRange] = (currentTemp.total + moodScore, currentTemp.count + 1)
|
||||
}
|
||||
|
||||
var conditionAverages: [String: Double] = [:]
|
||||
for (condition, data) in conditionTotals {
|
||||
conditionAverages[condition] = Double(data.total) / Double(data.count)
|
||||
}
|
||||
|
||||
var tempRangeAverages: [String: Double] = [:]
|
||||
for (range, data) in tempRangeTotals {
|
||||
tempRangeAverages[range] = Double(data.total) / Double(data.count)
|
||||
}
|
||||
|
||||
return (conditionAverages, tempRangeAverages)
|
||||
}
|
||||
|
||||
// MARK: - Absence Patterns
|
||||
|
||||
private func calculateAbsencePatterns(entries: [MoodEntryModel]) -> (gapCount: Int, preGapAverage: Double, postGapAverage: Double) {
|
||||
guard entries.count >= 2 else {
|
||||
return (0, 0, 0)
|
||||
}
|
||||
|
||||
var gapCount = 0
|
||||
var preGapScores: [Int] = []
|
||||
var postGapScores: [Int] = []
|
||||
|
||||
for i in 1..<entries.count {
|
||||
let dayDiff = calendar.dateComponents([.day], from: entries[i-1].forDate, to: entries[i].forDate).day ?? 0
|
||||
guard dayDiff >= 2 else { continue }
|
||||
|
||||
gapCount += 1
|
||||
|
||||
// Collect up to 3 entries before the gap
|
||||
let preStart = max(0, i - 3)
|
||||
for j in preStart..<i {
|
||||
preGapScores.append(Int(entries[j].moodValue) + 1)
|
||||
}
|
||||
|
||||
// Collect up to 3 entries after the gap
|
||||
let postEnd = min(entries.count, i + 3)
|
||||
for j in i..<postEnd {
|
||||
postGapScores.append(Int(entries[j].moodValue) + 1)
|
||||
}
|
||||
}
|
||||
|
||||
let preAvg = preGapScores.isEmpty ? 0.0 : Double(preGapScores.reduce(0, +)) / Double(preGapScores.count)
|
||||
let postAvg = postGapScores.isEmpty ? 0.0 : Double(postGapScores.reduce(0, +)) / Double(postGapScores.count)
|
||||
|
||||
return (gapCount, preAvg, postAvg)
|
||||
}
|
||||
|
||||
// MARK: - Entry Source Breakdown
|
||||
|
||||
private func calculateEntrySourceBreakdown(entries: [MoodEntryModel]) -> [String: Int] {
|
||||
var breakdown: [String: Int] = [:]
|
||||
|
||||
let sourceNames: [Int: String] = [
|
||||
0: "App",
|
||||
1: "Widget",
|
||||
2: "Watch",
|
||||
3: "Shortcut",
|
||||
4: "Auto-fill",
|
||||
5: "Notification",
|
||||
6: "Header",
|
||||
7: "Siri",
|
||||
8: "Control Center",
|
||||
9: "Live Activity"
|
||||
]
|
||||
|
||||
for entry in entries {
|
||||
let name = sourceNames[entry.entryType] ?? "Other"
|
||||
breakdown[name, default: 0] += 1
|
||||
}
|
||||
|
||||
return breakdown
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func formatDateRange(entries: [MoodEntryModel]) -> String {
|
||||
@@ -384,7 +548,16 @@ class MoodDataSummarizer {
|
||||
last7DaysMoods: [],
|
||||
hasAllMoodTypes: false,
|
||||
missingMoodTypes: ["great", "good", "average", "bad", "horrible"],
|
||||
healthAverages: nil
|
||||
healthAverages: nil,
|
||||
tagFrequencies: [:],
|
||||
goodDayTags: [:],
|
||||
badDayTags: [:],
|
||||
weatherMoodAverages: [:],
|
||||
tempRangeMoodAverages: [:],
|
||||
loggingGapCount: 0,
|
||||
preGapMoodAverage: 0,
|
||||
postGapMoodAverage: 0,
|
||||
entrySourceBreakdown: [:]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -469,6 +642,53 @@ class MoodDataSummarizer {
|
||||
lines.append("Analyze how these health metrics may correlate with mood patterns.")
|
||||
}
|
||||
|
||||
// Tag-mood correlations (only if tags exist)
|
||||
if !summary.tagFrequencies.isEmpty {
|
||||
let topTags = summary.tagFrequencies.sorted { $0.value > $1.value }.prefix(8)
|
||||
.map { "\($0.key)(\($0.value))" }.joined(separator: ", ")
|
||||
lines.append("Themes: \(topTags)")
|
||||
|
||||
if !summary.goodDayTags.isEmpty {
|
||||
let goodTags = summary.goodDayTags.sorted { $0.value > $1.value }.prefix(5)
|
||||
.map { "\($0.key)(\($0.value))" }.joined(separator: ", ")
|
||||
lines.append("Good day themes: \(goodTags)")
|
||||
}
|
||||
if !summary.badDayTags.isEmpty {
|
||||
let badTags = summary.badDayTags.sorted { $0.value > $1.value }.prefix(5)
|
||||
.map { "\($0.key)(\($0.value))" }.joined(separator: ", ")
|
||||
lines.append("Bad day themes: \(badTags)")
|
||||
}
|
||||
}
|
||||
|
||||
// Weather-mood (only if weather data exists)
|
||||
if !summary.weatherMoodAverages.isEmpty {
|
||||
let weatherMood = summary.weatherMoodAverages.sorted { $0.value > $1.value }
|
||||
.map { "\($0.key) avg \(String(format: "%.1f", $0.value))" }.joined(separator: ", ")
|
||||
lines.append("Weather-mood: \(weatherMood)")
|
||||
}
|
||||
if !summary.tempRangeMoodAverages.isEmpty {
|
||||
let tempMood = ["Cold", "Mild", "Warm", "Hot"].compactMap { range -> String? in
|
||||
guard let avg = summary.tempRangeMoodAverages[range] else { return nil }
|
||||
return "\(range) avg \(String(format: "%.1f", avg))"
|
||||
}.joined(separator: ", ")
|
||||
if !tempMood.isEmpty {
|
||||
lines.append("Temp-mood: \(tempMood)")
|
||||
}
|
||||
}
|
||||
|
||||
// Gaps (only if gaps exist)
|
||||
if summary.loggingGapCount > 0 {
|
||||
lines.append("Logging gaps: \(summary.loggingGapCount) breaks of 2+ days. Pre-gap avg: \(String(format: "%.1f", summary.preGapMoodAverage))/5, Post-return avg: \(String(format: "%.1f", summary.postGapMoodAverage))/5")
|
||||
}
|
||||
|
||||
// Sources (only if multiple sources)
|
||||
if summary.entrySourceBreakdown.count > 1 {
|
||||
let total = Double(summary.entrySourceBreakdown.values.reduce(0, +))
|
||||
let sources = summary.entrySourceBreakdown.sorted { $0.value > $1.value }
|
||||
.map { "\($0.key) \(Int(Double($0.value) / total * 100))%" }.joined(separator: ", ")
|
||||
lines.append("Entry sources: \(sources)")
|
||||
}
|
||||
|
||||
return lines.joined(separator: "\n")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user