Skip to content

Commit b8da30b

Browse files
author
Nicholas Newberry
committed
switch to chat api
1 parent 1c8d9b6 commit b8da30b

15 files changed

Lines changed: 270 additions & 134 deletions

File tree

app/lib/llm_clients/client.rb

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,6 @@ def initialize(
4141

4242
private
4343

44-
def uri(path)
45-
URI("#{@endpoint}#{"/" unless path.starts_with?("/") || @endpoint.ends_with?("/")}#{path}")
46-
end
47-
48-
def headers
49-
headers = { "Content-Type": "application/json" }
50-
headers[:Authorization] = "Bearer #{@api_key}" if @api_key
51-
52-
headers
53-
end
54-
5544
def context(prompt, model)
5645
template = template_for(model)
5746

app/lib/llm_clients/ollama/chat.rb

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,42 @@
11
module LlmClients
22
module Ollama
33
class Chat
4-
def initialize(conversation)
5-
@conversation = conversation
4+
def initialize(messages)
5+
@messages = messages
66
end
77

8-
def prompt
9-
{
10-
model: @conversation.model_config.model,
11-
messages:,
12-
}
8+
def chat_history
9+
@chat_history ||= generate_chat_history
1310
end
1411

1512
private
1613

17-
def messages
18-
@conversation.messages.map do |message|
14+
def generate_chat_history
15+
# use message content only for all messages but the last
16+
messages = conversation_messages[..-2].map do |message|
1917
{
2018
role: role_for_message(message),
2119
content: message.content,
2220
}
2321
end
22+
23+
# use the augmented prompt for the last message
24+
last_message = conversation_messages.last
25+
messages << {
26+
role: role_for_message(last_message),
27+
content: last_message.prompt || last_message.content,
28+
}
2429
end
2530

2631
def role_for_message(message)
2732
return nil if message.author.nil?
2833

2934
message.author.is_a?(User) ? "user" : "assistant"
3035
end
36+
37+
def conversation_messages
38+
@conversation_messages ||= @messages.sort
39+
end
3140
end
3241
end
3342
end

app/lib/llm_clients/ollama/client.rb

Lines changed: 40 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,35 @@ module Ollama
33
class Client < LlmClients::Client
44
NETWORK_TIMEOUT = 8
55

6+
def complete(prompt, &)
7+
request = helper.completion_request(prompt)
8+
9+
stream(request, &)
10+
end
11+
12+
def chat(chat, &)
13+
request = helper.chat_request(chat.chat_history)
14+
15+
stream(request, &)
16+
end
17+
18+
def embed(content)
19+
request = helper.embed_request(content)
20+
21+
request(request)
22+
end
23+
24+
private
25+
26+
def helper
27+
@helper ||= RequestHelper.new(@endpoint, @api_key, @embedding_model, @model, @temperature)
28+
end
29+
630
# rubocop:disable all
7-
def complete(prompt, &block)
31+
def stream(request, &block)
832
@stats = new_stats
9-
request = completion_request(prompt)
1033

34+
Rails.logger.info("Sending request body:\n#{request.body}")
1135
# TODO: switch to HTTParty for this?
1236
Net::HTTP.start(@uri.hostname, @uri.port, use_ssl: @uri.scheme == "https") do |http|
1337
stats[:start_time] = current_time
@@ -49,45 +73,36 @@ def complete(prompt, &block)
4973
end
5074
end
5175
end
52-
# rubocop:enable all
5376

54-
def embed(prompt)
77+
def request(request)
5578
response = with_retries do
56-
response = HTTParty.post(uri(embedding_path), headers:, timeout: NETWORK_TIMEOUT, body: {
57-
model: @embedding_model,
58-
prompt:,
59-
}.to_json)
79+
response = Net::HTTP.start(@uri.hostname, @uri.port, use_ssl: @uri.scheme == "https") do |http|
80+
http.request(request)
81+
end
6082

61-
raise RetryableError if response.code == 500
83+
# sometimes ollama just returns a 500 on an embed request when running locally, then is fine
84+
raise RetryableError if response.code == "500"
6285

6386
response
6487
end
6588

66-
raise response_error_for(response) unless response.success?
89+
raise response_error_for(response) unless response.kind_of?(Net::HTTPSuccess)
6790

68-
response.parsed_response
91+
parse_response(response)
6992
rescue SocketError
7093
raise ResponseError.new("Unable to connect to the active server", [ModelServer.active_server.url])
7194
end
95+
# rubocop:enable all
7296

73-
private
74-
75-
def completion_request(prompt)
76-
request = Net::HTTP::Post.new(uri(completion_path), **headers)
77-
request.body = {
78-
model: @model,
79-
prompt:,
80-
temperature: @temperature,
81-
stream: true,
82-
max_tokens: 200,
83-
}.to_json
84-
85-
request
97+
def parse_response(http_response)
98+
JSON.parse(http_response.body)
99+
rescue JSON::ParserError
100+
http_response.body
86101
end
87102

88103
def extract_message(response_string)
89104
response_hash = JSON.parse(response_string)
90-
message = response_hash["response"]
105+
message = response_hash["response"] || response_hash["message"]["content"]
91106
Rails.logger.debug { "<== '#{message}'" }
92107

93108
message
@@ -103,14 +118,6 @@ def template_for(model)
103118
{ prefix: "", suffix: "" }
104119
end
105120

106-
def completion_path
107-
"api/generate"
108-
end
109-
110-
def embedding_path
111-
"api/embeddings"
112-
end
113-
114121
def formatter
115122
@formatter ||= FORMATTERS.find { |key, _value| key.match?(@model) ? value : nil }
116123
end
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
module LlmClients
2+
module Ollama
3+
class RequestHelper
4+
def initialize(endpoint, api_key, embedding_model, model, temperature)
5+
@endpoint = endpoint
6+
@api_key = api_key
7+
@embedding_model = embedding_model
8+
@model = model
9+
@temperature = temperature
10+
end
11+
12+
def embed_request(content)
13+
request = Net::HTTP::Post.new(uri(embedding_path), **headers)
14+
request.body = { model: @embedding_model, prompt: content }.to_json
15+
16+
request
17+
end
18+
19+
def completion_request(prompt)
20+
request = Net::HTTP::Post.new(uri(completion_path), **headers)
21+
request.body = { model: @model, prompt:, temperature: @temperature, stream: true, max_tokens: 200 }.to_json
22+
23+
request
24+
end
25+
26+
def chat_request(messages)
27+
request = Net::HTTP::Post.new(uri(chat_path), **headers)
28+
request.body = { model: @model, messages: }.to_json
29+
30+
request
31+
end
32+
33+
private
34+
35+
def uri(path)
36+
URI("#{@endpoint}#{"/" unless path.starts_with?("/") || @endpoint.ends_with?("/")}#{path}")
37+
end
38+
39+
def headers
40+
headers = { "Content-Type": "application/json" }
41+
headers[:Authorization] = "Bearer #{@api_key}" if @api_key
42+
43+
headers
44+
end
45+
46+
def completion_path
47+
"api/generate"
48+
end
49+
50+
def embedding_path
51+
"api/embeddings"
52+
end
53+
54+
def chat_path
55+
"api/chat"
56+
end
57+
end
58+
end
59+
end

app/services/prompt_augmentor.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
class PromptAugmentor
2+
def initialize(message, search_hits)
3+
@message = message
4+
@search_hits = search_hits
5+
end
6+
7+
def prompt
8+
@prompt ||= begin
9+
prompt = "Here is some context that may help you answer the following question:\n\n"
10+
@search_hits.each do |hit|
11+
prompt << "#{hit.chunk.content}\n\n"
12+
end
13+
14+
prompt << "Question: #{@message.content}"
15+
end
16+
end
17+
end

app/services/reply_to_message.rb

Lines changed: 35 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,22 @@ class ReplyToMessage
77

88
def initialize(message)
99
@message = message
10+
@conversation = @message.conversation
1011
end
1112

1213
def execute
13-
search if collections_to_search.any?
14+
augment_message_prompt
15+
chat.chat_history
1416

1517
@reply = Message.create!(
1618
content: "",
17-
author: @message.conversation.model_config,
18-
conversation: @message.conversation
19+
author: @conversation.model_config,
20+
conversation: @conversation
1921
)
2022

2123
prepend(reply_id, "messages/spinner")
2224

23-
streamer.stream do |message|
25+
streamer.chat(chat) do |message|
2426
Rails.logger.info("Got back: #{message}")
2527
@reply.update!(content: @reply.content + message)
2628

@@ -29,22 +31,28 @@ def execute
2931

3032
@reply.update!(statistics:)
3133
rescue LlmClients::ResponseError, Errno::ECONNREFUSED, EOFError, ResponseStreamer::ResponseStreamerError,
32-
ResponseStreamer::NetworkError => e
34+
ResponseStreamer::NetworkError => e
3335
Rails.logger.error("\n#{e.class.name}: #{e.message}#{e.backtrace.join("\n")}")
3436

3537
active_message.update!(error: { message: e })
3638
end
3739

3840
private
3941

42+
def augment_message_prompt
43+
return unless collections_to_search.any?
44+
45+
@message.update!(prompt: prompt_augmentor.prompt)
46+
end
47+
4048
def active_message
4149
@reply || @message
4250
end
4351

4452
def collections_to_search
45-
@collections_to_search ||= if @message.conversation.search_collections
46-
message_collections = @message.conversation.collections
47-
message_collections.any? ? message_collections : @message.conversation.user.collections
53+
@collections_to_search ||= if @conversation.search_collections
54+
message_collections = @conversation.collections
55+
message_collections.any? ? message_collections : @conversation.user.collections
4856
else
4957
[]
5058
end
@@ -68,55 +76,44 @@ def prepend(target, partial, locals = {})
6876
end
6977

7078
def streamer
79+
# TODO: add api_key to ModelServer
7180
@streamer ||= ResponseStreamer.new(
72-
{
73-
endpoint: ModelServer.active_server.url,
74-
model: model_config.model,
75-
provider: ModelServer.active_server.provider,
76-
},
77-
prompt || @message.content
81+
endpoint: ModelServer.active_server.url,
82+
model: model_config.model,
83+
provider: ModelServer.active_server.provider
7884
)
7985
end
8086

81-
def search_hits
82-
@search_hits ||= searcher.search(@message.content)
83-
end
84-
85-
def prompt
86-
return unless collections_to_search.any?
87-
88-
@prompt ||= begin
89-
prompt = "Here is some context that may help you answer the following question:\n\n"
90-
search_hits.each do |hit|
91-
prompt << "#{hit.chunk.content}\n\n"
92-
end
93-
94-
prompt << "Question: #{@message.content}"
95-
end
96-
end
97-
98-
def search
99-
@message.update!(prompt:) if prompt.present?
100-
end
101-
10287
def searcher
103-
@searcher ||= Search::SearchMultiple.new(collections_to_search, num_results: 10)
88+
@searcher ||= Search::SearchMultiple.new(collections_to_search, num_results: 5)
10489
end
10590

10691
def model_config
107-
@message.conversation.model_config
92+
@conversation.model_config
10893
end
10994

11095
def reply_id
11196
@reply_id ||= "message_#{@reply.id}"
11297
end
11398

11499
def channel_name
115-
"#{@message.conversation.user.id}_conversations"
100+
"#{@conversation.user.id}_conversations"
116101
end
117102

118103
def statistics
119104
streamer.stats.merge({ server: ModelServer.active_server.name })
120105
end
106+
107+
def chat
108+
@chat ||= LlmClients::Ollama::Chat.new(@conversation.messages.sort)
109+
end
110+
111+
def prompt_augmentor
112+
@prompt_augmentor ||= begin
113+
search_hits = searcher.search(@message.content)
114+
115+
PromptAugmentor.new(@message, search_hits)
116+
end
117+
end
121118
end
122119
# rubocop:enable Metrics/AbcSize

0 commit comments

Comments
 (0)