|
|
|
@ -252,7 +252,8 @@ public class ChatApiService: NSObject { |
|
|
|
} |
|
|
|
|
|
|
|
/// 发送消息(流式输出) |
|
|
|
public func sendMessageStream(_ sessionId:String,messages: [[String: Any]]) { |
|
|
|
// MARK: - 流式请求实现 |
|
|
|
public func sendMessageStream(_ sessionId: String, messages: [[String: Any]]) { |
|
|
|
// 1. 立即中止旧会话 |
|
|
|
abortCurrentSession() |
|
|
|
|
|
|
|
@ -261,35 +262,32 @@ public class ChatApiService: NSObject { |
|
|
|
|
|
|
|
guard isInitialized && !apiKey.isEmpty, let openAI = openAI else { |
|
|
|
let error = ChatApiException("ChatAPI服务未初始化") |
|
|
|
getSessionCallback(sessionId)?.onError(sessionId,error) |
|
|
|
getSessionCallback(sessionId)?.onError(sessionId, error) |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 重置状态 |
|
|
|
// 保存当前消息和回调 |
|
|
|
currentMessages = messages |
|
|
|
toolCalls = [:] |
|
|
|
sessionCallbacks[sessionId] = getSessionCallback(sessionId) |
|
|
|
|
|
|
|
// 检查是否包含图片,决定使用哪个模型 |
|
|
|
var currentModel = model |
|
|
|
if containsImage(in: messages) { |
|
|
|
currentModel = visionModel |
|
|
|
} |
|
|
|
// 检查是否包含图片,决定模型 |
|
|
|
let currentModel = containsImage(in: messages) ? visionModel : model |
|
|
|
|
|
|
|
currentStreamTask = Task { [weak self] in |
|
|
|
guard let self = self else { return } |
|
|
|
var replyContext = "" // 收集AI回复内容 |
|
|
|
|
|
|
|
do { |
|
|
|
// 转换消息格式 |
|
|
|
let chatMessages = try self.convertToChatMessages(messages) |
|
|
|
|
|
|
|
// 获取MCP工具列表 |
|
|
|
// 获取工具列表 |
|
|
|
let tools = self.mcpClient?.getToolMaps() ?? [] |
|
|
|
|
|
|
|
// 构建请求 |
|
|
|
let query: ChatQuery |
|
|
|
if !tools.isEmpty { |
|
|
|
// 转换工具格式 |
|
|
|
let chatTools = self.convertToOpenAITools(tools) |
|
|
|
query = ChatQuery( |
|
|
|
messages: chatMessages, |
|
|
|
@ -310,16 +308,18 @@ public class ChatApiService: NSObject { |
|
|
|
let chatsStream: AsyncThrowingStream<ChatStreamResult, Error> = openAI.chatsStream(query: query) |
|
|
|
|
|
|
|
for try await result in chatsStream { |
|
|
|
// 检查会话是否已变更 |
|
|
|
if sessionId != self.currSessionId { |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
if let choice = result.choices.first { |
|
|
|
guard let choice = result.choices.first else { continue } |
|
|
|
let delta = choice.delta |
|
|
|
|
|
|
|
// 处理普通文本内容 |
|
|
|
// 处理文本内容 |
|
|
|
if let content = delta.content { |
|
|
|
getSessionCallback(sessionId)?.onToken(sessionId,content) |
|
|
|
replyContext += content |
|
|
|
self.getSessionCallback(sessionId)?.onToken(sessionId, content) |
|
|
|
} |
|
|
|
|
|
|
|
// 收集工具调用信息 |
|
|
|
@ -327,12 +327,12 @@ public class ChatApiService: NSObject { |
|
|
|
for toolCall in toolCalls { |
|
|
|
let index = toolCall.index |
|
|
|
|
|
|
|
// 创建或获取现有的工具调用信息 |
|
|
|
// 初始化工具调用信息 |
|
|
|
if self.toolCalls[index] == nil { |
|
|
|
self.toolCalls[index] = ToolCallInfo() |
|
|
|
} |
|
|
|
|
|
|
|
let toolCallInfo = self.toolCalls[index]! |
|
|
|
guard let toolCallInfo = self.toolCalls[index] else { continue } |
|
|
|
|
|
|
|
// 更新ID |
|
|
|
if let id = toolCall.id { |
|
|
|
@ -352,81 +352,168 @@ public class ChatApiService: NSObject { |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 流式响应结束 |
|
|
|
if sessionId == self.currSessionId { |
|
|
|
// 处理工具调用或完成 |
|
|
|
print("发送消息(流式输出)\(replyContext)") |
|
|
|
// 计算token使用量 |
|
|
|
let promptTokens = self.estimatePromptTokens(messages: self.currentMessages) |
|
|
|
let completionTokens = self.roughTokenCount(text: replyContext) |
|
|
|
let totalTokens = promptTokens + completionTokens |
|
|
|
self.getSessionCallback(sessionId)?.onUsage(sessionId, promptTokens, completionTokens, totalTokens) |
|
|
|
|
|
|
|
// 处理工具调用 |
|
|
|
let hasToolCalls = await self.processToolCalls(sessionId) |
|
|
|
if !hasToolCalls { |
|
|
|
getSessionCallback(sessionId)?.onComplete(sessionId) |
|
|
|
self.getSessionCallback(sessionId)?.onComplete(sessionId) |
|
|
|
self.clearCurrentSession() |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
} catch { |
|
|
|
if sessionId == self.currSessionId { |
|
|
|
let chatApiError = ChatApiException("流式请求失败: \(error.localizedDescription)") |
|
|
|
getSessionCallback(sessionId)?.onError(sessionId,chatApiError) |
|
|
|
let errorMessage = "流式请求失败: \(error.localizedDescription)" |
|
|
|
let chatApiError = ChatApiException(errorMessage) |
|
|
|
self.getSessionCallback(sessionId)?.onError(sessionId, chatApiError) |
|
|
|
self.clearCurrentSession() |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// MARK: - 新增辅助方法 |
|
|
|
private func estimatePromptTokens(messages: [[String: Any]]) -> Int { |
|
|
|
var totalText = "" |
|
|
|
for message in messages { |
|
|
|
if let content = message["content"] as? String { |
|
|
|
totalText += content + " " |
|
|
|
} else if let contentArray = message["content"] as? [[String: Any]] { |
|
|
|
for item in contentArray { |
|
|
|
if let type = item["type"] as? String, |
|
|
|
type == "text", |
|
|
|
let text = item["text"] as? String { |
|
|
|
totalText += text + " " |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
return roughTokenCount(text: totalText) |
|
|
|
} |
|
|
|
|
|
|
|
private func roughTokenCount(text: String) -> Int { |
|
|
|
let separators = CharacterSet.whitespacesAndNewlines.union(.punctuationCharacters) |
|
|
|
let tokens = text.components(separatedBy: separators).filter { !$0.isEmpty } |
|
|
|
return tokens.count |
|
|
|
} |
|
|
|
|
|
|
|
/// 处理工具调用 |
|
|
|
/// 处理工具调用 |
|
|
|
private func processToolCalls(_ sessionId:String) async -> Bool { |
|
|
|
guard let firstToolCall = toolCalls.values.first, firstToolCall.isValid() else { |
|
|
|
private func processToolCalls(_ sessionId:String) async -> Bool { |
|
|
|
// 验证工具调用集合不为空 |
|
|
|
if toolCalls.isEmpty { |
|
|
|
print("[Session: \(sessionId)] 没有工具调用需要处理") |
|
|
|
return false |
|
|
|
} |
|
|
|
|
|
|
|
// 输出所有收集到的工具调用信息(用于调试) |
|
|
|
print("[Session: \(sessionId)] 收集到 \(toolCalls.count) 个工具调用") |
|
|
|
for (index, toolCall) in toolCalls { |
|
|
|
print("[Session: \(sessionId)] 工具调用[\(index)]: name=\(toolCall.name), args.length=\(toolCall.arguments.count), args=\(toolCall.arguments.prefix(100))...") |
|
|
|
} |
|
|
|
|
|
|
|
// 只处理有效的工具调用 |
|
|
|
let validToolCalls = toolCalls.values.filter { $0.isValid() } |
|
|
|
if validToolCalls.isEmpty { |
|
|
|
print("[Session: \(sessionId)] 没有有效的工具调用(参数可能不完整)") |
|
|
|
return false |
|
|
|
} |
|
|
|
|
|
|
|
// 处理第一个有效的工具调用 |
|
|
|
let firstToolCall = validToolCalls.first! |
|
|
|
print("[Session: \(sessionId)] 处理工具调用: \(firstToolCall.name),参数: \(firstToolCall.arguments)") |
|
|
|
|
|
|
|
// 创建函数调用字典 |
|
|
|
let functionCall: [String: Any] = [ |
|
|
|
"name": firstToolCall.name, |
|
|
|
"arguments": firstToolCall.arguments, |
|
|
|
"id": firstToolCall.id |
|
|
|
] |
|
|
|
|
|
|
|
// 转换为JSON对象格式 |
|
|
|
let jsonFunctionCall = convertMapToJsonObject(functionCall) |
|
|
|
getSessionCallback(sessionId)?.onFunctionCall(sessionId, jsonFunctionCall) |
|
|
|
// 通知上层工具调用事件 |
|
|
|
getSessionCallback(sessionId)?.onFunctionCall(sessionId,functionCall) |
|
|
|
// getSessionCallback(sessionId)?.onFunctionCall(sessionId, functionCall) |
|
|
|
|
|
|
|
// 在后台处理工具调用 |
|
|
|
Task { [weak self] in |
|
|
|
guard let self = self else { return } |
|
|
|
|
|
|
|
// 在后台队列处理工具调用 |
|
|
|
Task { |
|
|
|
do { |
|
|
|
if sessionId == self.currSessionId { |
|
|
|
// 调用工具 |
|
|
|
let result: [String: Any] |
|
|
|
// 解析参数 |
|
|
|
let args = try self.parseJsonArguments(firstToolCall.arguments) |
|
|
|
|
|
|
|
// 通过MCP客户端处理工具调用 |
|
|
|
var result: [String: Any] = [:] |
|
|
|
|
|
|
|
if let client = self.mcpClient { |
|
|
|
result = await client.callTool(name: firstToolCall.name, arguments: args) ?? ["context": "工具调用失败"] |
|
|
|
// 调用MCP工具 |
|
|
|
if let toolResult = await client.callTool(name: firstToolCall.name, arguments: args) { |
|
|
|
print("[Session: \(sessionId)] AI调用工具结果: \(firstToolCall.name), 参数: \(args), 结果: \(toolResult)") |
|
|
|
|
|
|
|
// 统一结果格式 |
|
|
|
if toolResult["isError"] as? Bool == true { |
|
|
|
if let content = toolResult["content"] as? [[String: Any]], |
|
|
|
let firstContent = content.first, |
|
|
|
let errorText = firstContent["text"] as? String { |
|
|
|
result = ["context": errorText] |
|
|
|
} else { |
|
|
|
result = ["context": "Tool execution failed"] |
|
|
|
} |
|
|
|
} else if let context = toolResult["context"] { |
|
|
|
// 本地函数结果 |
|
|
|
result = ["context": context] |
|
|
|
} else if let content = toolResult["content"] as? [[String: Any]], |
|
|
|
let firstContent = content.first, |
|
|
|
let text = firstContent["text"] as? String { |
|
|
|
// MCP工具结果 |
|
|
|
result = ["context": text] |
|
|
|
} else { |
|
|
|
print("[Session: \(sessionId)] MCP工具调用返回无法解析的结果") |
|
|
|
result = ["context": "Tool call failed"] |
|
|
|
} |
|
|
|
} else { |
|
|
|
print("[Session: \(sessionId)] MCP工具调用返回nil") |
|
|
|
result = ["context": "Tool call failed"] |
|
|
|
} |
|
|
|
} else { |
|
|
|
result = ["context": "MCP客户端未初始化"] |
|
|
|
// 工具不存在 |
|
|
|
result = ["context": "Tool not found: \(firstToolCall.name)"] |
|
|
|
} |
|
|
|
|
|
|
|
if sessionId == self.currSessionId { |
|
|
|
// 处理结果 |
|
|
|
getSessionCallback(sessionId)?.onFunctionCallResult(sessionId,functionCall, result) |
|
|
|
|
|
|
|
// 将result转换为JSON字符串 |
|
|
|
let context: String |
|
|
|
do { |
|
|
|
let jsonData = try JSONSerialization.data(withJSONObject: result, options: .prettyPrinted) |
|
|
|
context = String(data: jsonData, encoding: .utf8) ?? "工具调用失败" |
|
|
|
} catch { |
|
|
|
context = "工具调用失败" |
|
|
|
} |
|
|
|
self.getSessionCallback(sessionId)?.onFunctionCallResult( |
|
|
|
sessionId, |
|
|
|
functionCall, |
|
|
|
result |
|
|
|
) |
|
|
|
|
|
|
|
// 将结果发送回OpenAI继续对话 |
|
|
|
await self.sendFunctionCallResultInternal( |
|
|
|
sessionId:sessionId, |
|
|
|
sessionId: sessionId, |
|
|
|
messages: self.currentMessages, |
|
|
|
functionCall: functionCall, |
|
|
|
functionResult: context |
|
|
|
functionResult: self.jsonToString(result) ?? "{}" |
|
|
|
) |
|
|
|
} |
|
|
|
} |
|
|
|
} catch { |
|
|
|
print("[Session: \(sessionId)] 工具调用处理过程中出错: \(error.localizedDescription)") |
|
|
|
if sessionId == self.currSessionId { |
|
|
|
let errorMessage = "工具调用处理失败: \(error.localizedDescription)" |
|
|
|
await self.sendFunctionCallResultInternal( |
|
|
|
sessionId:sessionId, |
|
|
|
sessionId: sessionId, |
|
|
|
messages: self.currentMessages, |
|
|
|
functionCall: functionCall, |
|
|
|
functionResult: errorMessage |
|
|
|
@ -436,8 +523,42 @@ public class ChatApiService: NSObject { |
|
|
|
} |
|
|
|
|
|
|
|
return true |
|
|
|
} |
|
|
|
|
|
|
|
/// 辅助方法:将字典转换为JSON字符串 |
|
|
|
private func jsonToString(_ json: [String: Any]) -> String? { |
|
|
|
do { |
|
|
|
let jsonData = try JSONSerialization.data(withJSONObject: json, options: []) |
|
|
|
return String(data: jsonData, encoding: .utf8) |
|
|
|
} catch { |
|
|
|
print("JSON序列化失败: \(error)") |
|
|
|
return nil |
|
|
|
} |
|
|
|
} |
|
|
|
// MARK: - JSON转换工具 |
|
|
|
private func convertMapToJsonObject(_ map: [String: Any]) -> [String: Any] { |
|
|
|
var result = [String: Any]() |
|
|
|
|
|
|
|
for (key, value) in map { |
|
|
|
if let nestedMap = value as? [String: Any] { |
|
|
|
result[key] = convertMapToJsonObject(nestedMap) |
|
|
|
} else if let array = value as? [Any] { |
|
|
|
var jsonArray = [Any]() |
|
|
|
for item in array { |
|
|
|
if let itemMap = item as? [String: Any] { |
|
|
|
jsonArray.append(convertMapToJsonObject(itemMap)) |
|
|
|
} else { |
|
|
|
jsonArray.append(item) |
|
|
|
} |
|
|
|
} |
|
|
|
result[key] = jsonArray |
|
|
|
} else { |
|
|
|
result[key] = value |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return result |
|
|
|
} |
|
|
|
/// 发送函数调用结果(内部方法) |
|
|
|
private func sendFunctionCallResultInternal( |
|
|
|
sessionId:String, |
|
|
|
|