mirror of
https://github.com/discourse/discourse.git
synced 2026-08-09 21:45:25 +08:00
Escape control characters only within JSON string values so pretty-printed formatting remains intact. Retain lexical state across streaming chunks to correctly handle split strings, escapes, and control characters.
57 lines
1.3 KiB
Ruby
Vendored
57 lines
1.3 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
module DiscourseAi
|
|
module Completions
|
|
class JsonStreamingTracker
|
|
attr_reader :stream_consumer
|
|
|
|
def initialize(stream_consumer)
|
|
@stream_consumer = stream_consumer
|
|
@escaped_buffer = +""
|
|
@completer = JsonCompleter.new
|
|
@broken = false
|
|
@last_notified = {}
|
|
@control_character_escaper = DiscourseAi::Utils::JsonControlCharacterEscaper.new
|
|
end
|
|
|
|
def broken?
|
|
@broken
|
|
end
|
|
|
|
def <<(raw_json)
|
|
return if @broken
|
|
|
|
if !raw_json.is_a?(String)
|
|
@broken = true
|
|
return
|
|
end
|
|
|
|
@escaped_buffer << @control_character_escaper.escape(raw_json)
|
|
|
|
parsed =
|
|
begin
|
|
@completer.parse(@escaped_buffer)
|
|
rescue JsonCompleter::ParseError
|
|
@broken = true
|
|
return
|
|
end
|
|
|
|
notify_changes(parsed) if parsed.is_a?(Hash)
|
|
end
|
|
|
|
private
|
|
|
|
def notify_changes(parsed)
|
|
parsed.each do |key, value|
|
|
next if value.nil?
|
|
next if @last_notified[key] == value
|
|
|
|
# the completer mutates parsed containers in place between calls, so
|
|
# compare against a snapshot
|
|
@last_notified[key] = value.deep_dup
|
|
stream_consumer.notify_progress(key, value)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|