action_prompter 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2e39b2cf1925be44f693de2d977ffadb9696388b05e7453b36c3e8340d0e79eb
4
+ data.tar.gz: fb7bc10ee65be65492023a52bc28566c3f56ea4a3d6fb872e6066f1ae004d9a4
5
+ SHA512:
6
+ metadata.gz: f819ed0a124a05367a6f2f5ddde73ade099299ac5083be5da5ea4fb0bc2d9fa9ed8d1c919a7c4c9d901384758087a00235e30aeee0d8f02ff45f2956d1847837
7
+ data.tar.gz: c80fd8a2c9edea9ae85e935f06a8a30b327c3a98224e5dfb6b9f8221ede0abf87969146a5860b8b4f270a4acefa37a7a49fb8ce73724ad415d5c30ed6ce35730
data/CHANGELOG.md ADDED
@@ -0,0 +1,24 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-02-26
11
+
12
+ ### Added
13
+
14
+ - Published as `action_prompter` gem (`require "action_prompt"`, module `ActionPrompt`)
15
+ - `ActionPrompt::Base` with ActionMailer-inspired DSL for defining prompt actions
16
+ - ERB template rendering via ActionView (templates in `app/views/action_prompts/`)
17
+ - Pluggable adapter system (`ActionPrompt::Adapters::Base`)
18
+ - Built-in `Test` adapter for use in test suites (captures deliveries in memory)
19
+ - Built-in `Null` adapter for suppressing LLM calls in specific environments
20
+ - Global configuration via `ActionPrompt.configure`
21
+ - Per-class default options via `.default(model:, temperature:, ...)`
22
+ - `ActionPrompt::Message` with `deliver_now` for synchronous delivery
23
+ - Rails generator: `rails generate action_prompt NAME [action action ...]`
24
+ - `Railtie` for automatic integration into Rails apps
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Your Name
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,586 @@
1
+ # ActionPrompt
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/action_prompt.svg)](https://badge.fury.io/rb/action_prompt)
4
+ [![CI](https://github.com/soran-me/action_prompt/actions/workflows/ci.yml/badge.svg)](https://github.com/soran-me/action_prompt/actions)
5
+
6
+ **ActionPrompt** brings Rails-native conventions to Large Language Model (LLM) integration. Inspired by ActionMailer, it lets you define prompt classes with structured actions, write prompts as ERB templates, and deliver them to any LLM provider through a pluggable adapter system.
7
+
8
+ ```ruby
9
+ # app/prompts/article_summarizer_prompt.rb
10
+ class ArticleSummarizerPrompt < ActionPrompt::Base
11
+ default model: "gpt-4o-mini", temperature: 0.5
12
+
13
+ def summarize(article)
14
+ @article = article
15
+ prompt
16
+ end
17
+ end
18
+
19
+ # app/views/action_prompts/article_summarizer_prompt/summarize.text.erb
20
+ Summarize the following article in three concise bullet points.
21
+
22
+ Title: <%= @article.title %>
23
+
24
+ <%= @article.body %>
25
+
26
+ # Anywhere in your app
27
+ response = ArticleSummarizerPrompt.summarize(article).deliver_now
28
+ ```
29
+
30
+ ---
31
+
32
+ ## Table of Contents
33
+
34
+ - [Installation](#installation)
35
+ - [Configuration](#configuration)
36
+ - [Core Concepts](#core-concepts)
37
+ - [Quick Start](#quick-start)
38
+ - [Generator](#generator)
39
+ - [Writing Templates](#writing-templates)
40
+ - [Default Options](#default-options)
41
+ - [Adapters](#adapters)
42
+ - [Writing an OpenAI Adapter](#writing-an-openai-adapter)
43
+ - [Writing a Google Gemini Adapter](#writing-a-google-gemini-adapter)
44
+ - [Testing](#testing)
45
+ - [Advanced Usage](#advanced-usage)
46
+ - [Contributing](#contributing)
47
+ - [License](#license)
48
+
49
+ ---
50
+
51
+ ## Installation
52
+
53
+ Add to your `Gemfile`:
54
+
55
+ ```ruby
56
+ gem "action_prompter"
57
+ ```
58
+
59
+ Then run:
60
+
61
+ ```sh
62
+ bundle install
63
+ ```
64
+
65
+ ActionPrompt requires **Ruby 3.1+** and **Rails 7.0+**.
66
+
67
+ ---
68
+
69
+ ## Configuration
70
+
71
+ Create an initializer:
72
+
73
+ ```ruby
74
+ # config/initializers/action_prompt.rb
75
+
76
+ ActionPrompt.configure do |config|
77
+ # Set the delivery adapter (required for real LLM calls)
78
+ config.adapter = MyOpenAIAdapter.new(api_key: ENV.fetch("OPENAI_API_KEY"))
79
+
80
+ # Global defaults applied to every prompt unless overridden
81
+ config.default_options = {
82
+ model: "gpt-4o",
83
+ temperature: 0.7
84
+ }
85
+ end
86
+ ```
87
+
88
+ In development/test, the gem ships with two built-in adapters so you can work
89
+ without live API credentials:
90
+
91
+ | Adapter | Behaviour |
92
+ |---|---|
93
+ | `ActionPrompt::Adapters::Test` | Records deliveries in memory (default) |
94
+ | `ActionPrompt::Adapters::Null` | Silently discards all prompts, returns `nil` |
95
+
96
+ ---
97
+
98
+ ## Core Concepts
99
+
100
+ | Concept | ActionMailer analogy | ActionPrompt equivalent |
101
+ |---|---|---|
102
+ | Prompt class | Mailer class | `ApplicationPrompt < ActionPrompt::Base` |
103
+ | Action method | `welcome_email(user)` | `summarize(article)` |
104
+ | Template | `welcome_email.html.erb` | `summarize.text.erb` |
105
+ | Message object | `Mail::Message` | `ActionPrompt::Message` |
106
+ | Delivery | `deliver_now` | `deliver_now` |
107
+ | Adapter | `smtp_settings` | `config.adapter = MyAdapter.new` |
108
+
109
+ ### Flow
110
+
111
+ ```
112
+ YourPrompt.action(args) # class-level call
113
+ └─► ActionPrompt::Base # creates instance, calls action method
114
+ └─► #prompt(options) # sets options, returns Message
115
+ └─► Message # lazily renders ERB template
116
+ └─► #deliver_now
117
+ └─► Adapter#complete(body, options) # → LLM → response
118
+ ```
119
+
120
+ ---
121
+
122
+ ## Quick Start
123
+
124
+ ### 1. Create the prompt class
125
+
126
+ ```ruby
127
+ # app/prompts/article_summarizer_prompt.rb
128
+
129
+ class ArticleSummarizerPrompt < ActionPrompt::Base
130
+ # Class-level defaults — override global config for this class only.
131
+ default model: "gpt-4o-mini", temperature: 0.5
132
+
133
+ # Each public method is a "prompt action".
134
+ # Set instance variables here; they become available in the ERB template.
135
+ def summarize(article)
136
+ @article = article
137
+ @word_limit = 150
138
+
139
+ # Call `prompt` last. Pass per-action overrides if needed.
140
+ prompt(model: "gpt-4o")
141
+ end
142
+ end
143
+ ```
144
+
145
+ ### 2. Create the ERB template
146
+
147
+ ```erb
148
+ <%# app/views/action_prompts/article_summarizer_prompt/summarize.text.erb %>
149
+
150
+ You are an expert editorial assistant. Summarize the article below in no more
151
+ than <%= @word_limit %> words, using three concise bullet points.
152
+
153
+ ## Article
154
+
155
+ Title: <%= @article.title %>
156
+ Author: <%= @article.author.name %>
157
+ Published: <%= @article.published_at.strftime("%B %-d, %Y") %>
158
+
159
+ ---
160
+
161
+ <%= @article.body %>
162
+ ```
163
+
164
+ ### 3. Deliver
165
+
166
+ ```ruby
167
+ # In a controller, background job, or anywhere else:
168
+ article = Article.find(params[:id])
169
+ response = ArticleSummarizerPrompt.summarize(article).deliver_now
170
+
171
+ render json: { summary: response }
172
+ ```
173
+
174
+ ---
175
+
176
+ ## Generator
177
+
178
+ The generator creates both the prompt class and the view template(s) in one
179
+ command:
180
+
181
+ ```sh
182
+ rails generate action_prompt NAME [action action ...]
183
+ ```
184
+
185
+ ### Example
186
+
187
+ ```sh
188
+ rails generate action_prompt ArticleSummarizer summarize
189
+ ```
190
+
191
+ Generates:
192
+
193
+ ```
194
+ create app/prompts/article_summarizer_prompt.rb
195
+ create app/views/action_prompts/article_summarizer_prompt/summarize.text.erb
196
+ ```
197
+
198
+ ### Multiple actions
199
+
200
+ ```sh
201
+ rails generate action_prompt Moderation classify flag summarize
202
+ ```
203
+
204
+ Generates one class with three stubbed action methods and three view templates.
205
+
206
+ ### Namespaced prompts
207
+
208
+ ```sh
209
+ rails generate action_prompt Admin::Report generate
210
+ ```
211
+
212
+ Generates:
213
+
214
+ ```
215
+ create app/prompts/admin/report_prompt.rb
216
+ create app/views/action_prompts/admin/report_prompt/generate.text.erb
217
+ ```
218
+
219
+ ---
220
+
221
+ ## Writing Templates
222
+
223
+ Templates live in `app/views/action_prompts/` and follow the naming convention:
224
+
225
+ ```
226
+ app/views/action_prompts/<prompt_class_underscored>/<action_name>.text.erb
227
+ ```
228
+
229
+ All instance variables set in the action method are available in the template.
230
+ Standard ERB interpolation, conditionals, and partials all work.
231
+
232
+ ```erb
233
+ <%# app/views/action_prompts/article_summarizer_prompt/summarize.text.erb %>
234
+
235
+ You are a helpful assistant specialised in content summarisation.
236
+
237
+ <% if @article.paywalled? %>
238
+ Note: this article is paywalled; focus only on the excerpt provided.
239
+ <% end %>
240
+
241
+ Please summarise the following content:
242
+
243
+ <%= @article.excerpt %>
244
+ ```
245
+
246
+ ### System prompts
247
+
248
+ A common pattern is to split system and user content into separate template
249
+ "sections" within a single file using a plain delimiter:
250
+
251
+ ```erb
252
+ ---SYSTEM---
253
+ You are an expert Ruby on Rails developer. Answer questions concisely.
254
+
255
+ ---USER---
256
+ <%= @question %>
257
+ ```
258
+
259
+ Your adapter can then parse the delimiter and map sections to the LLM's
260
+ `system` / `user` message roles.
261
+
262
+ ---
263
+
264
+ ## Default Options
265
+
266
+ Options are merged in increasing precedence:
267
+
268
+ ```
269
+ global config < class default < per-action option
270
+ ```
271
+
272
+ ```ruby
273
+ # Global (lowest priority)
274
+ ActionPrompt.configure { |c| c.default_options = { model: "gpt-3.5-turbo", temperature: 0.9 } }
275
+
276
+ class MyPrompt < ActionPrompt::Base
277
+ # Class default (overrides global)
278
+ default model: "gpt-4o", max_tokens: 1024
279
+
280
+ def draft(brief)
281
+ @brief = brief
282
+ # Per-action (overrides class default — highest priority)
283
+ prompt(temperature: 0.2)
284
+ end
285
+ end
286
+
287
+ # Effective options for draft():
288
+ # { model: "gpt-4o", temperature: 0.2, max_tokens: 1024 }
289
+ ```
290
+
291
+ ---
292
+
293
+ ## Adapters
294
+
295
+ An adapter is any object that inherits from `ActionPrompt::Adapters::Base` and
296
+ implements the `#complete` method:
297
+
298
+ ```ruby
299
+ # @param prompt_text [String] the fully rendered prompt string
300
+ # @param options [Hash] merged LLM options (model, temperature, …)
301
+ # @return [String] the LLM response text
302
+ def complete(prompt_text, options = {})
303
+ raise NotImplementedError
304
+ end
305
+ ```
306
+
307
+ ### Writing an OpenAI Adapter
308
+
309
+ ```ruby
310
+ # lib/my_app/adapters/open_ai_adapter.rb
311
+
312
+ # Gemfile: gem "ruby-openai"
313
+ require "openai"
314
+
315
+ module MyApp
316
+ module Adapters
317
+ class OpenAIAdapter < ActionPrompt::Adapters::Base
318
+ def initialize(api_key:)
319
+ @client = OpenAI::Client.new(access_token: api_key)
320
+ end
321
+
322
+ def complete(prompt_text, options = {})
323
+ response = @client.chat(
324
+ parameters: {
325
+ model: options.fetch(:model, "gpt-4o"),
326
+ temperature: options.fetch(:temperature, 0.7),
327
+ max_tokens: options[:max_tokens],
328
+ messages: [{ role: "user", content: prompt_text }]
329
+ }.compact
330
+ )
331
+
332
+ response.dig("choices", 0, "message", "content")
333
+ rescue OpenAI::Error => e
334
+ raise ActionPrompt::DeliveryError, "OpenAI error: #{e.message}"
335
+ end
336
+ end
337
+ end
338
+ end
339
+ ```
340
+
341
+ Register in your initializer:
342
+
343
+ ```ruby
344
+ # config/initializers/action_prompt.rb
345
+ ActionPrompt.configure do |config|
346
+ config.adapter = MyApp::Adapters::OpenAIAdapter.new(
347
+ api_key: ENV.fetch("OPENAI_API_KEY")
348
+ )
349
+ config.default_options = { model: "gpt-4o", temperature: 0.7 }
350
+ end
351
+ ```
352
+
353
+ ### Writing a Google Gemini Adapter
354
+
355
+ ```ruby
356
+ # lib/my_app/adapters/gemini_adapter.rb
357
+
358
+ # Gemfile: gem "gemini-ai"
359
+ require "gemini-ai"
360
+
361
+ module MyApp
362
+ module Adapters
363
+ class GeminiAdapter < ActionPrompt::Adapters::Base
364
+ def initialize(api_key:, default_model: "gemini-1.5-pro")
365
+ @default_model = default_model
366
+ @api_key = api_key
367
+ end
368
+
369
+ def complete(prompt_text, options = {})
370
+ model = options.fetch(:model, @default_model)
371
+ client = Gemini.new(
372
+ credentials: { service: "generative-language-api", api_key: @api_key },
373
+ options: { model: model, server_sent_events: false }
374
+ )
375
+
376
+ result = client.generate_content(
377
+ {
378
+ contents: { role: "user", parts: { text: prompt_text } },
379
+ generationConfig: {
380
+ temperature: options[:temperature],
381
+ maxOutputTokens: options[:max_tokens]
382
+ }.compact
383
+ }
384
+ )
385
+
386
+ result.dig("candidates", 0, "content", "parts", 0, "text")
387
+ end
388
+ end
389
+ end
390
+ end
391
+ ```
392
+
393
+ Register in your initializer:
394
+
395
+ ```ruby
396
+ ActionPrompt.configure do |config|
397
+ config.adapter = MyApp::Adapters::GeminiAdapter.new(
398
+ api_key: ENV.fetch("GOOGLE_AI_API_KEY"),
399
+ default_model: "gemini-1.5-flash"
400
+ )
401
+ end
402
+ ```
403
+
404
+ ---
405
+
406
+ ## Testing
407
+
408
+ ActionPrompt ships with `ActionPrompt::Adapters::Test`, which captures all
409
+ deliveries in memory without making real API calls.
410
+
411
+ ### Setup
412
+
413
+ ```ruby
414
+ # spec/support/action_prompt.rb
415
+
416
+ RSpec.configure do |config|
417
+ config.before do
418
+ ActionPrompt.reset_configuration!
419
+ ActionPrompt.configure do |c|
420
+ c.adapter = ActionPrompt::Adapters::Test.new
421
+ end
422
+ ActionPrompt::Adapters::Test.clear_deliveries!
423
+ end
424
+ end
425
+ ```
426
+
427
+ Require this file from `spec/rails_helper.rb`:
428
+
429
+ ```ruby
430
+ require "support/action_prompt"
431
+ ```
432
+
433
+ ### Writing tests
434
+
435
+ ```ruby
436
+ # spec/prompts/article_summarizer_prompt_spec.rb
437
+
438
+ RSpec.describe ArticleSummarizerPrompt do
439
+ let(:article) do
440
+ double(:article,
441
+ title: "The Future of Rails",
442
+ body: "Rails continues to evolve...",
443
+ author: double(name: "DHH"),
444
+ paywalled?: false,
445
+ published_at: Date.today,
446
+ excerpt: "Rails continues to evolve..."
447
+ )
448
+ end
449
+
450
+ describe ".summarize" do
451
+ it "returns an ActionPrompt::Message" do
452
+ message = described_class.summarize(article)
453
+ expect(message).to be_an(ActionPrompt::Message)
454
+ end
455
+
456
+ it "uses the configured model" do
457
+ described_class.summarize(article).deliver_now
458
+ options = ActionPrompt::Adapters::Test.deliveries.last[:options]
459
+ expect(options[:model]).to eq("gpt-4o")
460
+ end
461
+
462
+ it "includes the article title in the rendered prompt" do
463
+ described_class.summarize(article).deliver_now
464
+ prompt = ActionPrompt::Adapters::Test.deliveries.last[:prompt]
465
+ expect(prompt).to include("The Future of Rails")
466
+ end
467
+
468
+ it "includes the word limit in the rendered prompt" do
469
+ described_class.summarize(article).deliver_now
470
+ prompt = ActionPrompt::Adapters::Test.deliveries.last[:prompt]
471
+ expect(prompt).to include("150")
472
+ end
473
+ end
474
+ end
475
+ ```
476
+
477
+ ### Testing the adapter in isolation
478
+
479
+ ```ruby
480
+ RSpec.describe ArticleSummarizerPrompt do
481
+ it "calls the adapter with the rendered body" do
482
+ adapter = instance_double(MyApp::Adapters::OpenAIAdapter, complete: "Summary text.")
483
+ ActionPrompt.configure { |c| c.adapter = adapter }
484
+
485
+ described_class.summarize(article).deliver_now
486
+
487
+ expect(adapter).to have_received(:complete)
488
+ .with(include("The Future of Rails"), hash_including(model: "gpt-4o"))
489
+ end
490
+ end
491
+ ```
492
+
493
+ ---
494
+
495
+ ## Advanced Usage
496
+
497
+ ### Accessing the raw message body before delivery
498
+
499
+ ```ruby
500
+ message = ArticleSummarizerPrompt.summarize(article)
501
+ puts message.body # renders the ERB and returns the string
502
+ puts message.options # { model: "gpt-4o", temperature: 0.5 }
503
+ response = message.deliver_now
504
+ ```
505
+
506
+ ### Extracting a base prompt class for your app
507
+
508
+ ```ruby
509
+ # app/prompts/application_prompt.rb
510
+ class ApplicationPrompt < ActionPrompt::Base
511
+ default model: "gpt-4o-mini", temperature: 0.7
512
+
513
+ private
514
+
515
+ # Shared helpers available in all subclass action methods.
516
+ def current_date
517
+ Date.today.strftime("%B %-d, %Y")
518
+ end
519
+ end
520
+ ```
521
+
522
+ ```ruby
523
+ class ArticleSummarizerPrompt < ApplicationPrompt
524
+ def summarize(article)
525
+ @article = article
526
+ @date = current_date # available from ApplicationPrompt
527
+ prompt
528
+ end
529
+ end
530
+ ```
531
+
532
+ ### Conditional adapter switching per environment
533
+
534
+ ```ruby
535
+ # config/initializers/action_prompt.rb
536
+ ActionPrompt.configure do |config|
537
+ config.adapter =
538
+ case Rails.env
539
+ when "production" then MyApp::Adapters::OpenAIAdapter.new(api_key: ENV.fetch("OPENAI_API_KEY"))
540
+ when "development" then ActionPrompt::Adapters::Null.new # save API credits locally
541
+ when "test" then ActionPrompt::Adapters::Test.new
542
+ end
543
+ end
544
+ ```
545
+
546
+ ---
547
+
548
+ ## Project Structure
549
+
550
+ ```
551
+ your_rails_app/
552
+ ├── app/
553
+ │ ├── prompts/
554
+ │ │ └── article_summarizer_prompt.rb # Prompt class
555
+ │ └── views/
556
+ │ └── action_prompts/
557
+ │ └── article_summarizer_prompt/
558
+ │ └── summarize.text.erb # ERB template
559
+ ├── config/
560
+ │ └── initializers/
561
+ │ └── action_prompt.rb # Adapter + global config
562
+ └── spec/
563
+ ├── prompts/
564
+ │ └── article_summarizer_prompt_spec.rb
565
+ └── support/
566
+ └── action_prompt.rb # Test setup
567
+ ```
568
+
569
+ ---
570
+
571
+ ## Contributing
572
+
573
+ 1. Fork the repository
574
+ 2. Create a feature branch (`git checkout -b feature/my-feature`)
575
+ 3. Make your changes and add tests
576
+ 4. Ensure all tests pass (`bundle exec rspec`) and the linter is happy (`bundle exec rubocop`)
577
+ 5. Open a Pull Request
578
+
579
+ Bug reports and feature requests are welcome at
580
+ [https://github.com/soran-me/action_prompt/issues](https://github.com/soran-me/action_prompt/issues).
581
+
582
+ ---
583
+
584
+ ## License
585
+
586
+ The gem is available as open source under the terms of the [MIT License](LICENSE.txt).
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionPrompt
4
+ module Adapters
5
+ # Abstract base class for all ActionPrompt delivery adapters.
6
+ #
7
+ # Implement a custom adapter by subclassing this and overriding {#complete}.
8
+ #
9
+ # == OpenAI Example
10
+ #
11
+ # # Gemfile: gem "ruby-openai"
12
+ #
13
+ # class OpenAIAdapter < ActionPrompt::Adapters::Base
14
+ # def initialize(api_key:)
15
+ # @client = OpenAI::Client.new(access_token: api_key)
16
+ # end
17
+ #
18
+ # def complete(prompt_text, options = {})
19
+ # response = @client.chat(
20
+ # parameters: {
21
+ # model: options.fetch(:model, "gpt-4o"),
22
+ # temperature: options.fetch(:temperature, 0.7),
23
+ # messages: [{ role: "user", content: prompt_text }]
24
+ # }
25
+ # )
26
+ # response.dig("choices", 0, "message", "content")
27
+ # end
28
+ # end
29
+ #
30
+ # == Google Gemini Example
31
+ #
32
+ # # Gemfile: gem "gemini-ai"
33
+ #
34
+ # class GeminiAdapter < ActionPrompt::Adapters::Base
35
+ # def initialize(api_key:)
36
+ # @client = Gemini.new(
37
+ # credentials: { service: "generative-language-api", api_key: api_key },
38
+ # options: { model: "gemini-pro", server_sent_events: false }
39
+ # )
40
+ # end
41
+ #
42
+ # def complete(prompt_text, options = {})
43
+ # result = @client.generate_content(
44
+ # { contents: { role: "user", parts: { text: prompt_text } } },
45
+ # request_options: { model: options[:model] }.compact
46
+ # )
47
+ # result.dig("candidates", 0, "content", "parts", 0, "text")
48
+ # end
49
+ # end
50
+ class Base
51
+ # Sends +prompt_text+ to the LLM and returns its text response.
52
+ #
53
+ # @param prompt_text [String] the fully rendered prompt
54
+ # @param options [Hash] adapter-specific options (e.g. +:model+, +:temperature+)
55
+ # @return [String] the LLM response text
56
+ # @raise [NotImplementedError] if the subclass does not implement this method
57
+ def complete(prompt_text, options = {}) # rubocop:disable Lint/UnusedMethodArgument
58
+ raise NotImplementedError, "#{self.class}#complete must be implemented by subclasses"
59
+ end
60
+ end
61
+ end
62
+ end