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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +36 -0
- data/README.md +86 -17
- data/VERSION +1 -1
- data/lib/prompt_builder/errors.rb +9 -1
- data/lib/prompt_builder/response.rb +34 -2
- data/lib/prompt_builder/serializers/base.rb +55 -1
- data/lib/prompt_builder/serializers/chat_completion/request.rb +20 -6
- data/lib/prompt_builder/serializers/chat_completion/response.rb +3 -1
- data/lib/prompt_builder/serializers/chat_completion/stream_accumulator.rb +143 -0
- data/lib/prompt_builder/serializers/chat_completion.rb +1 -8
- data/lib/prompt_builder/serializers/converse/request.rb +195 -23
- data/lib/prompt_builder/serializers/converse/response.rb +35 -2
- data/lib/prompt_builder/serializers/converse/stream_accumulator.rb +151 -0
- data/lib/prompt_builder/serializers/converse.rb +1 -8
- data/lib/prompt_builder/serializers/gemini/request.rb +26 -8
- data/lib/prompt_builder/serializers/gemini/stream_accumulator.rb +188 -0
- data/lib/prompt_builder/serializers/gemini.rb +1 -8
- data/lib/prompt_builder/serializers/messages/request.rb +95 -33
- data/lib/prompt_builder/serializers/messages/stream_accumulator.rb +110 -0
- data/lib/prompt_builder/serializers/messages.rb +1 -8
- data/lib/prompt_builder/serializers/open_responses/request.rb +25 -0
- data/lib/prompt_builder/serializers/open_responses/stream_accumulator.rb +55 -0
- data/lib/prompt_builder/serializers/open_responses.rb +1 -8
- data/lib/prompt_builder/session.rb +49 -31
- data/lib/prompt_builder/streaming/accumulator.rb +169 -0
- data/lib/prompt_builder/streaming/event_stream_decoder.rb +137 -0
- data/lib/prompt_builder/streaming/sse_parser.rb +152 -0
- data/lib/prompt_builder/streaming.rb +14 -0
- data/lib/prompt_builder.rb +1 -0
- 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
|
data/lib/prompt_builder.rb
CHANGED
|
@@ -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.
|
|
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
|