axn-ruby_llm 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: 5c8aae7528ce13d8b34d96781ea472dec9059aaadd029176c8442da9a828c88c
4
+ data.tar.gz: 5d45458a6c80b9679fa3a59c12e9674264891788bbd586b26fb589763403bd19
5
+ SHA512:
6
+ metadata.gz: e42eb1a60816cf49c579ebe9343ffa53ff39eadb95a746cd624a4bc78a1209ef8f52f8c2f566566d8d47fc45b16da86161bb88e179814b77bd919c5af44b45af
7
+ data.tar.gz: 12ca3f1d02a06c8c0e0d1120a9d4b600b37dde32fed917020cd50ff129958e9509cf4b87bb0037f79e50e227669bccec4ccf3bfb09bfb80ae371842ac66fd70a
data/CHANGELOG.md ADDED
@@ -0,0 +1,14 @@
1
+ # Changelog
2
+
3
+ ## [0.1.0] - 2026-05-21
4
+
5
+ Initial release.
6
+
7
+ - `Axn::RubyLLM::Ask` action — port of the `Actions::LLM::Ask` pattern from buyout-app, with parameterized model/system_prompt/temperature and opt-in JSON mode (default `false`).
8
+ - `Axn::RubyLLM.ask` / `ask!` module-level shortcuts.
9
+ - Structured output: pass `schema:` (a `RubyLLM::Schema` class, instance, or any JSON Schema hash) to enable provider-enforced structured output via `RubyLLM::Chat#with_schema`. Result returns a parsed Hash; non-JSON responses fail with `"Schema response was not valid JSON"`. Takes precedence over `json: true`.
10
+ - Result exposes `response`, `raw_message`, `input_tokens`, `output_tokens`, `cost` (Float USD total), and `cost_breakdown` (`RubyLLM::Cost` struct). Cost fields are nil when RubyLLM lacks pricing for the model.
11
+ - OpenTelemetry span enrichment: when an OTel SDK is loaded, every `Ask` call sets `gen_ai.request.model`, `gen_ai.response.model`, `gen_ai.usage.input_tokens`, `gen_ai.usage.output_tokens`, `gen_ai.usage.cost` (USD), and `axn.ruby_llm.stubbed` on the existing `axn.call` span. No configuration required; no-op if OTel is not loaded. Full LLM-level tracing (individual chat calls, tool calls, embeddings) requires [`opentelemetry-instrumentation-ruby_llm`](https://github.com/thoughtbot/opentelemetry-instrumentation-ruby_llm) in your own Gemfile.
12
+ - Production gating: `Axn::RubyLLM.configure { |c| c.enabled = -> { ... } }` (Boolean or callable). When disabled, `Ask` returns a success result with stub content (`response: "stubbed response value"` for plain, `{ "stubbed" => true }` for json/schema; `raw_message` is an `Ask::StubMessage` Data instance; tokens/cost zeroed) and `result.stubbed == true`.
13
+ - Rate-limit handling rescues `RubyLLM::RateLimitError` (HTTP 429, provider-agnostic) and fails with `"Rate limit reached: <message>"`.
14
+ - `Axn::RubyLLM::RSpec::Helpers` — `stub_axn_ruby_llm` helper accepting `response:`, optional `model:`, `schema:`, `input_tokens:`, `output_tokens:`, `cost:`.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Teamshares, Inc.
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,169 @@
1
+ # axn-ruby_llm
2
+
3
+ Call LLMs from [Axn](https://github.com/teamshares/axn) actions using [RubyLLM](https://github.com/crmne/ruby_llm), with declarative error handling, optional JSON mode, configurable defaults, and cost/token tracking.
4
+
5
+ Part of the `axn-*` extension ecosystem — see also [axn-mcp](https://github.com/teamshares/axn-mcp).
6
+
7
+ ### Why use this over calling RubyLLM directly?
8
+
9
+ Three things you'd otherwise build at every callsite:
10
+
11
+ 1. **Structured error handling.** The Axn error DSL declaratively maps `RateLimitError`, `JSON::ParserError`, and generic `StandardError` to clean failure messages. Callers check `result.ok?` instead of wrapping every call in `begin/rescue`.
12
+
13
+ 2. **Production gating.** A single `c.enabled = -> { Rails.env.production? }` in an initializer stubs every LLM call in non-prod environments — no per-callsite guards needed. The stub is typed (`stubbed: true`, `input_tokens: 0`, etc.) so downstream code doesn't need to branch on it either.
14
+
15
+ 3. **Cost/token tracking, exposed automatically.** Every call exposes `input_tokens`, `output_tokens`, `cost`, and `cost_breakdown` without you doing the `RubyLLM.models.find` lookup manually. If your app uses OpenTelemetry, these values are also set as attributes on the existing `axn.call` span — no configuration required.
16
+
17
+ > **Scope note:** This gem covers the subset of RubyLLM functionality that [Teamshares](https://github.com/teamshares) uses internally — single-turn chat, structured output, and basic observability. It is intentionally minimal rather than a full-featured wrapper. Feedback and pull requests to extend it are very welcome.
18
+
19
+ ---
20
+
21
+ ## Installation
22
+
23
+ ```ruby
24
+ gem "axn-ruby_llm"
25
+ ```
26
+
27
+ Configure RubyLLM as normal (e.g. in `config/initializers/ruby_llm.rb`). The default model is `gpt-4o-mini`, but any [RubyLLM-supported provider](https://rubyllm.com/llms) works — just configure the appropriate API key and pass `model:` to override:
28
+
29
+ ```ruby
30
+ RubyLLM.configure do |c|
31
+ c.openai_api_key = ENV["OPENAI_API_KEY"] # OpenAI
32
+ c.anthropic_api_key = ENV["ANTHROPIC_API_KEY"] # or Anthropic, Gemini, etc.
33
+ end
34
+ ```
35
+
36
+ Optionally configure gem-level defaults:
37
+
38
+ ```ruby
39
+ Axn::RubyLLM.configure do |c|
40
+ c.default_model = "gpt-4o-mini" # default; override with any RubyLLM model ID
41
+ end
42
+ ```
43
+
44
+ ## Usage
45
+
46
+ ```ruby
47
+ result = Axn::RubyLLM.ask(
48
+ prompt: "Summarize this Slack thread: #{thread_text}"
49
+ )
50
+ result.response # => "The team decided to..."
51
+
52
+ # JSON mode
53
+ result = Axn::RubyLLM.ask(
54
+ prompt: build_extraction_prompt(doc),
55
+ json: true
56
+ )
57
+ result.response # => { "company" => "Acme", "founded" => 1999 }
58
+
59
+ # With system prompt and model override
60
+ result = Axn::RubyLLM.ask(
61
+ prompt: user_message,
62
+ system_prompt: "You are a concise financial analyst.",
63
+ model: "gpt-4o",
64
+ temperature: 0.2
65
+ )
66
+ ```
67
+
68
+ The underlying action class is available as `Axn::RubyLLM::Ask` for cases where you need the full `Axn` interface (`call!`, `call_async`, instrumentation hooks, etc.).
69
+
70
+ ### Structured output via schema
71
+
72
+ Pass `schema:` to enable provider-enforced structured output (e.g. OpenAI strict mode) via `RubyLLM::Chat#with_schema`. The result's `response` is the parsed Hash.
73
+
74
+ ```ruby
75
+ class CompanyMatch < RubyLLM::Schema
76
+ integer :company_id, description: "ID of the matched company, or null"
77
+ number :confidence, description: "0.0–1.0"
78
+ string :reasoning
79
+ end
80
+
81
+ result = Axn::RubyLLM.ask(
82
+ prompt: "Which company is this thread about?\n\n#{thread_text}",
83
+ schema: CompanyMatch,
84
+ )
85
+ result.response # => { "company_id" => 42, "confidence" => 0.92, "reasoning" => "..." }
86
+ ```
87
+
88
+ `schema:` accepts a [`ruby_llm-schema`](https://github.com/crmne/ruby_llm-schema) class or instance — anything `RubyLLM::Chat#with_schema` accepts, including a raw JSON Schema hash. The `ruby_llm-schema` gem is recommended but not required; declare it in your own Gemfile if you want the DSL. When `schema:` is set, `json: true` is ignored.
89
+
90
+ ### Token counts and cost
91
+
92
+ Every successful result exposes token usage and cost in two tiers:
93
+
94
+ ```ruby
95
+ result = Axn::RubyLLM.ask(prompt: "...")
96
+
97
+ # Flat (common case)
98
+ result.input_tokens # => 412
99
+ result.output_tokens # => 78
100
+ result.cost # => 0.00056 (Float USD total; nil if RubyLLM has no pricing for the model)
101
+
102
+ # Resolved breakdown — RubyLLM::Cost struct
103
+ result.cost_breakdown # => #<Cost input: 0.0004, output: 0.00016, cache_read: 0.0, ..., total: 0.00056>
104
+
105
+ # Full escape hatch — the raw RubyLLM::Message for cache/thinking tokens, etc.
106
+ result.raw_message # => #<RubyLLM::Message ...>
107
+ ```
108
+
109
+ `cost` and `cost_breakdown` are both `nil` when RubyLLM lacks pricing for the model (e.g. unknown/custom endpoints). Token counts are nil only if the provider did not return them.
110
+
111
+ Errors are handled via Axn's declarative `error` DSL:
112
+ - `JSON::ParserError` → result fails with `"Failed to parse JSON from LLM response"`
113
+ - `RubyLLM::RateLimitError` (HTTP 429, provider-agnostic) → result fails with `"Rate limit reached: <message>"`
114
+ - `schema:` set but LLM returned non-JSON → result fails with `"Schema response was not valid JSON"`
115
+ - Any other `StandardError` → result fails with `"LLM request failed: <message>"`
116
+
117
+ ## Testing
118
+
119
+ In your specs, require the helpers and use `stub_axn_ruby_llm`:
120
+
121
+ ```ruby
122
+ require "axn/ruby_llm/rspec"
123
+
124
+ it "summarizes the thread" do
125
+ stub_axn_ruby_llm(response: "The team agreed to ship on Friday.")
126
+ result = Axn::RubyLLM.ask(prompt: "...")
127
+ expect(result.response).to include("ship on Friday")
128
+ end
129
+ ```
130
+
131
+ ## OpenTelemetry
132
+
133
+ If your app uses OpenTelemetry, `axn` already wraps every action in an `axn.call` span. This gem enriches that span with LLM-specific attributes automatically — no configuration required:
134
+
135
+ | Attribute | Value |
136
+ |---|---|
137
+ | `gen_ai.request.model` | The model requested |
138
+ | `gen_ai.response.model` | The model that responded |
139
+ | `gen_ai.usage.input_tokens` | Prompt token count |
140
+ | `gen_ai.usage.output_tokens` | Completion token count |
141
+ | `gen_ai.usage.cost` | USD total (non-standard; useful for spend filtering) |
142
+ | `axn.ruby_llm.stubbed` | `true` when production gating returned a stub |
143
+
144
+ For LLM-level tracing (individual `RubyLLM.chat` calls, tool calls, embeddings, prompt content), add [`opentelemetry-instrumentation-ruby_llm`](https://github.com/thoughtbot/opentelemetry-instrumentation-ruby_llm) to your own Gemfile and configure it per its README. It is not a dependency of this gem.
145
+
146
+ ## Production gating
147
+
148
+ Set `Configuration#enabled` to gate LLM calls — useful for skipping spend in non-production environments. Accepts a Boolean or a callable (evaluated per call):
149
+
150
+ ```ruby
151
+ Axn::RubyLLM.configure do |c|
152
+ c.enabled = -> { Rails.env.production? }
153
+ # c.enabled = false # always stub
154
+ # c.enabled = true # default; always run
155
+ end
156
+ ```
157
+
158
+ When disabled, `Ask` returns a **success** result with obvious stub content, so callers don't need per-callsite branching:
159
+
160
+ | Field | Stubbed value |
161
+ |---|---|
162
+ | `response` | `"stubbed response value"` (plain) / `{ "stubbed" => true }` (`json: true` or `schema:`) |
163
+ | `raw_message` | `Ask::StubMessage` Data instance with `.content`, `.input_tokens`, `.output_tokens`, `.model_id` |
164
+ | `input_tokens` / `output_tokens` | `0` |
165
+ | `cost` | `0.0` |
166
+ | `cost_breakdown` | `nil` |
167
+ | `stubbed` | `true` |
168
+
169
+ Check `result.stubbed` if you need to branch on it (e.g. skip downstream writes that would otherwise persist stub LLM output). The Axn result's `message` is `"disabled - returning stubbed values"` for the same purpose.
data/Rakefile ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+ require "rubocop/rake_task"
6
+
7
+ RSpec::Core::RakeTask.new(:spec)
8
+
9
+ RuboCop::RakeTask.new
10
+
11
+ task default: %i[spec rubocop]
12
+
13
+ Rake::Task["build"].enhance([:default])
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Axn
4
+ module RubyLLM
5
+ class Ask
6
+ include Axn
7
+
8
+ expects :prompt
9
+ expects :json, type: :boolean, default: false
10
+ expects :schema, optional: true
11
+ expects :model, optional: true
12
+ expects :system_prompt, optional: true
13
+ expects :temperature, optional: true
14
+
15
+ exposes :response
16
+ exposes :raw_message
17
+ exposes :input_tokens, allow_nil: true
18
+ exposes :output_tokens, allow_nil: true
19
+ exposes :cost, allow_nil: true
20
+ exposes :cost_breakdown, allow_nil: true
21
+ exposes :stubbed, type: :boolean, default: false
22
+
23
+ StubMessage = Data.define(:content, :input_tokens, :output_tokens, :model_id)
24
+
25
+ error prefix: "LLM request failed: "
26
+ error "Failed to parse JSON from LLM response", if: JSON::ParserError
27
+
28
+ before do
29
+ if disabled?
30
+ exposures = stubbed_exposures
31
+ record_otel_attributes!(
32
+ input_tokens: exposures[:input_tokens],
33
+ output_tokens: exposures[:output_tokens],
34
+ cost: exposures[:cost],
35
+ response_model: nil,
36
+ stubbed: true,
37
+ )
38
+ done!("disabled - returning stubbed values", **exposures)
39
+ end
40
+ end
41
+
42
+ def call
43
+ expose(
44
+ response: parsed_response,
45
+ raw_message: llm_response,
46
+ input_tokens: llm_response.input_tokens,
47
+ output_tokens: llm_response.output_tokens,
48
+ cost_breakdown:,
49
+ cost: cost_breakdown&.total,
50
+ stubbed: false,
51
+ )
52
+ record_otel_attributes!(
53
+ input_tokens: llm_response.input_tokens,
54
+ output_tokens: llm_response.output_tokens,
55
+ cost: cost_breakdown&.total,
56
+ response_model: llm_response.model_id,
57
+ stubbed: false,
58
+ )
59
+ rescue ::RubyLLM::RateLimitError => e
60
+ fail! "Rate limit reached: #{e.message}"
61
+ end
62
+
63
+ private
64
+
65
+ def disabled? = !Axn::RubyLLM.configuration.enabled?
66
+
67
+ def stubbed_exposures
68
+ content = schema || json ? { "stubbed" => true } : "stubbed response value"
69
+ {
70
+ response: content,
71
+ raw_message: StubMessage.new(content:, input_tokens: 0, output_tokens: 0, model_id: "stubbed"),
72
+ input_tokens: 0,
73
+ output_tokens: 0,
74
+ cost: 0.0,
75
+ cost_breakdown: nil,
76
+ stubbed: true,
77
+ }
78
+ end
79
+
80
+ def parsed_response
81
+ if schema
82
+ # with_schema makes RubyLLM parse the response into a Hash on success
83
+ return llm_response.content if llm_response.content.is_a?(Hash)
84
+
85
+ fail! "Schema response was not valid JSON"
86
+ end
87
+ json ? JSON.parse(llm_response.content) : llm_response.content
88
+ end
89
+
90
+ def cost_breakdown
91
+ return nil unless model_info
92
+
93
+ llm_response.cost(model: model_info)
94
+ end
95
+
96
+ memo def model_info
97
+ ::RubyLLM.models.find(llm_response.model_id)
98
+ rescue ::RubyLLM::ModelNotFoundError
99
+ nil
100
+ end
101
+
102
+ memo def llm_response = chat.ask(prompt)
103
+
104
+ memo def chat
105
+ ::RubyLLM.chat(model: resolved_model).tap do |c|
106
+ c.with_instructions(system_prompt) if system_prompt
107
+ c.with_schema(schema) if schema
108
+ c.with_params(response_format: { type: "json_object" }) if json && !schema
109
+ c.with_params(temperature:) if temperature
110
+ end
111
+ end
112
+
113
+ def resolved_model
114
+ model || Axn::RubyLLM.configuration.default_model
115
+ end
116
+
117
+ def record_otel_attributes!(input_tokens:, output_tokens:, cost:, response_model:, stubbed:)
118
+ return unless defined?(::OpenTelemetry::Trace)
119
+
120
+ span = ::OpenTelemetry::Trace.current_span
121
+ return unless span&.context&.valid?
122
+
123
+ span.set_attribute("gen_ai.request.model", resolved_model) if resolved_model
124
+ span.set_attribute("gen_ai.response.model", response_model) if response_model
125
+ span.set_attribute("gen_ai.usage.input_tokens", input_tokens) if input_tokens
126
+ span.set_attribute("gen_ai.usage.output_tokens", output_tokens) if output_tokens
127
+ span.set_attribute("gen_ai.usage.cost", cost) if cost
128
+ span.set_attribute("axn.ruby_llm.stubbed", stubbed) unless stubbed.nil?
129
+ rescue StandardError
130
+ # never let telemetry break the action
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Axn
4
+ module RubyLLM
5
+ class Configuration
6
+ DEFAULT_MODEL = "gpt-4o-mini"
7
+
8
+ attr_accessor :default_model, :enabled
9
+
10
+ def initialize
11
+ @default_model = DEFAULT_MODEL
12
+ @enabled = true
13
+ end
14
+
15
+ def enabled?
16
+ enabled.respond_to?(:call) ? !!enabled.call : !!enabled
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "axn-ruby_llm"
4
+
5
+ module Axn
6
+ module RubyLLM
7
+ module RSpec
8
+ module Helpers
9
+ # Stubs RubyLLM so that Ask returns a canned response.
10
+ #
11
+ # Usage in a spec:
12
+ # stub_axn_ruby_llm(response: "Here is a summary.")
13
+ # stub_axn_ruby_llm(response: { "key" => "value" }) # auto-JSON-serialized for json: true calls
14
+ # stub_axn_ruby_llm(response: { "k" => "v" }, schema: MySchema) # Hash passed through unparsed
15
+ # stub_axn_ruby_llm(response: "...", input_tokens: 100, output_tokens: 50, cost: 0.0023)
16
+ #
17
+ # Returns the chat instance double for further assertions if needed.
18
+ def stub_axn_ruby_llm(response:, model: nil, schema: nil, input_tokens: nil, output_tokens: nil, cost: nil)
19
+ resolved_model_id = model || Axn::RubyLLM.configuration.default_model
20
+ llm_message = _stub_axn_ruby_llm_message(response, resolved_model_id, input_tokens, output_tokens, schema:)
21
+ chat_instance = _stub_axn_ruby_llm_chat(model, llm_message, schema:)
22
+ _stub_axn_ruby_llm_cost(llm_message, resolved_model_id, cost)
23
+ chat_instance
24
+ end
25
+
26
+ private
27
+
28
+ def _stub_axn_ruby_llm_message(response, model_id, input_tokens, output_tokens, schema:)
29
+ content = if schema
30
+ response
31
+ elsif response.is_a?(Hash)
32
+ response.to_json
33
+ else
34
+ response.to_s
35
+ end
36
+ instance_double(::RubyLLM::Message, content:, input_tokens:, output_tokens:, model_id:)
37
+ end
38
+
39
+ def _stub_axn_ruby_llm_chat(model, llm_message, schema:)
40
+ chat_instance = instance_double(::RubyLLM::Chat)
41
+ if model
42
+ allow(::RubyLLM).to receive(:chat).with(model:).and_return(chat_instance)
43
+ else
44
+ allow(::RubyLLM).to receive(:chat).and_return(chat_instance)
45
+ end
46
+ allow(chat_instance).to receive(:with_instructions).and_return(chat_instance)
47
+ allow(chat_instance).to receive(:with_params).and_return(chat_instance)
48
+ # Always stub with_schema so specs don't blow up if production code passes schema:
49
+ # even when the helper is called without schema:. Use a tight matcher when schema
50
+ # is known so the stub still validates the correct class is passed.
51
+ if schema
52
+ allow(chat_instance).to receive(:with_schema).with(schema).and_return(chat_instance)
53
+ else
54
+ allow(chat_instance).to receive(:with_schema).and_return(chat_instance)
55
+ end
56
+ allow(chat_instance).to receive(:ask).and_return(llm_message)
57
+ chat_instance
58
+ end
59
+
60
+ def _stub_axn_ruby_llm_cost(llm_message, model_id, cost)
61
+ model_info = instance_double("RubyLLM::Model")
62
+ allow(::RubyLLM.models).to receive(:find).with(model_id).and_return(model_info)
63
+ # Default to zero cost so specs exercise the "model found, cost computed" path.
64
+ # Pass cost: explicitly to assert a specific value.
65
+ cost_total = cost || 0.0
66
+ cost_struct = instance_double(::RubyLLM::Cost, total: cost_total)
67
+ allow(llm_message).to receive(:cost).with(model: model_info).and_return(cost_struct)
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+
74
+ if defined?(RSpec)
75
+ RSpec.configure do |config|
76
+ config.include Axn::RubyLLM::RSpec::Helpers
77
+ end
78
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Axn
4
+ module RubyLLM
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby_llm"
4
+ require "axn"
5
+
6
+ require_relative "ruby_llm/version"
7
+ require_relative "ruby_llm/configuration"
8
+ require_relative "ruby_llm/ask"
9
+
10
+ module Axn
11
+ module RubyLLM
12
+ class << self
13
+ def configuration
14
+ @configuration ||= Configuration.new
15
+ end
16
+
17
+ def configure
18
+ yield configuration
19
+ end
20
+
21
+ def reset_configuration!
22
+ @configuration = nil
23
+ end
24
+
25
+ def ask(**) = Ask.call(**)
26
+ def ask!(**) = Ask.call!(**)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "axn/ruby_llm"
metadata ADDED
@@ -0,0 +1,95 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: axn-ruby_llm
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Kali Donovan
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: axn
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 0.1.0.pre.alpha.4.2
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: 0.2.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: 0.1.0.pre.alpha.4.2
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: 0.2.0
32
+ - !ruby/object:Gem::Dependency
33
+ name: ruby_llm
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '1.0'
39
+ - - "<"
40
+ - !ruby/object:Gem::Version
41
+ version: '2.0'
42
+ type: :runtime
43
+ prerelease: false
44
+ version_requirements: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '1.0'
49
+ - - "<"
50
+ - !ruby/object:Gem::Version
51
+ version: '2.0'
52
+ description: Call LLMs from Axn actions using RubyLLM, with structured error handling,
53
+ optional JSON mode, and cost/token tracking.
54
+ email:
55
+ - kali@teamshares.com
56
+ executables: []
57
+ extensions: []
58
+ extra_rdoc_files: []
59
+ files:
60
+ - CHANGELOG.md
61
+ - LICENSE
62
+ - README.md
63
+ - Rakefile
64
+ - lib/axn-ruby_llm.rb
65
+ - lib/axn/ruby_llm.rb
66
+ - lib/axn/ruby_llm/ask.rb
67
+ - lib/axn/ruby_llm/configuration.rb
68
+ - lib/axn/ruby_llm/rspec.rb
69
+ - lib/axn/ruby_llm/version.rb
70
+ homepage: https://github.com/teamshares/axn-ruby_llm
71
+ licenses:
72
+ - MIT
73
+ metadata:
74
+ homepage_uri: https://github.com/teamshares/axn-ruby_llm
75
+ source_code_uri: https://github.com/teamshares/axn-ruby_llm
76
+ changelog_uri: https://github.com/teamshares/axn-ruby_llm/blob/main/CHANGELOG.md
77
+ rubygems_mfa_required: 'true'
78
+ rdoc_options: []
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: 3.2.1
86
+ required_rubygems_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ requirements: []
92
+ rubygems_version: 3.6.8
93
+ specification_version: 4
94
+ summary: RubyLLM wrapper for Axn actions
95
+ test_files: []