@@ -41,7 +41,7 @@ defmodule ExOpenAI.StreamingClient do
41
41
end
42
42
43
43
def init ( stream_to: pid , convert_response_fx: fx ) do
44
- { :ok , % { stream_to: pid , convert_response_fx: fx } }
44
+ { :ok , % { stream_to: pid , convert_response_fx: fx , buffer: "" } }
45
45
end
46
46
47
47
@ doc """
@@ -57,6 +57,43 @@ defmodule ExOpenAI.StreamingClient do
57
57
callback_fx . ( data )
58
58
end
59
59
60
+ defp parse_lines ( lines , state ) do
61
+ # The last element might be incomplete JSON, which we keep.
62
+ # Everything that is valid JSON, we forward immediately.
63
+ { remaining_buffer , updated_state } =
64
+ Enum . reduce ( lines , { "" , state } , fn line , { partial_acc , st } ->
65
+ # Reconstruct the current attempt: partial data + the current line
66
+ attempt = ( partial_acc <> line ) |> String . trim ( )
67
+
68
+ cond do
69
+ attempt == "[DONE]" ->
70
+ Logger . debug ( "Received [DONE]" )
71
+ forward_response ( st . stream_to , :finish )
72
+ { "" , st }
73
+
74
+ attempt == "" ->
75
+ # Possibly just an empty line or leftover
76
+ { "" , st }
77
+
78
+ true ->
79
+ # Attempt to parse
80
+ case Jason . decode ( attempt ) do
81
+ { :ok , decoded } ->
82
+ # Once successfully decoded, forward, and reset partial buffer
83
+ message = st . convert_response_fx . ( { :ok , decoded } )
84
+ forward_response ( st . stream_to , { :data , message } )
85
+ { "" , st }
86
+
87
+ { :error , _ } ->
88
+ # Not valid JSON yet; treat entire attempt as partial
89
+ { attempt , st }
90
+ end
91
+ end
92
+ end )
93
+
94
+ { remaining_buffer , updated_state }
95
+ end
96
+
60
97
def handle_chunk (
61
98
chunk ,
62
99
% { stream_to: pid_or_fx , convert_response_fx: convert_fx }
@@ -103,20 +140,31 @@ defmodule ExOpenAI.StreamingClient do
103
140
{ :noreply , state }
104
141
end
105
142
106
- def handle_info (
107
- % HTTPoison.AsyncChunk { chunk: chunk } ,
108
- state
109
- ) do
110
- chunk
111
- |> String . trim ( )
112
- |> String . split ( "data:" )
113
- |> Enum . map ( & String . trim / 1 )
114
- |> Enum . filter ( & ( & 1 != "" ) )
115
- |> Enum . each ( fn subchunk ->
116
- handle_chunk ( subchunk , state )
117
- end )
143
+ # def handle_info(%HTTPoison.AsyncChunk{chunk: "data: " <> chunk_data}, state) do
144
+ # Logger.debug("Received AsyncChunk DATA: #{inspect(chunk_data)}")
145
+ # end
118
146
119
- { :noreply , state }
147
+ def handle_info ( % HTTPoison.AsyncChunk { chunk: chunk } , state ) do
148
+ Logger . debug ( "Received AsyncChunk (partial): #{ inspect ( chunk ) } " )
149
+
150
+ # Combine the existing buffer with the new chunk
151
+ new_buffer = state . buffer <> chunk
152
+
153
+ # Split by "data:" lines, but be mindful of partial JSON
154
+ lines =
155
+ new_buffer
156
+ |> String . split ( ~r/ data: / )
157
+
158
+ # The first chunk might still hold partial data from the end
159
+ # or the last chunk might be partial data at the end.
160
+
161
+ # We'll need a function that attempts to parse each line. If it fails, we store
162
+ # that line back to the buffer for the next chunk.
163
+ { remaining_buffer , state_after_parse } = parse_lines ( lines , state )
164
+
165
+ # Update the buffer in the new state
166
+ new_state = % { state_after_parse | buffer: remaining_buffer }
167
+ { :noreply , new_state }
120
168
end
121
169
122
170
def handle_info ( % HTTPoison.Error { reason: reason } , state ) do
0 commit comments