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
62 changes: 32 additions & 30 deletions Sources/MacOptimizer/Infrastructure/AI/LocalHeuristicProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Foundation
/// 100% Offline, zero-network rule-based diagnostic and intelligence provider.
public struct LocalHeuristicProvider: AIProvider {
public let providerId: String = "local_heuristics"
public let displayName: String = "Yerel Kural Motoru (Çevrimdışı & Güvenli)"
public let displayName: String = L10n.string("Local Rule Engine (100% Offline & Secure)", table: .ai)
public let requiresNetwork: Bool = false

public init() {}
Expand All @@ -20,22 +20,22 @@ public struct LocalHeuristicProvider: AIProvider {
var insights: [AIInsight] = []
let heaviestApps = topProcesses.filter { $0.isUserApp && !$0.isProtected }.prefix(3)
let memoryAdvice = heaviestApps.isEmpty
? "En çok bellek kullanan uygulamaları kapatmayı deneyin."
: "Bellek kullanımını azaltmak için şu yoğun uygulamaları kapatmayı deneyin: " + heaviestApps.map { "\($0.name) (\($0.memoryFormatted))" }.joined(separator: ", ") + "."
? L10n.string("Try closing the apps using the most memory.", table: .ai)
: L10n.string("To reduce memory usage, try closing these heavy apps: %@.", table: .ai, heaviestApps.map { "\($0.name) (\($0.memoryFormatted))" }.joined(separator: ", "))

// 1. Memory Pressure Evaluation
if memory.pressureLevel == .critical || memory.usedPercentage > 0.88 {
insights.append(AIInsight(
title: "Kritik Bellek Baskısı Tespit Edildi",
summary: "RAM kullanımı %\(Int(memory.usedPercentage * 100)) seviyesinde. \(memoryAdvice)",
title: L10n.string("Critical Memory Pressure Detected", table: .ai),
summary: L10n.string("RAM usage is at %lld%%. %@", table: .ai, Int(memory.usedPercentage * 100), memoryAdvice),
severity: .critical,
category: "RAM",
actions: []
))
} else if memory.pressureLevel == .warning || memory.usedPercentage > 0.75 {
insights.append(AIInsight(
title: "Orta Düzey Bellek Yoğunluğu",
summary: "Bellek doluluğu %\(Int(memory.usedPercentage * 100)). \(memoryAdvice)",
title: L10n.string("Moderate Memory Load", table: .ai),
summary: L10n.string("Memory usage is %lld%%. %@", table: .ai, Int(memory.usedPercentage * 100), memoryAdvice),
severity: .warning,
category: "RAM",
actions: []
Expand All @@ -46,12 +46,12 @@ public struct LocalHeuristicProvider: AIProvider {
let heavyProcs = topProcesses.filter { $0.cpuPercentage > 75.0 && !$0.isProtected }
if let runaway = heavyProcs.first {
insights.append(AIInsight(
title: "Yüksek CPU Tüketen Süreç: \(runaway.name)",
summary: "\(runaway.name) (PID: \(runaway.pid)) tek başına %\(String(format: "%.1f", runaway.cpuPercentage)) CPU tüketiyor. Bu durum fan gürültüsüne ve pilin hızlı tükenmesine yol açabilir.",
title: L10n.string("High CPU Process: %@", table: .ai, runaway.name),
summary: L10n.string("%@ (PID: %d) alone is using CPU at %@%%. This can cause fan noise and fast battery drain.", table: .ai, runaway.name, runaway.pid, String(format: "%.1f", runaway.cpuPercentage)),
severity: .warning,
category: "İşlemci",
category: L10n.string("CPU", table: .ai),
actions: [
AIAction(title: "\(runaway.name) Kapat", type: .killProcess, targetPID: runaway.pid)
AIAction(title: L10n.string("Quit %@", table: .ai, runaway.name), type: .killProcess, targetPID: runaway.pid)
]
))
}
Expand All @@ -60,36 +60,36 @@ public struct LocalHeuristicProvider: AIProvider {
let totalJunk = junkGroups.reduce(0) { $0 + $1.totalSizeBytes }
if totalJunk > 3 * 1024 * 1024 * 1024 {
insights.append(AIInsight(
title: "\(ByteFormatter.format(totalJunk)) Gereksiz Önbellek Birikimi",
summary: "Xcode DerivedData, sistem logları ve tarayıcı önbelleklerinde önemli miktarda yer kazanılabilir alan tespit edildi.",
title: L10n.string("%@ of Unneeded Cache Buildup", table: .ai, ByteFormatter.format(totalJunk)),
summary: L10n.string("A significant amount of reclaimable space was found in Xcode DerivedData, system logs, and browser caches.", table: .ai),
severity: .recommendation,
category: "Disk",
category: L10n.string("Disk", table: .ai),
actions: [
AIAction(title: "Gereksiz Dosyaları Temizle", type: .cleanJunk)
AIAction(title: L10n.string("Clean Junk Files", table: .ai), type: .cleanJunk)
]
))
}

// 4. Software Updates
if outdatedAppsCount > 0 {
insights.append(AIInsight(
title: "\(outdatedAppsCount) Uygulama Güncellemesi Mevcut",
summary: "Yüklü uygulamalarınızın yeni sürümleri yayınlandı. En son güvenlik yamaları ve performans iyileştirmeleri için güncellemeniz önerilir.",
title: L10n.string("%lld App Updates Available", table: .ai, outdatedAppsCount),
summary: L10n.string("New versions of your installed apps have been released. Updating is recommended for the latest security patches and performance improvements.", table: .ai),
severity: .info,
category: "Yazılım",
category: L10n.string("Software", table: .ai),
actions: [
AIAction(title: "Güncellemeleri İncele", type: .checkUpdates)
AIAction(title: L10n.string("Review Updates", table: .ai), type: .checkUpdates)
]
))
}

// 5. Default Healthy State
if insights.isEmpty {
insights.append(AIInsight(
title: "Mac'iniz Harika Durumda",
summary: "Bellek baskısı normal seviyede, olağandışı kaçak CPU süreci bulunmuyor ve disk alanınız dengeli.",
title: L10n.string("Your Mac Is in Great Shape", table: .ai),
summary: L10n.string("Memory pressure is normal, there is no unusual runaway CPU process, and your disk space is balanced.", table: .ai),
severity: .info,
category: "Genel"
category: L10n.string("General", table: .ai)
))
}

Expand All @@ -101,17 +101,19 @@ public struct LocalHeuristicProvider: AIProvider {
snapshotContext: String
) async throws -> String {
guard let lastUserMsg = messages.last(where: { $0.role == .user })?.content.lowercased() else {
return "Size nasıl yardımcı olabilirim?"
return L10n.string("How can I help you?", table: .ai)
}

if lastUserMsg.contains("ram") || lastUserMsg.contains("bellek") {
return "Mac'inizin bellek durumunu inceledim. Bellek baskısını azaltmak için en yoğun uygulamaları kapatmayı deneyebilirsiniz.\n\nSistem Bilgisi:\n\(snapshotContext)"
} else if lastUserMsg.contains("ısın") || lastUserMsg.contains("cpu") || lastUserMsg.contains("fan") {
return "İşlemci ve donanım telemetrisine göre en çok kaynak tüketen süreçleri Görev Yöneticisi'nden kontrol edebilir, askıda kalan kullanıcı uygulamalarını güvenle sonlandırabilirsiniz."
} else if lastUserMsg.contains("temiz") || lastUserMsg.contains("disk") || lastUserMsg.contains("yer") {
return "Disk Temizleyici modülü ile sistem önbelleklerini, derleme kalıntılarını (Xcode DerivedData) ve tarayıcı verilerini güvenle temizleyebilirsiniz."
// Keyword lists cover both Turkish and English user input.
if lastUserMsg.contains("ram") || lastUserMsg.contains("bellek") || lastUserMsg.contains("memory") {
return L10n.string("I reviewed your Mac’s memory status. To reduce memory pressure, you can try closing the most demanding apps.\n\nSystem Info:\n%@", table: .ai, snapshotContext)
} else if lastUserMsg.contains("ısın") || lastUserMsg.contains("cpu") || lastUserMsg.contains("fan") || lastUserMsg.contains("heat") {
return L10n.string("Based on processor and hardware telemetry, you can check the most resource-hungry processes in Task Manager and safely terminate unresponsive user apps.", table: .ai)
} else if lastUserMsg.contains("temiz") || lastUserMsg.contains("disk") || lastUserMsg.contains("yer")
|| lastUserMsg.contains("clean") || lastUserMsg.contains("space") || lastUserMsg.contains("storage") {
return L10n.string("With the Disk Cleaner module, you can safely clean system caches, build leftovers (Xcode DerivedData), and browser data.", table: .ai)
} else {
return "MacOptimizer Pro yerel yapay zeka motoru devrede. Mac'inizin performansı, bellek yönetimi ve sistem sağlığı hakkında dilediğiniz soruyu sorabilirsiniz."
return L10n.string("The MacOptimizer Pro local AI engine is active. Ask any question about your Mac’s performance, memory management, and system health.", table: .ai)
}
}
}
20 changes: 15 additions & 5 deletions Sources/MacOptimizer/Infrastructure/AI/NvidiaNIMProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,43 @@ import Foundation

public struct NvidiaNIMProvider: AIProvider {
public let providerId = "nvidia_nim"
public let displayName = "NVIDIA NIM (Bulut)"
public let displayName = L10n.string("NVIDIA NIM (Cloud)", table: .ai)
public let requiresNetwork = true
private let config: NIMConfig

public init(config: NIMConfig) { self.config = config }

public nonisolated static func cloudContext(_ base: String, processNames: [String], includeProcesses: Bool) -> String {
guard includeProcesses, !processNames.isEmpty else { return base }
return base + "\nÇalışan uygulamalar: " + processNames.joined(separator: ", ")
return base + "\nRunning apps: " + processNames.joined(separator: ", ")
}

/// Model instructions are always English; the reply language follows the app's UI language.
public nonisolated static func diagnosisPrompt(_ metrics: String, replyLanguage: String) -> String {
metrics + " Give short system recommendations. Reply in \(replyLanguage)."
}

public nonisolated static func copilotSystemPrompt(snapshotContext: String, replyLanguage: String) -> String {
"You are the MacOptimizer Pro macOS assistant. Reply in \(replyLanguage); only offer recommendations, never execute actions. System summary: \(snapshotContext)"
}

public func diagnose(memory: MemoryStats, cpu: CPUStats, disk: DiskStats, hardware: HardwareInfo,
topProcesses: [ProcessInfoModel], junkGroups: [JunkCategoryGroup], outdatedAppsCount: Int) async -> [AIInsight] {
let totalJunk = junkGroups.reduce(0) { $0 + $1.totalSizeBytes }
let base = "Mac: \(hardware.modelName), \(hardware.chipName), \(hardware.osVersion). RAM %\(Int(memory.usedPercentage * 100)), CPU %\(cpu.totalUsage), Boş disk %\(Int(disk.freePercentage * 100)), Gereksiz dosyalar \(ByteFormatter.format(totalJunk)), Güncelleme bekleyen uygulama: \(outdatedAppsCount). Türkçe kısa sistem önerileri ver."
let metrics = "Mac: \(hardware.modelName), \(hardware.chipName), \(hardware.osVersion). RAM \(Int(memory.usedPercentage * 100))%, CPU \(cpu.totalUsage)%, Free disk \(Int(disk.freePercentage * 100))%, Junk files \(ByteFormatter.format(totalJunk)), Apps with pending updates: \(outdatedAppsCount)."
let base = Self.diagnosisPrompt(metrics, replyLanguage: L10n.currentLanguageEnglishName())
let prompt = Self.cloudContext(base, processNames: topProcesses.prefix(4).map { "\($0.name) (PID: \($0.pid))" }, includeProcesses: UserDefaults.standard.bool(forKey: "MacOptimizer_IncludeRunningAppNames"))
do {
let answer = try await NvidiaNIMService.shared.sendChatCompletion(messages: [["role": "user", "content": prompt]], config: config)
return [AIInsight(title: "NVIDIA NIM Sistem Analizi", summary: answer, severity: .recommendation, category: "Yapay Zeka Raporu")]
return [AIInsight(title: L10n.string("NVIDIA NIM System Analysis", table: .ai), summary: answer, severity: .recommendation, category: L10n.string("AI Report", table: .ai))]
} catch {
return await LocalHeuristicProvider().diagnose(memory: memory, cpu: cpu, disk: disk, hardware: hardware,
topProcesses: topProcesses, junkGroups: junkGroups, outdatedAppsCount: outdatedAppsCount)
}
}

public func queryCopilot(messages: [AIChatMessage], snapshotContext: String) async throws -> String {
let system = "MacOptimizer Pro macOS asistanısın. Türkçe yanıt ver; sadece öneri sun, eylem çalıştırma. Sistem özeti: \(snapshotContext)"
let system = Self.copilotSystemPrompt(snapshotContext: snapshotContext, replyLanguage: L10n.currentLanguageEnglishName())
let chat = messages.suffix(6).map { ["role": $0.role.rawValue, "content": $0.content] }
return try await NvidiaNIMService.shared.sendChatCompletion(messages: chat, config: config, systemPrompt: system)
}
Expand Down
15 changes: 13 additions & 2 deletions Sources/MacOptimizer/L10n.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,26 @@ public enum L10n {
case common, dashboard, cleanup, ai, services
}

public static func string(_ key: String, table: Table = .common) -> String {
/// The module localization (e.g. "en", "tr") that `string(_:table:)` resolves against, if any.
public static func currentLanguage() -> String? {
let preferences = UserDefaults.standard.stringArray(forKey: "AppleLanguages")
?? Bundle.main.preferredLocalizations
let language = preferences.lazy.compactMap { preference in
return preferences.lazy.compactMap { preference in
Bundle.module.localizations.first {
preference.caseInsensitiveCompare($0) == .orderedSame
|| preference.lowercased().hasPrefix($0.lowercased() + "-")
}
}.first
}

/// English name of the UI language in use (e.g. "Turkish", "English"), for model instructions.
public static func currentLanguageEnglishName() -> String {
let code = currentLanguage() ?? Bundle.module.developmentLocalization ?? "en"
return Locale(identifier: "en").localizedString(forLanguageCode: code) ?? "English"
}

public static func string(_ key: String, table: Table = .common) -> String {
let language = currentLanguage()
if let language,
let path = Bundle.module.path(forResource: language, ofType: "lproj"),
let bundle = Bundle(path: path) {
Expand Down
47 changes: 47 additions & 0 deletions Sources/MacOptimizer/Resources/en.lproj/ai.strings
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,50 @@
"General & Menu Bar" = "General & Menu Bar";
"Show live status icon in the menu bar" = "Show live status icon in the menu bar";
"Open-source, security-first, local macOS system health and optimization toolkit." = "Open-source, security-first, local macOS system health and optimization toolkit.";
"NVIDIA NIM (Cloud)" = "NVIDIA NIM (Cloud)";
"Try closing the apps using the most memory." = "Try closing the apps using the most memory.";
"To reduce memory usage, try closing these heavy apps: %@." = "To reduce memory usage, try closing these heavy apps: %@.";
"Critical Memory Pressure Detected" = "Critical Memory Pressure Detected";
"RAM usage is at %lld%%. %@" = "RAM usage is at %lld%%. %@";
"Moderate Memory Load" = "Moderate Memory Load";
"Memory usage is %lld%%. %@" = "Memory usage is %lld%%. %@";
"High CPU Process: %@" = "High CPU Process: %@";
"%@ (PID: %d) alone is using CPU at %@%%. This can cause fan noise and fast battery drain." = "%@ (PID: %d) alone is using CPU at %@%%. This can cause fan noise and fast battery drain.";
"CPU" = "CPU";
"Quit %@" = "Quit %@";
"%@ of Unneeded Cache Buildup" = "%@ of Unneeded Cache Buildup";
"A significant amount of reclaimable space was found in Xcode DerivedData, system logs, and browser caches." = "A significant amount of reclaimable space was found in Xcode DerivedData, system logs, and browser caches.";
"Disk" = "Disk";
"Clean Junk Files" = "Clean Junk Files";
"New versions of your installed apps have been released. Updating is recommended for the latest security patches and performance improvements." = "New versions of your installed apps have been released. Updating is recommended for the latest security patches and performance improvements.";
"Software" = "Software";
"Review Updates" = "Review Updates";
"Your Mac Is in Great Shape" = "Your Mac Is in Great Shape";
"Memory pressure is normal, there is no unusual runaway CPU process, and your disk space is balanced." = "Memory pressure is normal, there is no unusual runaway CPU process, and your disk space is balanced.";
"General" = "General";
"How can I help you?" = "How can I help you?";
"I reviewed your Mac’s memory status. To reduce memory pressure, you can try closing the most demanding apps.\n\nSystem Info:\n%@" = "I reviewed your Mac’s memory status. To reduce memory pressure, you can try closing the most demanding apps.\n\nSystem Info:\n%@";
"Based on processor and hardware telemetry, you can check the most resource-hungry processes in Task Manager and safely terminate unresponsive user apps." = "Based on processor and hardware telemetry, you can check the most resource-hungry processes in Task Manager and safely terminate unresponsive user apps.";
"With the Disk Cleaner module, you can safely clean system caches, build leftovers (Xcode DerivedData), and browser data." = "With the Disk Cleaner module, you can safely clean system caches, build leftovers (Xcode DerivedData), and browser data.";
"The MacOptimizer Pro local AI engine is active. Ask any question about your Mac’s performance, memory management, and system health." = "The MacOptimizer Pro local AI engine is active. Ask any question about your Mac’s performance, memory management, and system health.";
"NVIDIA NIM System Analysis" = "NVIDIA NIM System Analysis";
"AI Report" = "AI Report";
"Check for Updates" = "Check for Updates";
"Reset DNS Cache" = "Reset DNS Cache";
"NVIDIA NIM connection error. Local suggestions are offered in the action buttons below." = "NVIDIA NIM connection error. Local suggestions are offered in the action buttons below.";
"Invalid Base URL format." = "Invalid Base URL format.";
"Security Warning: HTTPS is required for remote API connections." = "Security Warning: HTTPS is required for remote API connections.";
"Unsupported URL protocol. (Use https://)" = "Unsupported URL protocol. (Use https://)";
"NVIDIA NIM API key is missing. Please enter your key." = "NVIDIA NIM API key is missing. Please enter your key.";
"Invalid /models URL." = "Invalid /models URL.";
"Received an invalid HTTP response from the server." = "Received an invalid HTTP response from the server.";
"NVIDIA NIM Error (%lld): %@" = "NVIDIA NIM Error (%lld): %@";
"HTTP Error Code: %lld" = "HTTP Error Code: %lld";
"Could not parse the model list." = "Could not parse the model list.";
"Model scanned via NVIDIA NIM." = "Model scanned via NVIDIA NIM.";
"No NVIDIA NIM API key entered. Please enter your key starting with 'nvapi-...'." = "No NVIDIA NIM API key entered. Please enter your key starting with 'nvapi-...'.";
"NVIDIA NIM connection and model ('%@') verified!" = "NVIDIA NIM connection and model ('%@') verified!";
"Connection error: %@" = "Connection error: %@";
"NVIDIA NIM API key is missing." = "NVIDIA NIM API key is missing.";
"Invalid /chat/completions URL format." = "Invalid /chat/completions URL format.";
"Could not parse the API response." = "Could not parse the API response.";
Loading
Loading