我在SWIFT中制作了一个简单的ChatGPT应用程序,它以流的形式从API请求数据.API返回正确的数据,但当我收到它时,单词和字符都丢失了.我试着调试,但我不确定为什么url请求没有返回完整的数据.

我将在下面包括两个例子.第一种方法使用一个基本的HTTP请求来获取响应.这就是我正在经历这个错误的那个.我想保留这种方法,因为它使我能够取消任务.当使用第二种方法时,我不确定如何取消任务.第二种方法使用Almofire库来请求数据.这种方法是有效的,并且所有数据都返回完整.实际上,我喜欢使用Almofire作为我的主方法(首先),因为它更健壮,但我不确定如何在中途取消一条流的能力.我感谢一些关于如何不完全返回数据的洞察力.

第一种方法(错误)

func sendMessageStream(Question_To_Be_Asked: String) async throws -> AsyncThrowingStream<String, Error> {
    var urlRequest = self.urlRequest
    urlRequest.httpBody = try jsonBody(text: Question_To_Be_Asked)
    
    let (result, response) = try await urlSession.bytes(for: urlRequest)
    try Task.checkCancellation()
    
    guard let httpResponse = response as? HTTPURLResponse else {
        throw "Invalid response"
    }
    
    guard 200...299 ~= httpResponse.statusCode else {
        var errorText = ""
        for try await line in result.lines {
            try Task.checkCancellation()
            errorText += line
        }
        
        if let data = errorText.data(using: .utf8), let errorResponse = try? jsonDecoder.decode(ErrorRootResponse.self, from: data).error {
            errorText = "\n\(errorResponse.message)"
        }
        
        throw "Bad Response: \(httpResponse.statusCode), \(errorText)"
    }
    
    var responseText = ""
    return AsyncThrowingStream { [weak self] in
        guard let self else { return nil }
        for try await line in result.lines {
            //print(line) <- incomplete data
            try Task.checkCancellation()
            if line.hasPrefix("data: "), let data = line.dropFirst(6).data(using: .utf8), let response = try? self.jsonDecoder.decode(StreamCompletionResponse.self, from: data), let text = response.choices.first?.delta.content {
                
                responseText += text
                return text
            }
        }
        return nil
    }
}

第二种方法(工作)

func sendStreamMessage(messages: [Message]) -> DataStreamRequest{
    let openAIMessages = messages.map({OpenAIChatMessage(role: $0.role, content: $0.content)})
    let body = OpenAIChatBody(model: "gpt-4", messages: openAIMessages, stream: true)
    let headers: HTTPHeaders = [
        "Authorization": "Bearer \(Constants.openAIApiKey)"
    ]
    
    return AF.streamRequest(endpointUrl, method: .post, parameters: body, encoder: .json, headers: headers)
}

func sendMessage(question: String)  {
    let messages = [Message(id: UUID().uuidString, role: .user, content: question, createAt: Date())]
    currentInput = ""
    
    sendStreamMessage(messages: messages).responseStreamString { [weak self] stream in
        guard let self = self else { return }
        switch stream.event {
        case .stream(let response):
            switch response {
            case .success(let string):
                let streamResponse = self.parseStreamData(string)
                
                streamResponse.forEach { newMessageResponse in
                    guard let messageContent = newMessageResponse.choices.first?.delta.content else {
                        return
                    }
                    //here messageContent is final complete string from stream
                }
            case .failure(_):
                print("Something failes")
            }
            print(response)
        case .complete(_):
            print("COMPLETE")
        }
    }
}

func parseStreamData(_ data: String) ->[ChatStreamCompletionResponse] {
    let responseStrings = data.split(separator: "data:").map({$0.trimmingCharacters(in: .whitespacesAndNewlines)}).filter({!$0.isEmpty})
    let jsonDecoder = JSONDecoder()
    
    return responseStrings.compactMap { jsonString in
        guard let jsonData = jsonString.data(using: .utf8), let streamResponse = try? jsonDecoder.decode(ChatStreamCompletionResponse.self, from: jsonData) else {
            return nil
        }
        return streamResponse
    }
}

struct ChatStreamCompletionResponse: Decodable {
    let id: String
    let choices: [ChatStreamChoice]
}

struct ChatStreamChoice: Decodable {
    let delta: ChatStreamContent
}

struct ChatStreamContent: Decodable {
    let content: String
}

struct Message: Decodable, Hashable {
    let id: String
    let role: SenderRole
    let content: String
    let createAt: Date
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
}

struct OpenAIChatBody: Encodable {
    let model: String
    let messages: [OpenAIChatMessage]
    let stream: Bool
}

struct OpenAIChatMessage: Codable {
    let role: SenderRole
    let content: String
}

enum SenderRole: String, Codable {
    case system
    case user
    case assistant
}

推荐答案

第二种方法使用AlamoFire库来请求数据.这种方法是有效的,并且所有数据都返回完整.实际上,我希望将Almofire用于My Main方法(首先),因为它更健壮,但我不确定如何使用中途取消流的能力

我同意,Alamofire Usage Guide on "Canceling and Resuming a Download "更多的是取消下载,而不是流媒体.

Yet, the basic cancel() method is available for all request types in Alamofire, including streaming requests.
You could use this method to cancel an ongoing streaming request.
Modify the sendStreamMessage function to return the DataStreamRequest object. That object can then be stored in a property for later access
And implement a method to cancel the stored DataStreamRequest.

class ChatService {
    private var currentStreamRequest: DataStreamRequest?

    func sendStreamMessage(messages: [Message]) -> DataStreamRequest {
        // Existing code to set up and start the stream request

        let streamRequest = AF.streamRequest(endpointUrl, method: .post, parameters: body, encoder: .json, headers: headers)
        currentStreamRequest = streamRequest
        return streamRequest
    }

    func cancelStream() {
        currentStreamRequest?.cancel()
        currentStreamRequest = nil
    }

    // other methods
}

sendStreamMessage now stores the DataStreamRequest in currentStreamRequest. The cancelStream method then cancels this request.
You would use it like this:

let chatService = ChatService()
// Start streaming
let streamRequest = chatService.sendStreamMessage(messages: messages)

// Cancel the stream when needed
chatService.cancelStream()

请注意,它不涉及恢复数据,因为它与文件下载更相关,其中部分数据可以保存并在以后恢复.在来自API的数据流的情况下,恢复通常是不可行的,除非API本身支持这样的机制.

Swift相关问答推荐

Swift Tree实现中的弱var

SwiftData:线程1:&Quot;NSFetchRequest找不到实体名称';提醒&q;的NSEntityDescription

Swift:iVar + Equatable上的协议约束

如果通过计时器循环运行,则检索CPU利用率百分比的SWIFT脚本运行良好.似乎在没有计时器的情况下停留在初始百分比上

如何偏移HStack中的视图,但仍然约束框架以与偏移匹配?

快速并行读取进程 standardOutput 和 standardError 而不会阻塞

使用`JSONSerialiser`时,省略通用可选项

在 RealityKit 中查找特定模型的 findEntity(named:) 方法的替代方法?

如何修改下面的代码,使图像 Select 的顺序与使用快速并发的图像呈现的顺序相匹配?

在 struct 中的序列和非序列泛型函数之间进行 Select

如何判断一个值是否为 Int 类型

Pod lib lint 命令找不到 watchos 模拟器

如何在 SwiftUI 中用图像替换占位符文本?

如何更改 Picker 的边框 colored颜色

在 Select 器上显示alert Select SwiftUI

Subclass.fetchRequest() Swift 3.0,扩展并没有真正帮助 100%?

在运行时访问 UIView 宽度

关闭 UITableViewRowAction

如何以编程方式读取 wkwebview 的控制台日志(log)

如何在 iOS 上的 Swift 中获取 sharedApplication?