Skip to content

Commit cf5f38b

Browse files
authored
Merge pull request rizethereum#2 from justi-blue/66ve70-codex/configure-openai-api-with-gem
Integrate OpenAI sentence expansion
2 parents d5348ba + d68c39d commit cf5f38b

5 files changed

Lines changed: 111 additions & 0 deletions

File tree

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ gem "stimulus-rails"
1818
gem "tailwindcss-rails"
1919
# Build JSON APIs with ease [https://github.com/rails/jbuilder]
2020
gem "jbuilder"
21+
gem "ruby-openai"
2122

2223
# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword]
2324
# gem "bcrypt", "~> 3.1.7"

Gemfile.lock

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,15 @@ GEM
9898
erubi (1.13.1)
9999
et-orbi (1.2.11)
100100
tzinfo
101+
event_stream_parser (1.0.0)
102+
faraday (2.13.4)
103+
faraday-net_http (>= 2.0, < 3.5)
104+
json
105+
logger
106+
faraday-multipart (1.1.1)
107+
multipart-post (~> 2.0)
108+
faraday-net_http (3.4.1)
109+
net-http (>= 0.5.0)
101110
fugit (1.11.1)
102111
et-orbi (~> 1, >= 1.2.11)
103112
raabro (~> 1.4)
@@ -144,6 +153,9 @@ GEM
144153
mini_mime (1.1.5)
145154
minitest (5.25.5)
146155
msgpack (1.8.0)
156+
multipart-post (2.4.1)
157+
net-http (0.6.0)
158+
uri
147159
net-imap (0.5.9)
148160
date
149161
net-protocol
@@ -281,6 +293,10 @@ GEM
281293
rubocop (>= 1.72)
282294
rubocop-performance (>= 1.24)
283295
rubocop-rails (>= 2.30)
296+
ruby-openai (8.1.0)
297+
event_stream_parser (>= 0.3.0, < 2.0.0)
298+
faraday (>= 1)
299+
faraday-multipart (>= 1)
284300
ruby-progressbar (1.13.0)
285301
securerandom (0.4.1)
286302
solid_cable (3.0.11)
@@ -371,6 +387,7 @@ DEPENDENCIES
371387
rails (~> 8.0.2)
372388
rspec-rails
373389
rubocop-rails-omakase
390+
ruby-openai
374391
solid_cable
375392
solid_cache
376393
solid_queue

app/services/sentence_expander.rb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,28 @@ def expand(base_sentence)
2727
"B2" => "#{subject} thoroughly enjoy spending time with #{object}, as their captivating nature fascinates #{object_pronoun}."
2828
}
2929
end
30+
31+
def expand_with_ai(base_sentence, model: "gpt-4o-mini")
32+
client = OpenAI::Client.new
33+
safe_sentence = base_sentence.to_s.gsub(/[\r\n]/, " ")
34+
prompt = "Expand the sentence #{JSON.generate(safe_sentence)} into CEFR levels: #{LEVELS.join(', ')}. Respond as JSON with keys #{LEVELS.join(', ')}."
35+
response = client.chat(
36+
parameters: {
37+
model: model,
38+
messages: [
39+
{ role: "user", content: prompt }
40+
],
41+
response_format: { type: "json_object" }
42+
}
43+
)
44+
45+
content = response.dig("choices", 0, "message", "content")
46+
begin
47+
raise "OpenAI API response missing expected 'content' field: #{response.inspect}" unless content.is_a?(String) && !content.strip.empty?
48+
JSON.parse(content)
49+
rescue JSON::ParserError, RuntimeError
50+
# Log the error or handle it as needed; return empty hash for safety
51+
{}
52+
end
53+
end
3054
end

config/initializers/openai.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
OpenAI.configure do |config|
2+
config.access_token = ENV["OPENAI_API_KEY"]
3+
end

spec/services/sentence_expander_spec.rb

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,70 @@
1919
expect(result["B2"]).to include("fascinates her.")
2020
end
2121
end
22+
23+
describe "#expand_with_ai" do
24+
it "parses JSON response from OpenAI" do
25+
fake_client = instance_double(OpenAI::Client)
26+
allow(OpenAI::Client).to receive(:new).and_return(fake_client)
27+
allow(fake_client).to receive(:chat).and_return(
28+
{
29+
"choices" => [
30+
{ "message" => { "content" => '{"A1":"Hello."}' } }
31+
]
32+
}
33+
)
34+
35+
expander = described_class.new
36+
result = expander.expand_with_ai("Hello")
37+
38+
expect(result["A1"]).to eq("Hello.")
39+
end
40+
41+
it "returns empty hash when OpenAI response is missing content" do
42+
fake_client = instance_double(OpenAI::Client)
43+
allow(OpenAI::Client).to receive(:new).and_return(fake_client)
44+
allow(fake_client).to receive(:chat).and_return({})
45+
46+
expander = described_class.new
47+
48+
expect(expander.expand_with_ai("Hello")).to eq({})
49+
end
50+
51+
it "returns empty hash when OpenAI response contains invalid JSON" do
52+
fake_client = instance_double(OpenAI::Client)
53+
allow(OpenAI::Client).to receive(:new).and_return(fake_client)
54+
allow(fake_client).to receive(:chat).and_return(
55+
{
56+
"choices" => [
57+
{ "message" => { "content" => "not-json" } }
58+
]
59+
}
60+
)
61+
62+
expander = described_class.new
63+
64+
expect(expander.expand_with_ai("Hello")).to eq({})
65+
end
66+
67+
it "sanitizes the base sentence before sending to OpenAI" do
68+
fake_client = instance_double(OpenAI::Client)
69+
allow(OpenAI::Client).to receive(:new).and_return(fake_client)
70+
71+
captured_prompt = nil
72+
allow(fake_client).to receive(:chat) do |params|
73+
captured_prompt = params.dig(:parameters, :messages, 0, :content)
74+
{
75+
"choices" => [
76+
{ "message" => { "content" => '{"A1":"Hi."}' } }
77+
]
78+
}
79+
end
80+
81+
expander = described_class.new
82+
expander.expand_with_ai("O'Hara\n<script>")
83+
84+
expect(captured_prompt).not_to include("\n")
85+
expect(captured_prompt).to include("\"O'Hara <script>\"")
86+
end
87+
end
2288
end

0 commit comments

Comments
 (0)