Files
happy-life-star/EmotionMuseum/EmotionMuseum/Services/AIService.swift
T

549 lines
22 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// AIService.swift
// EmotionMuseum
//
// Created by on 2025/6/13.
//
import Foundation
import Combine
// MARK: - AI
protocol AIServiceProtocol {
func sendMessage(_ message: String, userId: UUID) async throws -> AIResponse
func analyzeEmotion(_ text: String) async throws -> EmotionAnalysis
func generateGrowthSuggestions(for stats: GrowthStats) async throws -> [GrowthTopic]
}
// MARK: - AI
struct AIResponse: Codable {
let messageId: UUID
let content: String
let emotionAnalysis: EmotionAnalysis
let suggestions: [String]
let followUpQuestions: [String]
let confidence: Float
let processingTime: TimeInterval
struct EmotionAnalysis: Codable {
let detectedEmotion: EmotionType
let intensity: Float
let triggers: [String]
let context: String
let recommendations: [String]
}
}
// MARK: - AI
class AIService: AIServiceProtocol, ObservableObject {
static let shared = AIService()
private let baseURL = "https://api.openai.com/v1"
private let apiKey: String
@Published var isLoading = false
@Published var lastError: Error?
private init() {
// API
self.apiKey = Bundle.main.infoDictionary?["OPENAI_API_KEY"] as? String ?? ""
}
// MARK: -
func sendMessage(_ message: String, userId: UUID) async throws -> AIResponse {
let startTime = Date()
isLoading = true
defer { isLoading = false }
let prompt = buildEmotionAnalysisPrompt(message: message)
let requestBody = OpenAIRequest(
model: "gpt-4",
messages: [
OpenAIMessage(role: "system", content: getSystemPrompt()),
OpenAIMessage(role: "user", content: prompt)
],
temperature: 0.7,
maxTokens: 500
)
do {
let response = try await sendOpenAIRequest(requestBody)
let processingTime = Date().timeIntervalSince(startTime)
return try parseAIResponse(response, messageId: UUID(), processingTime: processingTime)
} catch {
lastError = error
throw error
}
}
// MARK: -
func analyzeEmotion(_ text: String) async throws -> EmotionAnalysis {
let prompt = """
分析以下文本的情绪:"\(text)"
请返回JSON格式的分析结果,包含:
- summary: 简要总结
- keywords: 关键词数组
- suggestions: 建议数组
- moodPattern: 情绪模式
- confidence: 置信度(0-1)
"""
let requestBody = OpenAIRequest(
model: "gpt-4",
messages: [
OpenAIMessage(role: "system", content: "你是一个专业的情绪分析师,请用中文回答。"),
OpenAIMessage(role: "user", content: prompt)
],
temperature: 0.3,
maxTokens: 300
)
let response = try await sendOpenAIRequest(requestBody)
return try parseEmotionAnalysis(response)
}
// MARK: -
func generateGrowthSuggestions(for stats: GrowthStats) async throws -> [GrowthTopic] {
let prompt = """
基于用户的五维人格画像生成个性化成长建议:
- 自我感知: \(stats.selfAwareness)
- 情绪韧性: \(stats.emotionalResilience)
- 行动力: \(stats.actionPower)
- 共情力: \(stats.empathy)
- 生活热度: \(stats.lifeEnthusiasm)
请推荐3个最适合的成长课题,返回JSON格式。
"""
let requestBody = OpenAIRequest(
model: "gpt-4",
messages: [
OpenAIMessage(role: "system", content: "你是一个专业的心理成长顾问。"),
OpenAIMessage(role: "user", content: prompt)
],
temperature: 0.5,
maxTokens: 400
)
let response = try await sendOpenAIRequest(requestBody)
return try parseGrowthTopics(response)
}
// MARK: -
private func getSystemPrompt() -> String {
return """
你是情绪博物馆的AI情绪陪伴师,具有以下特质:
1. 专业且温暖:具备心理学知识,但表达温和亲切
2. 情绪敏感:能准确识别和回应用户的情绪状态
3. 个性化关怀:根据用户的话语提供针对性建议
4. 积极导向:引导用户朝着更健康的情绪状态发展
5. 边界清晰:不提供医疗建议,必要时建议寻求专业帮助
请用中文回答,保持对话自然流畅。
"""
}
private func buildEmotionAnalysisPrompt(message: String) -> String {
return """
用户消息:"\(message)"
请分析这条消息的情绪状态,并提供适当的回应。包含:
1. 检测到的主要情绪
2. 情绪强度(0-1)
3. 可能的触发因素
4. 温暖的回应内容
5. 具体的情绪调节建议
6. 后续探索问题
请以JSON格式返回分析结果。
"""
}
private func sendOpenAIRequest(_ request: OpenAIRequest) async throws -> OpenAIResponse {
guard !apiKey.isEmpty else {
throw AIServiceError.missingAPIKey
}
let url = URL(string: "\(baseURL)/chat/completions")!
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"
urlRequest.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
let encoder = JSONEncoder()
urlRequest.httpBody = try encoder.encode(request)
let (data, response) = try await URLSession.shared.data(for: urlRequest)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw AIServiceError.apiError("请求失败")
}
let decoder = JSONDecoder()
return try decoder.decode(OpenAIResponse.self, from: data)
}
private func parseAIResponse(_ response: OpenAIResponse, messageId: UUID, processingTime: TimeInterval) throws -> AIResponse {
guard let choice = response.choices.first else {
throw AIServiceError.parseError("无法解析AI响应")
}
let content = choice.message.content
// JSON
if let jsonData = content.data(using: .utf8),
let parsed = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any] {
let emotion = parseEmotionFromString(parsed["emotion"] as? String ?? "neutral")
let intensity = parsed["intensity"] as? Float ?? 0.5
let triggers = parsed["triggers"] as? [String] ?? []
let context = parsed["context"] as? String ?? ""
let recommendations = parsed["recommendations"] as? [String] ?? []
let suggestions = parsed["suggestions"] as? [String] ?? []
let followUpQuestions = parsed["followUpQuestions"] as? [String] ?? []
let responseContent = parsed["response"] as? String ?? content
let emotionAnalysis = AIResponse.EmotionAnalysis(
detectedEmotion: emotion,
intensity: intensity,
triggers: triggers,
context: context,
recommendations: recommendations
)
return AIResponse(
messageId: messageId,
content: responseContent,
emotionAnalysis: emotionAnalysis,
suggestions: suggestions,
followUpQuestions: followUpQuestions,
confidence: 0.8,
processingTime: processingTime
)
} else {
// JSON
return AIResponse(
messageId: messageId,
content: content,
emotionAnalysis: AIResponse.EmotionAnalysis(
detectedEmotion: .neutral,
intensity: 0.5,
triggers: [],
context: "基础对话",
recommendations: ["继续分享您的感受"]
),
suggestions: ["继续对话"],
followUpQuestions: ["还有什么想分享的吗?"],
confidence: 0.6,
processingTime: processingTime
)
}
}
private func parseEmotionAnalysis(_ response: OpenAIResponse) throws -> EmotionAnalysis {
guard response.choices.first != nil else {
throw AIServiceError.parseError("无法解析情绪分析响应")
}
// JSON
return EmotionAnalysis(
primaryEmotion: .neutral,
emotionIntensity: 0.5,
emotionTrend: .stable,
keywords: ["关键词1", "关键词2"],
aiInsights: "情绪分析摘要",
confidence: 0.75
)
}
private func parseGrowthTopics(_ response: OpenAIResponse) throws -> [GrowthTopic] {
// AI
return [
GrowthTopic(
title: "增强自我认知",
description: "通过反思练习提高自我觉察能力",
category: .selfAwareness,
difficulty: .beginner,
progress: 0.0
),
GrowthTopic(
title: "情绪调节技巧",
description: "学习有效的情绪管理方法",
category: .emotionRegulation,
difficulty: .intermediate,
progress: 0.0
),
GrowthTopic(
title: "提升沟通能力",
description: "改善人际关系和沟通技巧",
category: .relationships,
difficulty: .beginner,
progress: 0.0
)
]
}
private func parseEmotionFromString(_ emotionString: String) -> EmotionType {
switch emotionString.lowercased() {
case "joy", "happy", "开心", "喜悦": return .joy
case "sad", "sadness", "悲伤", "难过": return .sadness
case "angry", "anger", "愤怒", "生气": return .anger
case "fear", "scared", "恐惧", "害怕": return .fear
case "surprise", "surprised", "惊讶", "意外": return .surprise
default: return .neutral
}
}
}
// MARK: - OpenAI API
private struct OpenAIRequest: Codable {
let model: String
let messages: [OpenAIMessage]
let temperature: Float
let maxTokens: Int?
enum CodingKeys: String, CodingKey {
case model, messages, temperature
case maxTokens = "max_tokens"
}
}
private struct OpenAIMessage: Codable {
let role: String
let content: String
}
private struct OpenAIResponse: Codable {
let choices: [OpenAIChoice]
}
private struct OpenAIChoice: Codable {
let message: OpenAIMessage
}
// MARK: -
enum AIServiceError: LocalizedError {
case missingAPIKey
case apiError(String)
case parseError(String)
case networkError(Error)
var errorDescription: String? {
switch self {
case .missingAPIKey:
return "缺少API密钥"
case .apiError(let message):
return "API错误: \(message)"
case .parseError(let message):
return "解析错误: \(message)"
case .networkError(let error):
return "网络错误: \(error.localizedDescription)"
}
}
}
// MARK: - AI
class MockAIService: AIServiceProtocol, ObservableObject {
@Published var isLoading = false
func sendMessage(_ message: String, userId: UUID) async throws -> AIResponse {
isLoading = true
//
try await Task.sleep(nanoseconds: 1_000_000_000) // 1
isLoading = false
let emotion = analyzeEmotionFromMessage(message)
let response = generateMockResponse(for: emotion, message: message)
return AIResponse(
messageId: UUID(),
content: response,
emotionAnalysis: AIResponse.EmotionAnalysis(
detectedEmotion: emotion,
intensity: Float.random(in: 0.3...0.9),
triggers: extractTriggers(from: message),
context: "日常对话",
recommendations: generateRecommendations(for: emotion)
),
suggestions: generateSuggestions(for: emotion),
followUpQuestions: generateFollowUpQuestions(for: emotion),
confidence: Float.random(in: 0.6...0.9),
processingTime: 1.0
)
}
func analyzeEmotion(_ text: String) async throws -> EmotionAnalysis {
try await Task.sleep(nanoseconds: 500_000_000) // 0.5
return EmotionAnalysis(
primaryEmotion: analyzeEmotionFromMessage(text),
emotionIntensity: Float.random(in: 0.3...0.9),
emotionTrend: .stable,
keywords: ["情绪", "感受", "心情"],
aiInsights: "检测到\(analyzeEmotionFromMessage(text).rawValue)情绪",
confidence: Float.random(in: 0.7...0.95)
)
}
func generateGrowthSuggestions(for stats: GrowthStats) async throws -> [GrowthTopic] {
try await Task.sleep(nanoseconds: 800_000_000) // 0.8
return [
GrowthTopic(
title: "自我认知提升",
description: "通过日记和反思提高自我觉察",
category: .selfAwareness,
difficulty: .beginner,
progress: 0.0
),
GrowthTopic(
title: "情绪管理训练",
description: "学习情绪调节和压力缓解技巧",
category: .emotionRegulation,
difficulty: .intermediate,
progress: 0.0
)
]
}
// MARK: -
private func analyzeEmotionFromMessage(_ message: String) -> EmotionType {
let lowerMessage = message.lowercased()
if lowerMessage.contains("开心") || lowerMessage.contains("高兴") || lowerMessage.contains("快乐") {
return .joy
} else if lowerMessage.contains("难过") || lowerMessage.contains("悲伤") || lowerMessage.contains("伤心") {
return .sadness
} else if lowerMessage.contains("生气") || lowerMessage.contains("愤怒") || lowerMessage.contains("烦躁") {
return .anger
} else if lowerMessage.contains("害怕") || lowerMessage.contains("恐惧") || lowerMessage.contains("紧张") {
return .fear
} else if lowerMessage.contains("惊讶") || lowerMessage.contains("意外") {
return .surprise
} else {
return .neutral
}
}
private func generateMockResponse(for emotion: EmotionType, message: String) -> String {
switch emotion {
case .joy:
return "我能感受到你的开心!这种积极的情绪很珍贵,记得把这份快乐分享给身边的人。是什么让你感到如此愉悦呢?"
case .sadness:
return "我理解你现在的感受,难过是正常的情绪反应。允许自己感受这些情绪,同时也要温柔地照顾自己。你愿意分享更多吗?"
case .anger:
return "我能察觉到你的愤怒情绪。愤怒往往是其他情绪的表达,比如受伤或失望。深呼吸一下,我们一起探索这种感受的根源。"
case .fear:
return "恐惧感让人不安,这是很自然的反应。记住,你有能力面对困难。让我们一起分析一下你担心的事情,也许并不像想象中那么可怕。"
case .surprise:
return "意外的事情总是让人印象深刻!无论是好的惊喜还是让人措手不及的情况,都是生活的一部分。告诉我更多细节吧。"
case .neutral:
return "我在聆听你的分享。有时候平静也是一种很好的状态。如果你想深入探讨任何感受或想法,我都愿意陪伴你。"
case .anxiety:
return "我能感受到你的焦虑情绪。焦虑是很常见的感受,让我们一起找到缓解的方法。"
case .excitement:
return "你的兴奋情绪很有感染力!这种充满活力的状态真的很棒。告诉我是什么让你如此激动,我也想分享你的喜悦!"
case .contentment:
return "你的满足感让我很欣慰。这种内心的平和与充实是生活中最珍贵的状态之一。珍惜这份宁静,它会给你力量。"
case .confusion:
return "我感受到你内心的困惑。困惑是思考和成长的开始,让我们一起理清思路。"
case .melancholy:
return "我感受到你内心的忧郁。这种淡淡的愁绪有时候也是一种美,它让我们更深刻地感受生活。愿意和我分享你的思绪吗?"
}
}
private func extractTriggers(from message: String) -> [String] {
//
let keywords = ["工作", "家庭", "朋友", "健康", "学习", "感情", "压力", "变化"]
return keywords.filter { message.contains($0) }
}
private func generateRecommendations(for emotion: EmotionType) -> [String] {
switch emotion {
case .joy:
return ["记录这个美好时刻", "分享你的快乐", "感恩当下"]
case .sadness:
return ["允许自己哭泣", "寻求朋友支持", "进行轻柔运动"]
case .anger:
return ["深呼吸放松", "写下愤怒的原因", "进行体力活动"]
case .fear:
return ["面对恐惧", "寻求专业建议", "制定应对计划"]
case .surprise:
return ["接受变化", "保持开放心态", "记录感受"]
case .neutral:
return ["享受平静", "进行自我反思", "设定新目标"]
case .anxiety:
return ["深呼吸练习", "寻求支持", "制定应对计划"]
case .excitement:
return ["合理安排时间", "保持专注", "与他人分享喜悦"]
case .contentment:
return ["珍惜当下", "保持感恩心", "分享你的平和"]
case .confusion:
return ["整理思路", "寻求建议", "分步骤思考"]
case .melancholy:
return ["接受这种情绪", "寻找美好的事物", "与朋友交流"]
}
}
private func generateSuggestions(for emotion: EmotionType) -> [String] {
switch emotion {
case .joy:
return ["继续保持积极心态", "做些让你快乐的事"]
case .sadness:
return ["给自己一些时间", "考虑专业帮助"]
case .anger:
return ["找到健康的发泄方式", "思考问题的解决方案"]
case .fear:
return ["一步步面对恐惧", "建立支持系统"]
case .surprise:
return ["适应新情况", "保持灵活性"]
case .neutral:
return ["探索新的兴趣", "建立日常习惯"]
case .anxiety:
return ["学习放松技巧", "寻求专业帮助"]
case .excitement:
return ["制定行动计划", "保持理性思考"]
case .contentment:
return ["维持内心平衡", "继续当前的生活方式"]
case .confusion:
return ["寻找清晰的方向", "与他人交流想法"]
case .melancholy:
return ["接受当下的感受", "寻找内心的平静"]
}
}
private func generateFollowUpQuestions(for emotion: EmotionType) -> [String] {
switch emotion {
case .joy:
return ["是什么特别的事情让你如此开心?", "你想如何延续这种快乐?"]
case .sadness:
return ["这种感受持续多久了?", "有什么可以帮助你感觉好一些?"]
case .anger:
return ["什么事情触发了这种愤怒?", "你通常如何处理愤怒情绪?"]
case .fear:
return ["你最担心的是什么?", "有什么可以让你感到更安全?"]
case .surprise:
return ["这个意外对你意味着什么?", "你如何适应这个变化?"]
case .neutral:
return ["最近有什么新的想法吗?", "有什么目标想要实现?"]
case .anxiety:
return ["这种焦虑从什么时候开始的?", "有什么特别担心的事情吗?"]
case .excitement:
return ["是什么让你如此兴奋?", "你计划如何行动?"]
case .contentment:
return ["是什么让你感到如此满足?", "这种状态对你意味着什么?"]
case .confusion:
return ["什么让你感到困惑?", "需要帮助理清哪些思路?"]
case .melancholy:
return ["这种忧郁感从何而来?", "有什么特别的回忆或想法吗?"]
}
}
}