接收数据不完整的 url 请求错误

Receiving url request with incomplete data error

提问人:ahmed 提问时间:11/15/2023 最后编辑:halferahmed 更新时间:11/18/2023 访问量:44

问:

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

我将在下面包括 2 个示例。第一个使用基本的 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
}
Swift HTTP AlamoFire URL请求

评论


答:

1赞 VonC 11/15/2023 #1

第二种方法使用 Alamofire 库来请求数据。此方法有效,所有数据都完整返回。实际上,我想将 Almofire 用作我的主要方法(首先),因为它更强大,但我不确定如何中途取消流

我同意 Alamofire 使用指南中关于“取消和恢复下载”的内容 “更多的是取消下载,而不是流。

然而,基本方法适用于 Alamofire 中的所有请求类型,包括流式处理请求
您可以使用此方法取消正在进行的流式处理请求。
修改函数以返回对象。然后,可以将该对象存储在属性中以供以后访问。
并实现一个方法来取消存储的.
cancel()sendStreamMessageDataStreamRequestDataStreamRequest

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现在将 in 存储在 .然后,该方法取消此请求。
你可以这样使用它:
DataStreamRequestcurrentStreamRequestcancelStream

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

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

请注意,它不涉及恢复数据,因为它与文件下载更相关,其中可以保存部分数据并在以后恢复。对于来自 API 的数据流,除非 API 本身支持此类机制,否则恢复通常不可行。

评论

0赞 ahmed 11/16/2023
谢谢!这是我发布的另一个问题:stackoverflow.com/questions/77491756/......