549 lines
22 KiB
Swift
549 lines
22 KiB
Swift
//
|
||
// 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 ["这种忧郁感从何而来?", "有什么特别的回忆或想法吗?"]
|
||
}
|
||
}
|
||
} |