16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
|
# File 'lib/vsm/drivers/openai/async_driver.rb', line 16
def run!(conversation:, tools:, policy: {}, &emit)
internet = Async::HTTP::Internet.new
begin
= {
"Authorization" => "Bearer #{@api_key}",
"Content-Type" => "application/json",
"Accept" => "text/event-stream"
}
messages = to_openai_messages(conversation, policy[:system_prompt])
tool_list = normalize_openai_tools(tools)
req_body = JSON.dump({
model: @model,
messages: messages,
tools: tool_list,
tool_choice: "auto",
stream: true
})
if ENV["VSM_DEBUG_STREAM"] == "1"
$stderr.puts "openai => messages: #{JSON.pretty_generate(messages)}"
$stderr.puts "openai => tools count: #{tool_list.size}"
end
response = internet.post("#{@base}/chat/completions", , req_body)
if response.status != 200
body = response.read
warn "openai HTTP #{response.status}: #{body}"
emit.call(:assistant_final, "")
return :done
end
buffer = +""
text_buffer = +""
tc_partial = Hash.new { |h,k| h[k] = { id: nil, name: nil, args_str: +"" } }
response.body.each do |chunk|
buffer << chunk
while (line = (buffer))
next if line.empty? || line.start_with?(":")
next unless line.start_with?("data:")
data = line.sub("data:","").strip
$stderr.puts("openai <= #{data}") if ENV["VSM_DEBUG_STREAM"] == "1"
next if data == "[DONE]"
obj = JSON.parse(data) rescue nil
next unless obj
choice = obj.dig("choices",0) || {}
delta = choice["delta"] || {}
if (content = delta["content"])
text_buffer << content
emit.call(:assistant_delta, content)
end
if (tcs = delta["tool_calls"])
tcs.each do |tc|
idx = tc["index"] || 0
cell = tc_partial[idx]
cell[:id] ||= tc["id"]
fn = tc["function"] || {}
cell[:name] ||= fn["name"] if fn["name"]
cell[:args_str] << (fn["arguments"] || "")
end
end
if (fr = choice["finish_reason"])
case fr
when "tool_calls"
calls = tc_partial.keys.sort.map do |i|
cell = tc_partial[i]
{
id: cell[:id] || "call_#{i}",
name: cell[:name] || "unknown_tool",
arguments: safe_json(cell[:args_str])
}
end
tc_partial.clear
emit.call(:tool_calls, calls)
when "stop", "length", "content_filter"
emit.call(:assistant_final, text_buffer.dup)
text_buffer.clear
end
end
end
end
ensure
internet.close
end
:done
end
|