feather-ai 0.4.0 → 0.5.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 +4 -4
- data/CHANGELOG.md +8 -0
- data/CLAUDE.md +30 -13
- data/README.md +8 -9
- data/lib/feather_ai/configuration.rb +2 -0
- data/lib/feather_ai/consensus.rb +11 -7
- data/lib/feather_ai/identifier.rb +15 -34
- data/lib/feather_ai/photography_tips.rb +2 -2
- data/lib/feather_ai/version.rb +1 -1
- data/lib/feather_ai.rb +0 -1
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fa4e06ced9d8d48c533d41f97148fba4c26f3ffa478a603c4562960d50e6594f
|
|
4
|
+
data.tar.gz: 6f04ec946376b964fbf0a049a7837f20bae6eab45690e802b22e4f03b3789bc3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 943e45ed67690b5791d953ff443e3d94ba9b1866f1143373ca08dc2ef59826720cbdd4a34eeb6d91cf8d2a626822738e068a9cae4750f92fbe8a4aa2a91b2cb7
|
|
7
|
+
data.tar.gz: 903b3b7968b1ebebc411e5e8c518649ca8b5cf617461cb630725b861a3dfaba8eed7e914b01803ff5b65c05d92d1806ac6607e4cc5ce235db5f74a9ed59d0d9e
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.0 - 2026-10-02
|
|
4
|
+
|
|
5
|
+
- **Breaking:** requires `ruby_llm` 2.x (fixes the ReDoS advisory in 1.x). Dev dependencies updated to clear all `bundle-audit` findings (rubocop no longer pulls in `mcp`).
|
|
6
|
+
- `Result#cost` now comes from RubyLLM's per-model pricing registry instead of a hardcoded Anthropic-only table, so Haiku and non-Anthropic models are priced correctly; `nil` when the registry has no price.
|
|
7
|
+
- `config.provider` is now a no-op kept for compatibility; RubyLLM 2 resolves the provider from the model.
|
|
8
|
+
- Fix: `media_resolution` (a Gemini `generationConfig` field) is only sent to Gemini models. It was previously sent to every provider, which Anthropic rejects.
|
|
9
|
+
- Fix: audio transcripts are now interpolated as text; previously the `RubyLLM::Transcription` object was interpolated.
|
|
10
|
+
|
|
3
11
|
## 0.4.0 - 2026-07-05
|
|
4
12
|
|
|
5
13
|
- Identification now returns ranked `candidates` (up to 3, each `{common_name:, species:, score:}`) on every `Result`, populated straight from the structured-output schema.
|
data/CLAUDE.md
CHANGED
|
@@ -42,35 +42,51 @@ bundle exec rake release
|
|
|
42
42
|
|
|
43
43
|
## Architecture
|
|
44
44
|
|
|
45
|
-
The gem's only runtime dependency is `ruby_llm
|
|
45
|
+
The gem's only runtime dependency is `ruby_llm` 2.x (which pulls in `schematist` for the schema DSL). All classes live under the `FeatherAi` module in `lib/feather_ai/`.
|
|
46
46
|
|
|
47
47
|
### Data Flow
|
|
48
48
|
|
|
49
|
-
`FeatherAi.identify(image, audio, location:, consensus:)` is the top-level entry point defined in `lib/feather_ai.rb`. It delegates to:
|
|
49
|
+
`FeatherAi.identify(image, audio, location:, consensus:)` is the top-level entry point defined in `lib/feather_ai.rb`. `image` accepts a single path or an array of paths (multiple angles of the same bird). It delegates to:
|
|
50
50
|
|
|
51
|
-
1. **Identifier** (`lib/feather_ai/identifier.rb`) — Core identification logic. Uses RubyLLM's vision for images and `RubyLLM.transcribe` for audio. When both inputs are provided, they're combined into a single multi-modal prompt. Uses `
|
|
51
|
+
1. **Identifier** (`lib/feather_ai/identifier.rb`) — Core identification logic. Uses RubyLLM's vision for images and `RubyLLM.transcribe` for audio. When both inputs are provided, they're combined into a single multi-modal prompt. Uses `Schematist::Schema` for structured output (including a `reasoning` field that forces step-by-step visual analysis before the identification); the parsed Hash is read via `response.parsed`, tokens via `response.tokens`, and USD cost via `response.cost.total` (RubyLLM's pricing registry). `config.media_resolution` is only sent to Gemini models.
|
|
52
52
|
|
|
53
|
-
2. **Consensus** (`lib/feather_ai/consensus.rb`) — When `consensus: true`, runs identification through
|
|
53
|
+
2. **Consensus** (`lib/feather_ai/consensus.rb`) — When `consensus: true`, runs identification through the configured `consensus_models` in parallel threads. Agreement is compared on normalized species name. If they agree, returns `confident: true`; if they disagree, returns both as `candidates` with low confidence. Token counts, cost, and duration are summed across models.
|
|
54
54
|
|
|
55
|
-
3. **Result** (`lib/feather_ai/result.rb`) — Immutable value object wrapping all identification output.
|
|
55
|
+
3. **Result** (`lib/feather_ai/result.rb`) — Immutable value object wrapping all identification output. Identification fields: `common_name`, `species`, `family`, `confidence` (`:high`/`:medium`/`:low`), `confident?`, `region_native?`, `reasoning`, `candidates`, `photography_tips`, `to_h`. Observability fields from the LLM call: `model_id`, `input_tokens`, `output_tokens`, `cost`, `duration_ms`, `source` (`:vision`/`:audio`/`:multimodal`), `consensus_models`. Photography tips are lazy-loaded via a second cheap LLM call only when accessed.
|
|
56
56
|
|
|
57
|
-
4. **PhotographyTips** (`lib/feather_ai/photography_tips.rb`) — Separate LLM call (small model) returning structured shooting advice for the identified species. Only invoked when `result.photography_tips` is called.
|
|
57
|
+
4. **PhotographyTips** (`lib/feather_ai/photography_tips.rb`) — Separate LLM call (uses `tips_model`, a small model) returning structured shooting advice for the identified species. Only invoked when `result.photography_tips` is called.
|
|
58
|
+
|
|
59
|
+
### Instrumentation
|
|
60
|
+
|
|
61
|
+
`Instrumentation.instrument` (`lib/feather_ai/instrumentation.rb`) wraps identification in `ActiveSupport::Notifications` when ActiveSupport is loaded, and is a plain `yield` otherwise. Events: `identify.feather_ai` (Identifier) and `consensus.feather_ai` (Consensus); the `Result` is added to the payload after the call completes.
|
|
62
|
+
|
|
63
|
+
### Errors
|
|
64
|
+
|
|
65
|
+
All errors inherit from `FeatherAi::Error`: `ConfigurationError` (e.g. no image or audio provided) and `IdentificationError` (LLM call failure).
|
|
58
66
|
|
|
59
67
|
### Configuration
|
|
60
68
|
|
|
61
69
|
```ruby
|
|
62
70
|
FeatherAi.configure do |c|
|
|
63
|
-
c.provider
|
|
64
|
-
c.
|
|
65
|
-
c.
|
|
71
|
+
c.provider = :anthropic # default
|
|
72
|
+
c.model = "claude-sonnet-4" # default
|
|
73
|
+
c.location = "Perth, Western Australia" # biases results to local species
|
|
74
|
+
c.consensus_models = ["claude-sonnet-4", "claude-haiku-4"] # default
|
|
75
|
+
c.tips_model = "claude-haiku-4" # default
|
|
76
|
+
c.media_resolution = :high # default; Gemini-only image resolution
|
|
66
77
|
end
|
|
67
78
|
```
|
|
68
79
|
|
|
69
|
-
Location can be set globally or per-call via `location:` keyword. It's injected into the system prompt to reduce false positives.
|
|
80
|
+
`FeatherAi.configuration` is a lazily-initialized process singleton; `FeatherAi.reset!` clears it (used before every spec). Location can be set globally or per-call via the `location:` keyword. It's injected into the system prompt to reduce false positives.
|
|
70
81
|
|
|
71
82
|
### Rails Integration
|
|
72
83
|
|
|
73
|
-
`lib/feather_ai/rails/` contains a Railtie and `acts_as_sighting` mixin. When included in an ActiveRecord model, it expects `photo` (ActiveStorage) and `location` (string) attributes, and adds
|
|
84
|
+
`lib/feather_ai/rails/` contains a Railtie and `acts_as_sighting` mixin. When included in an ActiveRecord model, it expects `photo` (ActiveStorage) and `location` (string) attributes, and adds:
|
|
85
|
+
|
|
86
|
+
- `identify!` — downloads the photo, runs identification, persists species fields, returns the `Result`.
|
|
87
|
+
- `correct!(attrs)` / `corrected?` / `correction_delta` — human corrections of AI identifications, stored in `corrected_*` columns with a `corrected_at` timestamp. `correction_delta` always diffs against the original AI values.
|
|
88
|
+
|
|
89
|
+
Generators: `rails generate feather_ai:install [model_name]` scaffolds the identification-columns migration; `rails generate feather_ai:add_corrections [model_name]` adds the correction columns. Templates live in `lib/generators/feather_ai/templates/`.
|
|
74
90
|
|
|
75
91
|
## Code Style
|
|
76
92
|
|
|
@@ -81,7 +97,8 @@ Location can be set globally or per-call via `location:` keyword. It's injected
|
|
|
81
97
|
|
|
82
98
|
## Testing
|
|
83
99
|
|
|
84
|
-
-
|
|
85
|
-
-
|
|
100
|
+
- Specs stub RubyLLM directly with `instance_double`s (e.g. `allow(RubyLLM).to receive(:chat)`) — no API keys needed. VCR + WebMock are configured in `spec_helper.rb` but mainly serve to block real HTTP; there are no recorded cassettes.
|
|
101
|
+
- `spec/support/helpers.rb` provides `build_result(overrides)` for constructing `FeatherAi::Result` test objects.
|
|
102
|
+
- `FeatherAi.reset!` runs before every example, so config is always at defaults unless the spec configures it.
|
|
86
103
|
- SimpleCov for coverage reporting
|
|
87
104
|
- Dev test dependencies go in the `Gemfile`, not the gemspec
|
data/README.md
CHANGED
|
@@ -22,17 +22,17 @@ gem install feather-ai
|
|
|
22
22
|
|
|
23
23
|
```ruby
|
|
24
24
|
FeatherAi.configure do |c|
|
|
25
|
-
c.provider = :anthropic #
|
|
25
|
+
c.provider = :anthropic # Deprecated no-op; RubyLLM 2 resolves the provider from the model
|
|
26
26
|
c.model = "claude-sonnet-4-5" # Default: "claude-sonnet-4-5"
|
|
27
27
|
c.location = "Perth, WA" # Optional: biases results to local species
|
|
28
28
|
c.consensus_models = ["claude-sonnet-4-5", "claude-haiku-4-5"] # Models used in consensus mode
|
|
29
29
|
c.tips_model = "claude-haiku-4-5" # Model for photography tips (default)
|
|
30
|
-
c.media_resolution = :high #
|
|
30
|
+
c.media_resolution = :high # Gemini only (generationConfig.mediaResolution); ignored elsewhere
|
|
31
31
|
c.tools = [] # RubyLLM tools available to every identification (default: none)
|
|
32
32
|
end
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
RubyLLM must be configured with your provider credentials before using FeatherAi. See the [RubyLLM docs](https://
|
|
35
|
+
Requires RubyLLM 2.x. RubyLLM must be configured with your provider credentials before using FeatherAi. See the [RubyLLM docs](https://rubyllm.com) for setup.
|
|
36
36
|
|
|
37
37
|
## Usage
|
|
38
38
|
|
|
@@ -100,7 +100,7 @@ Pass RubyLLM tools so the model can verify its identification against real data
|
|
|
100
100
|
```ruby
|
|
101
101
|
class SpeciesLookupTool < RubyLLM::Tool
|
|
102
102
|
description "Looks up whether a species occurs in a region"
|
|
103
|
-
|
|
103
|
+
parameter :species, description: "Scientific species name"
|
|
104
104
|
|
|
105
105
|
def execute(species:)
|
|
106
106
|
Species.find_by(scientific_name: species)&.slice(:regions, :description) || { found: false }
|
|
@@ -114,7 +114,7 @@ Tools can also be set globally via `c.tools` in configuration. Note: Gemini reje
|
|
|
114
114
|
|
|
115
115
|
### Consensus Mode
|
|
116
116
|
|
|
117
|
-
Run identification through two models independently. When both agree on species, you get high confidence. When they disagree,
|
|
117
|
+
Run identification through two models independently. When both agree on species, you get high confidence. When they disagree, the top-vote candidate (ties break in `consensus_models` order) is still returned as the primary identification with `:low` confidence, and `candidates` carries the full ranked list:
|
|
118
118
|
|
|
119
119
|
```ruby
|
|
120
120
|
result = FeatherAi.identify("path/to/bird.jpg", consensus: true)
|
|
@@ -122,7 +122,7 @@ result = FeatherAi.identify("path/to/bird.jpg", consensus: true)
|
|
|
122
122
|
if result.confident?
|
|
123
123
|
puts "Both models agree: #{result.species}"
|
|
124
124
|
else
|
|
125
|
-
puts "Models disagree:"
|
|
125
|
+
puts "Models disagree — best guess: #{result.species}"
|
|
126
126
|
result.candidates.each { |c| puts " #{c[:common_name]} (#{c[:species]}) — #{c[:score]}" }
|
|
127
127
|
end
|
|
128
128
|
```
|
|
@@ -172,7 +172,7 @@ Every result also carries observability data from the LLM call:
|
|
|
172
172
|
| `model_id` | String | Model that produced the identification |
|
|
173
173
|
| `input_tokens` | Integer | Tokens sent to the model |
|
|
174
174
|
| `output_tokens` | Integer | Tokens received from the model |
|
|
175
|
-
| `cost` | Float |
|
|
175
|
+
| `cost` | Float | USD cost from RubyLLM's model pricing registry, or `nil` when unknown |
|
|
176
176
|
| `duration_ms` | Integer | Wall-clock time of the LLM call in milliseconds |
|
|
177
177
|
| `source` | Symbol | `:vision`, `:audio`, or `:multimodal` |
|
|
178
178
|
| `consensus_models` | Array | Models used when consensus mode was enabled |
|
|
@@ -303,8 +303,7 @@ after { FeatherAi.reset! }
|
|
|
303
303
|
```ruby
|
|
304
304
|
# In an initialiser or boot file — before any threads are created
|
|
305
305
|
FeatherAi.configure do |c|
|
|
306
|
-
c.
|
|
307
|
-
c.model = "claude-sonnet-4"
|
|
306
|
+
c.model = "claude-sonnet-4-5"
|
|
308
307
|
end
|
|
309
308
|
```
|
|
310
309
|
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
module FeatherAi
|
|
4
4
|
# Configuration object for FeatherAi gem settings.
|
|
5
5
|
class Configuration
|
|
6
|
+
# provider: kept for backwards compatibility; RubyLLM 2 resolves the provider from the model.
|
|
7
|
+
# media_resolution: Gemini-only (generationConfig.mediaResolution); ignored for other providers.
|
|
6
8
|
attr_accessor :provider, :model, :location, :consensus_models, :tips_model, :media_resolution, :tools
|
|
7
9
|
|
|
8
10
|
def initialize
|
data/lib/feather_ai/consensus.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module FeatherAi
|
|
4
4
|
# Multi-model consensus identification to improve accuracy.
|
|
5
|
-
# rubocop:disable Metrics/ClassLength
|
|
5
|
+
# rubocop:disable-next Metrics/ClassLength
|
|
6
6
|
class Consensus
|
|
7
7
|
def initialize(config: FeatherAi.configuration)
|
|
8
8
|
@config = config
|
|
@@ -74,24 +74,29 @@ module FeatherAi
|
|
|
74
74
|
}
|
|
75
75
|
end
|
|
76
76
|
|
|
77
|
+
# On disagreement the top-ranked candidate is promoted to the primary
|
|
78
|
+
# identification (still :low confidence) so consumers always get a usable
|
|
79
|
+
# name — the full ranked list stays in candidates.
|
|
77
80
|
def disagreed_result_attrs(results)
|
|
81
|
+
candidates = ranked_candidates(results)
|
|
78
82
|
{
|
|
79
|
-
common_name:
|
|
80
|
-
species:
|
|
83
|
+
common_name: candidates.first[:common_name],
|
|
84
|
+
species: candidates.first[:species],
|
|
81
85
|
family: calculate_agreed_family(results),
|
|
82
86
|
confidence: :low,
|
|
83
87
|
region_native: false,
|
|
84
88
|
model_id: nil,
|
|
85
|
-
candidates:
|
|
89
|
+
candidates: candidates
|
|
86
90
|
}
|
|
87
91
|
end
|
|
88
92
|
|
|
89
|
-
# Rank disagreeing identifications by vote share across the consensus
|
|
93
|
+
# Rank disagreeing identifications by vote share across the consensus
|
|
94
|
+
# models. Ties break by consensus_models order (sort_by isn't stable).
|
|
90
95
|
def ranked_candidates(results)
|
|
91
96
|
results.group_by { |r| r.species&.strip&.downcase }
|
|
92
97
|
.values
|
|
93
98
|
.map { |group| vote_candidate(group, results.size) }
|
|
94
|
-
.sort_by { |candidate| -candidate[:score] }
|
|
99
|
+
.sort_by.with_index { |candidate, index| [-candidate[:score], index] }
|
|
95
100
|
end
|
|
96
101
|
|
|
97
102
|
def vote_candidate(group, total)
|
|
@@ -133,5 +138,4 @@ module FeatherAi
|
|
|
133
138
|
dup_config
|
|
134
139
|
end
|
|
135
140
|
end
|
|
136
|
-
# rubocop:enable Metrics/ClassLength
|
|
137
141
|
end
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module FeatherAi
|
|
4
4
|
# Core bird identification using LLM vision and audio transcription.
|
|
5
|
-
# rubocop:disable Metrics/ClassLength
|
|
5
|
+
# rubocop:disable-next Metrics/ClassLength
|
|
6
6
|
class Identifier
|
|
7
|
-
SCHEMA =
|
|
7
|
+
SCHEMA = Schematist::Schema.create do
|
|
8
8
|
string :reasoning,
|
|
9
9
|
description: "Step-by-step visual analysis: describe body size, bill shape, " \
|
|
10
10
|
"plumage, markings, and rule out similar species before identifying"
|
|
@@ -24,12 +24,6 @@ module FeatherAi
|
|
|
24
24
|
end
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
-
# Approximate mid-2025 rates (USD per 1M tokens).
|
|
28
|
-
# Use your provider's dashboard for billing accuracy — these are estimates.
|
|
29
|
-
PROVIDER_RATES = {
|
|
30
|
-
anthropic: { input: 3.00, output: 15.00 }
|
|
31
|
-
}.freeze
|
|
32
|
-
|
|
33
27
|
def initialize(config: FeatherAi.configuration)
|
|
34
28
|
@config = config
|
|
35
29
|
end
|
|
@@ -99,21 +93,20 @@ module FeatherAi
|
|
|
99
93
|
chat.with_instructions(system_prompt(location, tools))
|
|
100
94
|
chat.with_schema(SCHEMA)
|
|
101
95
|
chat.with_tools(*tools) if tools.any?
|
|
102
|
-
chat
|
|
96
|
+
apply_media_resolution(chat)
|
|
103
97
|
chat
|
|
104
98
|
end
|
|
105
99
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
params
|
|
100
|
+
# generationConfig.mediaResolution is a Gemini request field; other providers reject unknown keys.
|
|
101
|
+
def apply_media_resolution(chat)
|
|
102
|
+
return unless @config.media_resolution && chat.provider.slug == "gemini"
|
|
103
|
+
|
|
104
|
+
resolution = "MEDIA_RESOLUTION_#{@config.media_resolution.to_s.upcase}"
|
|
105
|
+
chat.with_provider_options(generationConfig: { mediaResolution: resolution })
|
|
113
106
|
end
|
|
114
107
|
|
|
115
108
|
def build_result(response, duration_ms, source)
|
|
116
|
-
parsed = response.
|
|
109
|
+
parsed = response.parsed
|
|
117
110
|
Result.new(
|
|
118
111
|
**parsed_identification_attrs(parsed),
|
|
119
112
|
**response_observability_attrs(response, duration_ms, source)
|
|
@@ -145,10 +138,10 @@ module FeatherAi
|
|
|
145
138
|
|
|
146
139
|
def response_observability_attrs(response, duration_ms, source)
|
|
147
140
|
{
|
|
148
|
-
model_id: response.
|
|
149
|
-
input_tokens: response.
|
|
150
|
-
output_tokens: response.
|
|
151
|
-
cost:
|
|
141
|
+
model_id: response.model,
|
|
142
|
+
input_tokens: response.tokens.input,
|
|
143
|
+
output_tokens: response.tokens.output,
|
|
144
|
+
cost: response.cost.total,
|
|
152
145
|
duration_ms: duration_ms,
|
|
153
146
|
source: source
|
|
154
147
|
}
|
|
@@ -174,17 +167,6 @@ module FeatherAi
|
|
|
174
167
|
end
|
|
175
168
|
end
|
|
176
169
|
|
|
177
|
-
# Returns a USD cost estimate based on token counts, or nil when the count
|
|
178
|
-
# is unavailable or the configured provider has no rate table defined here.
|
|
179
|
-
def compute_cost(input_tokens, output_tokens)
|
|
180
|
-
return nil if input_tokens.nil? || output_tokens.nil?
|
|
181
|
-
|
|
182
|
-
rates = PROVIDER_RATES[@config.provider]
|
|
183
|
-
return nil if rates.nil?
|
|
184
|
-
|
|
185
|
-
((input_tokens * rates[:input]) + (output_tokens * rates[:output])) / 1_000_000.0
|
|
186
|
-
end
|
|
187
|
-
|
|
188
170
|
def system_prompt(location, tools = [])
|
|
189
171
|
prompt = base_system_prompt
|
|
190
172
|
if location
|
|
@@ -214,7 +196,7 @@ module FeatherAi
|
|
|
214
196
|
def build_text_prompt(images, audio)
|
|
215
197
|
parts = []
|
|
216
198
|
if audio
|
|
217
|
-
transcript = RubyLLM.transcribe(audio)
|
|
199
|
+
transcript = RubyLLM.transcribe(audio).text
|
|
218
200
|
parts << "Bird call/song transcript: #{transcript}"
|
|
219
201
|
end
|
|
220
202
|
parts << identification_prompt(images.size, has_audio: !audio.nil?)
|
|
@@ -231,5 +213,4 @@ module FeatherAi
|
|
|
231
213
|
end
|
|
232
214
|
end
|
|
233
215
|
end
|
|
234
|
-
# rubocop:enable Metrics/ClassLength
|
|
235
216
|
end
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module FeatherAi
|
|
4
4
|
# Generates photography tips for identified bird species.
|
|
5
5
|
class PhotographyTips
|
|
6
|
-
SCHEMA =
|
|
6
|
+
SCHEMA = Schematist::Schema.create do
|
|
7
7
|
string :time_of_day, description: "Best time of day to photograph this species"
|
|
8
8
|
string :approach, description: "How to approach without disturbing the bird"
|
|
9
9
|
string :settings, description: "Recommended camera settings (shutter speed, aperture, ISO)"
|
|
@@ -32,7 +32,7 @@ module FeatherAi
|
|
|
32
32
|
def fetch_from_llm
|
|
33
33
|
chat = RubyLLM.chat(model: @config.tips_model)
|
|
34
34
|
chat.with_schema(SCHEMA)
|
|
35
|
-
chat.ask(prompt).
|
|
35
|
+
chat.ask(prompt).parsed
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def build_tips_hash(parsed)
|
data/lib/feather_ai/version.rb
CHANGED
data/lib/feather_ai.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: feather-ai
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brandyn Britton
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '
|
|
18
|
+
version: '2.0'
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: '
|
|
25
|
+
version: '2.0'
|
|
26
26
|
description: A Ruby gem for identifying birds from photos and audio using RubyLLM.
|
|
27
27
|
Adds multi-modal identification, location-aware results, multi-model consensus,
|
|
28
28
|
and a Rails integration.
|