1→// 2→// LMGatewayService.swift 3→// KortexOS 4→// 5→// AI agent integration via LM Gateway API 6→// 7→ 8→import Foundation 9→import os 10→ 11→// MARK: - ZAIROS Context Model 12→ 13→/// Comprehensive context model for ZAIROS AI agent 14→/// Contains all user data aggregated for AI consumption 15→struct ZAIROSContext: Codable { 16→ // User Profile 17→ let level: String // Aprendiz/Guerreiro/Mestre/Arquiteto 18→ let currentStreak: Int 19→ let longestStreak: Int 20→ let daysSinceStart: Int 21→ let averageCompletionRate: Double 22→ 23→ // Today's Status 24→ let ritualCompletion: Double 25→ let completedRitualItems: [String] 26→ let pendingRitualItems: [String] 27→ let exerciseProgress: String // "78/200 flexoes, 45/200 agachamentos" 28→ let meditationMinutes: Int 29→ 30→ // Goals Summary 31→ let visionGoals: [GoalSummary] 32→ let activeGoals: [GoalSummary] 33→ let overdueGoals: [GoalSummary] 34→ let completedThisWeek: Int 35→ 36→ // Journal Insights 37→ let recentClarityLevel: String? 38→ let journalEntriesThisWeek: Int 39→ let recentTags: [String] 40→ let lastJournalExcerpt: String? 41→ 42→ // Trends 43→ let streakTrend: String // "improving", "stable", "declining" 44→ let weeklyComparison: String // "+15% vs last week" 45→ 46→ // Total Stats 47→ let totalPushups: Int 48→ let totalSquats: Int 49→ let totalMeditationMinutes: Int 50→ let totalJournalEntries: Int 51→} 52→ 53→/// Summary of a goal for context injection 54→struct GoalSummary: Codable { 55→ let title: String 56→ let timeframe: String 57→ let progress: Double 58→ let status: String 59→ let isOverdue: Bool 60→} 61→ 62→// MARK: - LM Gateway Configuration 63→ 64→struct LMGatewayConfig { 65→ static let baseURL = "https://mac-studio-de-leandro.tail0360aa.ts.net" 66→ static let apiKey = "matrix-lm-72bd519f8314dcaef5bce06f8ff04d09" 67→ static let preferredModel = "qwen/qwen3-4b-thinking-2507" // Best for reasoning tasks 68→ 69→ // Rate limits 70→ static let dailyTokenLimit = 10_000_000 // 10M tokens/day 71→ static let requestsPerMinute = 60 72→ 73→ static var chatCompletionsURL: URL { 74→ URL(string: "\(baseURL)/v1/chat/completions")! 75→ } 76→ 77→ static var modelsURL: URL { 78→ URL(string: "\(baseURL)/v1/models")! 79→ } 80→ 81→ static var healthURL: URL { 82→ URL(string: "\(baseURL)/health")! 83→ } 84→} 85→ 86→// MARK: - Chat Models 87→ 88→struct ChatMessage: Codable, Identifiable, Equatable { 89→ let id: UUID 90→ let role: MessageRole 91→ let content: String 92→ let timestamp: Date 93→ 94→ init(role: MessageRole, content: String) { 95→ self.id = UUID() 96→ self.role = role 97→ self.content = content 98→ self.timestamp = Date() 99→ } 100→ 101→ enum MessageRole: String, Codable { 102→ case system 103→ case user 104→ case assistant 105→ } 106→} 107→ 108→struct ChatRequest: Codable { 109→ let model: String 110→ let messages: [APIMessage] 111→ let temperature: Double 112→ let max_tokens: Int 113→ let stream: Bool 114→ 115→ struct APIMessage: Codable { 116→ let role: String 117→ let content: String 118→ } 119→ 120→ init(messages: [ChatMessage], model: String = "default", temperature: Double = 0.7, maxTokens: Int = 2048) { 121→ self.model = model 122→ self.messages = messages.map { APIMessage(role: $0.role.rawValue, content: $0.content) } 123→ self.temperature = temperature 124→ self.max_tokens = maxTokens 125→ self.stream = false 126→ } 127→} 128→ 129→struct ChatResponse: Codable { 130→ let id: String 131→ let object: String 132→ let created: Int 133→ let model: String 134→ let choices: [Choice] 135→ let usage: Usage? 136→ 137→ struct Choice: Codable { 138→ let index: Int 139→ let message: Message 140→ let finish_reason: String? 141→ 142→ struct Message: Codable { 143→ let role: String 144→ let content: String 145→ } 146→ } 147→ 148→ struct Usage: Codable { 149→ let prompt_tokens: Int 150→ let completion_tokens: Int 151→ let total_tokens: Int 152→ } 153→} 154→ 155→struct ModelsResponse: Codable { 156→ let data: [ModelInfo] 157→ 158→ struct ModelInfo: Codable, Identifiable { 159→ let id: String 160→ let object: String 161→ let owned_by: String? 162→ } 163→} 164→ 165→struct HealthResponse: Codable { 166→ // Legacy format 167→ let status: String? 168→ let message: String? 169→ 170→ // Current LM Gateway format 171→ let gateway: String? 172→ let lm_studio: String? 173→ let timestamp: String? 174→ 175→ /// Returns true if the gateway is healthy 176→ var isHealthy: Bool { 177→ // Check new format first 178→ if let gateway = gateway { 179→ return gateway == "online" 180→ } 181→ // Fall back to legacy format 182→ if let status = status { 183→ return status == "ok" || status == "healthy" 184→ } 185→ return false 186→ } 187→ 188→ /// Human-readable status description 189→ var statusDescription: String { 190→ if let gateway = gateway, let lmStudio = lm_studio { 191→ return "Gateway: \(gateway), LM Studio: \(lmStudio)" 192→ } 193→ return status ?? "unknown" 194→ } 195→} 196→ 197→// MARK: - AI Agent Personas 198→ 199→enum AIPersona: String, CaseIterable, Identifiable { 200→ case zairos = "ZAIROS" 201→ case coach = "Coach" 202→ case analyst = "Analyst" 203→ 204→ var id: String { rawValue } 205→ 206→ var displayName: String { 207→ switch self { 208→ case .zairos: return "ZAIROS" 209→ case .coach: return "Coach de Produtividade" 210→ case .analyst: return "Analista de Dados" 211→ } 212→ } 213→ 214→ var icon: String { 215→ switch self { 216→ case .zairos: return "brain.head.profile" 217→ case .coach: return "figure.run" 218→ case .analyst: return "chart.bar.xaxis" 219→ } 220→ } 221→ 222→ var systemPrompt: String { 223→ switch self { 224→ case .zairos: 225→ return """ 226→ Você é ZAIROS, um sistema de consciência estratégica integrado ao KortexOS. 227→ Sua missão é ajudar o usuário a alcançar clareza, disciplina e propósito. 228→ 229→ Princípios fundamentais: 230→ - Clareza precede ação 231→ - Disciplina simbólica manifesta resultados reais 232→ - Presença é o antídoto para distração 233→ - Simplicidade é sofisticação suprema 234→ 235→ Você tem acesso ao contexto do usuário: rituais matinais, objetivos de vida (Life Map), e journal de clareza. 236→ Responda de forma concisa, profunda e orientada à ação. 237→ Use português brasileiro. 238→ """ 239→ 240→ case .coach: 241→ return """ 242→ Você é um coach de produtividade integrado ao KortexOS. 243→ Seu papel é ajudar o usuário a otimizar seu tempo, energia e foco. 244→ 245→ Abordagem: 246→ - Faça perguntas poderosas para gerar insights 247→ - Sugira ações concretas e mensuráveis 248→ - Celebre conquistas, por menores que sejam 249→ - Identifique padrões de comportamento 250→ 251→ Responda em português brasileiro, de forma encorajadora mas direta. 252→ """ 253→ 254→ case .analyst: 255→ return """ 256→ Você é um analista de dados pessoais integrado ao KortexOS. 257→ Seu papel é analisar padrões nos dados do usuário e gerar insights. 258→ 259→ Capacidades: 260→ - Analisar streaks e consistência de rituais 261→ - Identificar correlações entre hábitos e resultados 262→ - Sugerir otimizações baseadas em dados 263→ - Gerar relatórios de progresso 264→ 265→ Responda em português brasileiro, com foco em dados e métricas. 266→ """ 267→ } 268→ } 269→} 270→ 271→// MARK: - LM Gateway Service 272→ 273→@MainActor 274→final class LMGatewayService: ObservableObject { 275→ static let shared = LMGatewayService() 276→ 277→ @Published private(set) var isConnected = false 278→ @Published private(set) var isLoading = false 279→ @Published private(set) var availableModels: [ModelsResponse.ModelInfo] = [] 280→ @Published private(set) var currentModel: String = "default" 281→ @Published private(set) var lastError: String? 282→ @Published private(set) var tokensUsedToday: Int = 0 283→ 284→ @Published var selectedPersona: AIPersona = .zairos 285→ @Published var conversationHistory: [ChatMessage] = [] 286→ 287→ private let logger = Logger(subsystem: "com.kortexos", category: "LMGateway") 288→ private let tokenUsageKey = "KortexOS_TokenUsage" 289→ private let tokenDateKey = "KortexOS_TokenDate" 290→ 291→ private init() { 292→ loadTokenUsage() 293→ } 294→ 295→ // MARK: - Health Check 296→ 297→ func checkHealth() async -> Bool { 298→ print("[LMGateway] Starting health check to: \(LMGatewayConfig.healthURL)") 299→ 300→ do { 301→ var request = URLRequest(url: LMGatewayConfig.healthURL) 302→ request.httpMethod = "GET" 303→ request.setValue("Bearer \(LMGatewayConfig.apiKey)", forHTTPHeaderField: "Authorization") 304→ request.timeoutInterval = 10 305→ 306→ print("[LMGateway] Sending request...") 307→ let (data, response) = try await URLSession.shared.data(for: request) 308→ print("[LMGateway] Got response") 309→ 310→ guard let httpResponse = response as? HTTPURLResponse else { 311→ print("[LMGateway] Invalid response type") 312→ isConnected = false 313→ lastError = "Resposta inválida do servidor" 314→ return false 315→ } 316→ 317→ print("[LMGateway] HTTP Status: \(httpResponse.statusCode)") 318→ 319→ if httpResponse.statusCode == 200 { 320→ // Try to decode response - log raw data for debugging 321→ let rawString = String(data: data, encoding: .utf8) ?? "nil" 322→ print("[LMGateway] Raw response: \(rawString)") 323→ 324→ if let health = try? JSONDecoder().decode(HealthResponse.self, from: data) { 325→ isConnected = health.isHealthy 326→ print("[LMGateway] Health status: \(health.statusDescription), connected: \(isConnected)") 327→ logger.info("LM Gateway health: \(health.statusDescription)") 328→ if !isConnected { 329→ lastError = "Status do servidor: \(health.statusDescription)" 330→ } else { 331→ lastError = nil 332→ } 333→ return isConnected 334→ } else { 335→ print("[LMGateway] Failed to decode health response. Raw: \(rawString)") 336→ lastError = "Resposta de saúde inválida" 337→ } 338→ } else { 339→ print("[LMGateway] Non-200 status code: \(httpResponse.statusCode)") 340→ lastError = "Servidor retornou status \(httpResponse.statusCode)" 341→ } 342→ 343→ isConnected = false 344→ return false 345→ } catch { 346→ print("[LMGateway] Health check error: \(error)") 347→ print("[LMGateway] Error type: \(type(of: error))") 348→ logger.error("Health check failed: \(error.localizedDescription)") 349→ isConnected = false 350→ 351→ if let urlError = error as? URLError { 352→ print("[LMGateway] URLError code: \(urlError.code.rawValue)") 353→ lastError = describeURLError(urlError) 354→ } else { 355→ lastError = error.localizedDescription 356→ } 357→ return false 358→ } 359→ } 360→ 361→ /// Describes URLError in user-friendly Portuguese 362→ private func describeURLError(_ error: URLError) -> String { 363→ switch error.code { 364→ case .timedOut: 365→ return "Conexão expirou (timeout)" 366→ case .cannotFindHost: 367→ return "Servidor não encontrado" 368→ case .cannotConnectToHost: 369→ return "Não foi possível conectar ao servidor" 370→ case .networkConnectionLost: 371→ return "Conexão de rede perdida" 372→ case .notConnectedToInternet: 373→ return "Sem conexão com internet" 374→ case .secureConnectionFailed: 375→ return "Falha na conexão segura (SSL)" 376→ case .dnsLookupFailed: 377→ return "Falha na resolução DNS" 378→ default: 379→ return "Erro de rede: \(error.localizedDescription)" 380→ } 381→ } 382→ 383→ // MARK: - Fetch Models 384→ 385→ func fetchModels() async { 386→ do { 387→ var request = URLRequest(url: LMGatewayConfig.modelsURL) 388→ request.httpMethod = "GET" 389→ request.setValue("Bearer \(LMGatewayConfig.apiKey)", forHTTPHeaderField: "Authorization") 390→ request.timeoutInterval = 30 391→ 392→ let (data, response) = try await URLSession.shared.data(for: request) 393→ 394→ guard let httpResponse = response as? HTTPURLResponse, 395→ httpResponse.statusCode == 200 else { 396→ logger.warning("Failed to fetch models") 397→ return 398→ } 399→ 400→ let modelsResponse = try JSONDecoder().decode(ModelsResponse.self, from: data) 401→ availableModels = modelsResponse.data 402→ logger.info("Fetched \(modelsResponse.data.count) models") 403→ 404→ if let firstModel = availableModels.first { 405→ currentModel = firstModel.id 406→ } 407→ } catch { 408→ logger.error("Fetch models error: \(error.localizedDescription)") 409→ } 410→ } 411→ 412→ // MARK: - Send Message 413→ 414→ func sendMessage(_ content: String) async -> String? { 415→ guard !content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { 416→ return nil 417→ } 418→ 419→ isLoading = true 420→ lastError = nil 421→ 422→ // Add user message to history 423→ let userMessage = ChatMessage(role: .user, content: content) 424→ conversationHistory.append(userMessage) 425→ 426→ // Build messages with system prompt 427→ var allMessages = [ChatMessage(role: .system, content: selectedPersona.systemPrompt)] 428→ allMessages.append(contentsOf: conversationHistory) 429→ 430→ do { 431→ var request = URLRequest(url: LMGatewayConfig.chatCompletionsURL) 432→ request.httpMethod = "POST" 433→ request.setValue("Bearer \(LMGatewayConfig.apiKey)", forHTTPHeaderField: "Authorization") 434→ request.setValue("application/json", forHTTPHeaderField: "Content-Type") 435→ request.timeoutInterval = 120 436→ 437→ let chatRequest = ChatRequest(messages: allMessages, model: currentModel) 438→ request.httpBody = try JSONEncoder().encode(chatRequest) 439→ 440→ let (data, response) = try await URLSession.shared.data(for: request) 441→ 442→ guard let httpResponse = response as? HTTPURLResponse else { 443→ throw LMGatewayError.invalidResponse 444→ } 445→ 446→ if httpResponse.statusCode != 200 { 447→ let errorBody = String(data: data, encoding: .utf8) ?? "Unknown error" 448→ logger.error("API error: \(httpResponse.statusCode) - \(errorBody)") 449→ throw LMGatewayError.apiError(statusCode: httpResponse.statusCode, message: errorBody) 450→ } 451→ 452→ let chatResponse = try JSONDecoder().decode(ChatResponse.self, from: data) 453→ 454→ guard let assistantContent = chatResponse.choices.first?.message.content else { 455→ throw LMGatewayError.noContent 456→ } 457→ 458→ // Add assistant message to history 459→ let assistantMessage = ChatMessage(role: .assistant, content: assistantContent) 460→ conversationHistory.append(assistantMessage) 461→ 462→ // Update token usage 463→ if let usage = chatResponse.usage { 464→ tokensUsedToday += usage.total_tokens 465→ saveTokenUsage() 466→ } 467→ 468→ isLoading = false 469→ isConnected = true 470→ return assistantContent 471→ 472→ } catch { 473→ isLoading = false 474→ lastError = error.localizedDescription 475→ logger.error("Send message error: \(error.localizedDescription)") 476→ 477→ // Remove the user message if we failed 478→ if conversationHistory.last?.id == userMessage.id { 479→ conversationHistory.removeLast() 480→ } 481→ 482→ return nil 483→ } 484→ } 485→ 486→ // MARK: - Conversation Management 487→ 488→ func clearConversation() { 489→ conversationHistory.removeAll() 490→ logger.info("Conversation cleared") 491→ } 492→ 493→ func setPersona(_ persona: AIPersona) { 494→ if selectedPersona != persona { 495→ selectedPersona = persona 496→ clearConversation() 497→ } 498→ } 499→ 500→ // MARK: - Context Injection 501→ 502→ /// Legacy method for backward compatibility 503→ func injectContext(ritualCompletion: Double?, currentStreak: Int?, todayGoals: [String]?) { 504→ var contextParts: [String] = [] 505→ 506→ if let completion = ritualCompletion { 507→ contextParts.append("Ritual matinal: \(Int(completion * 100))% completo") 508→ } 509→ 510→ if let streak = currentStreak, streak > 0 { 511→ contextParts.append("Streak atual: \(streak) dias") 512→ } 513→ 514→ if let goals = todayGoals, !goals.isEmpty { 515→ contextParts.append("Objetivos de hoje: \(goals.joined(separator: ", "))") 516→ } 517→ 518→ if !contextParts.isEmpty { 519→ let contextMessage = ChatMessage( 520→ role: .system, 521→ content: "Contexto atual do usuário: \(contextParts.joined(separator: ". "))" 522→ ) 523→ conversationHistory.insert(contextMessage, at: 0) 524→ } 525→ } 526→ 527→ /// Enhanced context injection with comprehensive user data 528→ func injectFullContext(_ context: ZAIROSContext) { 529→ let contextMessage = ChatMessage( 530→ role: .system, 531→ content: buildContextPrompt(from: context) 532→ ) 533→ 534→ // Remove any existing context message at the start 535→ if conversationHistory.first?.role == .system { 536→ conversationHistory.removeFirst() 537→ } 538→ 539→ conversationHistory.insert(contextMessage, at: 0) 540→ logger.info("Full context injected with \(context.activeGoals.count) active goals") 541→ } 542→ 543→ /// Builds a comprehensive context prompt in Portuguese for ZAIROS 544→ private func buildContextPrompt(from context: ZAIROSContext) -> String { 545→ var sections: [String] = [] 546→ 547→ // User Profile Section 548→ sections.append(""" 549→ 📊 PERFIL DO USUÁRIO: 550→ • Nível: \(context.level) 551→ • Streak atual: \(context.currentStreak) dias 552→ • Maior streak: \(context.longestStreak) dias 553→ • Dias desde o início: \(context.daysSinceStart) 554→ • Taxa média de conclusão: \(Int(context.averageCompletionRate * 100))% 555→ """) 556→ 557→ // Today's Status Section 558→ let ritualStatus = context.ritualCompletion >= 1.0 ? "✅ Completo" : "\(Int(context.ritualCompletion * 100))%" 559→ sections.append(""" 560→ 🌅 STATUS DE HOJE: 561→ • Ritual matinal: \(ritualStatus) 562→ • Exercícios: \(context.exerciseProgress) 563→ • Meditação: \(context.meditationMinutes) minutos 564→ """) 565→ 566→ if !context.completedRitualItems.isEmpty { 567→ sections.append("• Itens completos: \(context.completedRitualItems.joined(separator: ", "))") 568→ } 569→ 570→ if !context.pendingRitualItems.isEmpty { 571→ sections.append("• Itens pendentes: \(context.pendingRitualItems.joined(separator: ", "))") 572→ } 573→ 574→ // Goals Section 575→ if !context.visionGoals.isEmpty { 576→ let visionTitles = context.visionGoals.map { "• \($0.title)" }.joined(separator: "\n") 577→ sections.append(""" 578→ 🎯 VISÃO (5-10 ANOS): 579→ \(visionTitles) 580→ """) 581→ } 582→ 583→ if !context.activeGoals.isEmpty { 584→ let activeTitles = context.activeGoals.map { goal -> String in 585→ let progress = Int(goal.progress * 100) 586→ return "• \(goal.title) [\(goal.timeframe)] - \(progress)%" 587→ }.joined(separator: "\n") 588→ sections.append(""" 589→ 📈 OBJETIVOS ATIVOS: 590→ \(activeTitles) 591→ """) 592→ } 593→ 594→ if !context.overdueGoals.isEmpty { 595→ let overdueTitles = context.overdueGoals.map { "⚠️ \($0.title)" }.joined(separator: "\n") 596→ sections.append(""" 597→ 🚨 OBJETIVOS ATRASADOS: 598→ \(overdueTitles) 599→ """) 600→ } 601→ 602→ // Journal Insights Section 603→ var journalParts: [String] = [] 604→ journalParts.append("📔 JOURNAL:") 605→ journalParts.append("• Entradas esta semana: \(context.journalEntriesThisWeek)") 606→ 607→ if let clarity = context.recentClarityLevel { 608→ journalParts.append("• Nível de clareza recente: \(clarity)") 609→ } 610→ 611→ if !context.recentTags.isEmpty { 612→ journalParts.append("• Temas recentes: \(context.recentTags.joined(separator: ", "))") 613→ } 614→ 615→ if let excerpt = context.lastJournalExcerpt { 616→ journalParts.append("• Última reflexão: \"\(excerpt)\"") 617→ } 618→ 619→ sections.append(journalParts.joined(separator: "\n")) 620→ 621→ // Trends Section 622→ sections.append(""" 623→ 📊 TENDÊNCIAS: 624→ • Streak: \(context.streakTrend) 625→ • Comparação semanal: \(context.weeklyComparison) 626→ """) 627→ 628→ // Total Stats Section 629→ sections.append(""" 630→ 🏆 TOTAIS ACUMULADOS: 631→ • \(context.totalPushups) flexões 632→ • \(context.totalSquats) agachamentos 633→ • \(context.totalMeditationMinutes) minutos de meditação 634→ • \(context.totalJournalEntries) entradas no journal 635→ """) 636→ 637→ return """ 638→ [CONTEXTO DO USUÁRIO - DADOS DO KORTEXOS] 639→ 640→ \(sections.joined(separator: "\n\n")) 641→ 642→ Use este contexto para personalizar suas respostas. Referencie os dados quando relevante para dar feedback específico e acionável. 643→ """ 644→ } 645→ 646→ // MARK: - Token Usage 647→ 648→ private func loadTokenUsage() { 649→ let savedDate = UserDefaults.standard.string(forKey: tokenDateKey) ?? "" 650→ let today = dateString(from: Date()) 651→ 652→ if savedDate == today { 653→ tokensUsedToday = UserDefaults.standard.integer(forKey: tokenUsageKey) 654→ } else { 655→ tokensUsedToday = 0 656→ UserDefaults.standard.set(today, forKey: tokenDateKey) 657→ UserDefaults.standard.set(0, forKey: tokenUsageKey) 658→ } 659→ } 660→ 661→ private func saveTokenUsage() { 662→ let today = dateString(from: Date()) 663→ UserDefaults.standard.set(today, forKey: tokenDateKey) 664→ UserDefaults.standard.set(tokensUsedToday, forKey: tokenUsageKey) 665→ } 666→ 667→ private func dateString(from date: Date) -> String { 668→ let formatter = DateFormatter() 669→ formatter.dateFormat = "yyyy-MM-dd" 670→ return formatter.string(from: date) 671→ } 672→ 673→ var tokenUsagePercentage: Double { 674→ Double(tokensUsedToday) / Double(LMGatewayConfig.dailyTokenLimit) 675→ } 676→ 677→ var remainingTokens: Int { 678→ max(0, LMGatewayConfig.dailyTokenLimit - tokensUsedToday) 679→ } 680→} 681→ 682→// MARK: - Errors 683→ 684→enum LMGatewayError: LocalizedError { 685→ case invalidResponse 686→ case apiError(statusCode: Int, message: String) 687→ case noContent 688→ case networkError(Error) 689→ 690→ var errorDescription: String? { 691→ switch self { 692→ case .invalidResponse: 693→ return "Resposta inválida do servidor" 694→ case .apiError(let code, let message): 695→ return "Erro da API (\(code)): \(message)" 696→ case .noContent: 697→ return "Nenhum conteúdo na resposta" 698→ case .networkError(let error): 699→ return "Erro de rede: \(error.localizedDescription)" 700→ } 701→ } 702→} 703→ Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.