prompt_builder 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +36 -0
  3. data/README.md +86 -17
  4. data/VERSION +1 -1
  5. data/lib/prompt_builder/errors.rb +9 -1
  6. data/lib/prompt_builder/response.rb +34 -2
  7. data/lib/prompt_builder/serializers/base.rb +55 -1
  8. data/lib/prompt_builder/serializers/chat_completion/request.rb +20 -6
  9. data/lib/prompt_builder/serializers/chat_completion/response.rb +3 -1
  10. data/lib/prompt_builder/serializers/chat_completion/stream_accumulator.rb +143 -0
  11. data/lib/prompt_builder/serializers/chat_completion.rb +1 -8
  12. data/lib/prompt_builder/serializers/converse/request.rb +195 -23
  13. data/lib/prompt_builder/serializers/converse/response.rb +35 -2
  14. data/lib/prompt_builder/serializers/converse/stream_accumulator.rb +151 -0
  15. data/lib/prompt_builder/serializers/converse.rb +1 -8
  16. data/lib/prompt_builder/serializers/gemini/request.rb +26 -8
  17. data/lib/prompt_builder/serializers/gemini/stream_accumulator.rb +188 -0
  18. data/lib/prompt_builder/serializers/gemini.rb +1 -8
  19. data/lib/prompt_builder/serializers/messages/request.rb +95 -33
  20. data/lib/prompt_builder/serializers/messages/stream_accumulator.rb +110 -0
  21. data/lib/prompt_builder/serializers/messages.rb +1 -8
  22. data/lib/prompt_builder/serializers/open_responses/request.rb +25 -0
  23. data/lib/prompt_builder/serializers/open_responses/stream_accumulator.rb +55 -0
  24. data/lib/prompt_builder/serializers/open_responses.rb +1 -8
  25. data/lib/prompt_builder/session.rb +49 -31
  26. data/lib/prompt_builder/streaming/accumulator.rb +169 -0
  27. data/lib/prompt_builder/streaming/event_stream_decoder.rb +137 -0
  28. data/lib/prompt_builder/streaming/sse_parser.rb +152 -0
  29. data/lib/prompt_builder/streaming.rb +14 -0
  30. data/lib/prompt_builder.rb +1 -0
  31. metadata +10 -1
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PromptBuilder
4
+ module Streaming
5
+ # Incremental parser for a Server-Sent Events (text/event-stream) body.
6
+ #
7
+ # Chunks can split the body anywhere, including in the middle of a line, a
8
+ # CRLF pair, or a multi-byte UTF-8 character; only the current incomplete
9
+ # line and the current event's fields are kept between calls. Lines may end
10
+ # with LF, CRLF, or CR. Comment lines (starting with +:+) and unknown fields
11
+ # are ignored, multiple +data+ lines are joined with newlines, and a leading
12
+ # byte order mark is skipped. Unlike a browser EventSource, a final event
13
+ # that is not followed by a blank line is still dispatched by #finish, since
14
+ # some servers close the stream right after the last event.
15
+ class SSEParser
16
+ # A dispatched event.
17
+ #
18
+ # @!attribute [r] event
19
+ # @return [String, nil] the +event+ field, or nil when the event has none
20
+ # or an empty one (both mean the default +message+ event type)
21
+ # @!attribute [r] data
22
+ # @return [String] the +data+ lines joined with newlines (UTF-8)
23
+ # @!attribute [r] id
24
+ # @return [String, nil] the most recent +id+ field seen in the stream
25
+ Event = Struct.new(:event, :data, :id)
26
+
27
+ BOM = "\xEF\xBB\xBF".b.freeze
28
+ private_constant :BOM
29
+
30
+ LINE_BREAK = /[\r\n]/n
31
+ private_constant :LINE_BREAK
32
+
33
+ CR = 13
34
+ private_constant :CR
35
+
36
+ LF = 10
37
+ private_constant :LF
38
+
39
+ def initialize
40
+ @buffer = +"".b
41
+ # Where to resume searching for a line break, so an incomplete line
42
+ # held across chunks is not rescanned from the start on every chunk.
43
+ @scan_from = 0
44
+ @bom_checked = false
45
+ @event_type = nil
46
+ @data_lines = []
47
+ @last_event_id = nil
48
+ end
49
+
50
+ # Parse a chunk of the body, yielding each event it completes.
51
+ #
52
+ # @param chunk [String] the next chunk of the response body
53
+ # @yieldparam event [Event] a completed event
54
+ # @return [self]
55
+ def feed(chunk, &block)
56
+ @buffer << chunk.b
57
+ return self unless skip_bom
58
+
59
+ events = []
60
+ pos = 0
61
+ while (index = @buffer.index(LINE_BREAK, [pos, @scan_from].max))
62
+ next_pos = index + 1
63
+ if @buffer.getbyte(index) == CR
64
+ # A CR at the end of the buffer may be the first half of a CRLF
65
+ # split across chunks; wait for the next chunk to decide.
66
+ break if next_pos == @buffer.bytesize
67
+ next_pos += 1 if @buffer.getbyte(next_pos) == LF
68
+ end
69
+ process_line(@buffer.byteslice(pos, index - pos), events)
70
+ pos = next_pos
71
+ end
72
+ @buffer = @buffer.byteslice(pos, @buffer.bytesize - pos) if pos > 0
73
+ # The rest of the buffer has no line break, except possibly a trailing
74
+ # CR, so the next search only needs to start at the last byte.
75
+ @scan_from = [@buffer.bytesize - 1, 0].max
76
+
77
+ events.each(&block)
78
+ self
79
+ end
80
+
81
+ # Signal the end of the body, yielding the final event if it was not
82
+ # followed by a blank line.
83
+ #
84
+ # @yieldparam event [Event] the final event
85
+ # @return [self]
86
+ def finish(&block)
87
+ @bom_checked = true
88
+ events = []
89
+ unless @buffer.empty?
90
+ line = @buffer.end_with?("\r") ? @buffer.byteslice(0, @buffer.bytesize - 1) : @buffer
91
+ process_line(line, events)
92
+ @buffer = +"".b
93
+ @scan_from = 0
94
+ end
95
+ dispatch(events)
96
+
97
+ events.each(&block)
98
+ self
99
+ end
100
+
101
+ private
102
+
103
+ # Strip a byte order mark from the start of the stream. Returns false
104
+ # while the buffer is too short to tell whether it starts with one.
105
+ def skip_bom
106
+ return true if @bom_checked
107
+
108
+ if @buffer.bytesize < BOM.bytesize && BOM.start_with?(@buffer)
109
+ return false
110
+ end
111
+
112
+ @buffer = @buffer.byteslice(BOM.bytesize, @buffer.bytesize) if @buffer.start_with?(BOM)
113
+ @bom_checked = true
114
+ end
115
+
116
+ def process_line(line, events)
117
+ if line.empty?
118
+ dispatch(events)
119
+ return
120
+ end
121
+ return if line.start_with?(":")
122
+
123
+ field, value = line.split(":", 2)
124
+ value = value.to_s
125
+ value = value.byteslice(1, value.bytesize) if value.start_with?(" ")
126
+
127
+ case field
128
+ when "event"
129
+ # An empty event field means the default "message" type, the same
130
+ # as no event field at all.
131
+ @event_type = value.empty? ? nil : utf8(value)
132
+ when "data"
133
+ @data_lines << value
134
+ when "id"
135
+ @last_event_id = utf8(value) unless value.include?("\0")
136
+ end
137
+ end
138
+
139
+ def dispatch(events)
140
+ unless @data_lines.empty?
141
+ events << Event.new(@event_type, utf8(@data_lines.join("\n")), @last_event_id)
142
+ end
143
+ @event_type = nil
144
+ @data_lines = []
145
+ end
146
+
147
+ def utf8(value)
148
+ value.dup.force_encoding(Encoding::UTF_8)
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PromptBuilder
4
+ # Building blocks for reassembling streamed API responses into the body the
5
+ # non-streaming endpoint would have returned. Each serializer defines a
6
+ # +StreamAccumulator+ subclass of Streaming::Accumulator for its format;
7
+ # use +Serializers::Base.stream_accumulator+, +Serializers::Base.collapse_stream+,
8
+ # or +Response.parse_stream+ rather than these classes directly.
9
+ module Streaming
10
+ autoload :Accumulator, File.expand_path("streaming/accumulator", __dir__)
11
+ autoload :EventStreamDecoder, File.expand_path("streaming/event_stream_decoder", __dir__)
12
+ autoload :SSEParser, File.expand_path("streaming/sse_parser", __dir__)
13
+ end
14
+ end
@@ -12,6 +12,7 @@ module PromptBuilder
12
12
  autoload :ToolRegistry, "prompt_builder/tool_registry"
13
13
  autoload :Usage, "prompt_builder/usage"
14
14
  autoload :Serializers, "prompt_builder/serializers"
15
+ autoload :Streaming, "prompt_builder/streaming"
15
16
  autoload :Tools, "prompt_builder/tools"
16
17
 
17
18
  VERSION = File.read(File.join(__dir__, "../VERSION")).strip
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: prompt_builder
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Durand
@@ -46,19 +46,28 @@ files:
46
46
  - lib/prompt_builder/serializers/chat_completion.rb
47
47
  - lib/prompt_builder/serializers/chat_completion/request.rb
48
48
  - lib/prompt_builder/serializers/chat_completion/response.rb
49
+ - lib/prompt_builder/serializers/chat_completion/stream_accumulator.rb
49
50
  - lib/prompt_builder/serializers/converse.rb
50
51
  - lib/prompt_builder/serializers/converse/request.rb
51
52
  - lib/prompt_builder/serializers/converse/response.rb
53
+ - lib/prompt_builder/serializers/converse/stream_accumulator.rb
52
54
  - lib/prompt_builder/serializers/gemini.rb
53
55
  - lib/prompt_builder/serializers/gemini/request.rb
54
56
  - lib/prompt_builder/serializers/gemini/response.rb
57
+ - lib/prompt_builder/serializers/gemini/stream_accumulator.rb
55
58
  - lib/prompt_builder/serializers/messages.rb
56
59
  - lib/prompt_builder/serializers/messages/request.rb
57
60
  - lib/prompt_builder/serializers/messages/response.rb
61
+ - lib/prompt_builder/serializers/messages/stream_accumulator.rb
58
62
  - lib/prompt_builder/serializers/open_responses.rb
59
63
  - lib/prompt_builder/serializers/open_responses/request.rb
60
64
  - lib/prompt_builder/serializers/open_responses/response.rb
65
+ - lib/prompt_builder/serializers/open_responses/stream_accumulator.rb
61
66
  - lib/prompt_builder/session.rb
67
+ - lib/prompt_builder/streaming.rb
68
+ - lib/prompt_builder/streaming/accumulator.rb
69
+ - lib/prompt_builder/streaming/event_stream_decoder.rb
70
+ - lib/prompt_builder/streaming/sse_parser.rb
62
71
  - lib/prompt_builder/tool_registry.rb
63
72
  - lib/prompt_builder/tools.rb
64
73
  - lib/prompt_builder/tools/definition.rb