ruby_llm-text 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 +7 -0
- data/CHANGELOG.md +31 -0
- data/LICENSE +21 -0
- data/README.md +312 -0
- data/lib/ruby_llm/text/base.rb +27 -0
- data/lib/ruby_llm/text/classify.rb +29 -0
- data/lib/ruby_llm/text/configuration.rb +25 -0
- data/lib/ruby_llm/text/extract.rb +55 -0
- data/lib/ruby_llm/text/string_ext.rb +18 -0
- data/lib/ruby_llm/text/summarize.rb +34 -0
- data/lib/ruby_llm/text/translate.rb +26 -0
- data/lib/ruby_llm/text/version.rb +5 -0
- data/lib/ruby_llm/text.rb +40 -0
- metadata +127 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: bb717f8d64403316c14b7da6ae2354f5f72d60a2caae6c892ca3f9fe109343d8
|
|
4
|
+
data.tar.gz: 286d134ed832cee3a0289052b5b81693a6b70822080dc9f4fa3535c492f73198
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 7227c8ae62491a00d308d3d59751435f404d5ee1e405b71db323a3b4edab832dac23bbc7162ef530562bae28d58ef5a765ba89f790d0f4e07469b99964519873
|
|
7
|
+
data.tar.gz: bb9eea87d7aafb8251a49ea5137f073fc5eea5a0ffcf29a699c2251bde7cfd31131afb81d51f293befa5faba8e1f397ca0080cd0626f6c394db4fcd2729a5f25
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
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.0.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] - 2025-02-16
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Initial release of ruby_llm-text gem
|
|
14
|
+
- Core functionality for LLM text operations:
|
|
15
|
+
- `summarize` - Condense text into shorter summaries with configurable length
|
|
16
|
+
- `translate` - Translate text between languages
|
|
17
|
+
- `extract` - Extract structured data from unstructured text using schemas
|
|
18
|
+
- `classify` - Classify text into predefined categories
|
|
19
|
+
- Configuration system with per-method model overrides
|
|
20
|
+
- String extensions for Rails-like method chaining (optional)
|
|
21
|
+
- Integration with ruby_llm ecosystem
|
|
22
|
+
- Comprehensive test suite with 100% coverage
|
|
23
|
+
- GitHub Actions CI/CD pipeline
|
|
24
|
+
- Complete documentation and API reference
|
|
25
|
+
|
|
26
|
+
### Dependencies
|
|
27
|
+
- ruby_llm ~> 1.0 (core dependency)
|
|
28
|
+
- Ruby >= 3.2.0
|
|
29
|
+
|
|
30
|
+
[Unreleased]: https://github.com/patrols/ruby_llm-text/compare/v0.1.0...HEAD
|
|
31
|
+
[0.1.0]: https://github.com/patrols/ruby_llm-text/releases/tag/v0.1.0
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Patrick Rendal Olsen
|
|
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,312 @@
|
|
|
1
|
+
# ruby_llm-text
|
|
2
|
+
|
|
3
|
+
ActiveSupport-style LLM utilities for Ruby that make AI operations feel like native Ruby.
|
|
4
|
+
|
|
5
|
+
[](https://badge.fury.io/rb/ruby_llm-text)
|
|
6
|
+
[](https://github.com/patrols/ruby_llm-text/actions)
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
`ruby_llm-text` provides intuitive one-liner utility methods for common LLM tasks like summarizing text, translation, data extraction, and classification. It integrates seamlessly with the [ruby_llm](https://github.com/crmne/ruby_llm) ecosystem, providing a simple interface without requiring chat objects, message arrays, or configuration boilerplate.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
Add this line to your application's Gemfile:
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
gem 'ruby_llm-text'
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
And then execute:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
$ bundle install
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Or install it yourself as:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
$ gem install ruby_llm-text
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Quick Start
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
require 'ruby_llm/text'
|
|
36
|
+
|
|
37
|
+
# Configure ruby_llm with your API key
|
|
38
|
+
RubyLLM.configure do |config|
|
|
39
|
+
config.openai_api_key = ENV["OPENAI_API_KEY"]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Summarize text
|
|
43
|
+
long_article = "This is a very long article about..."
|
|
44
|
+
summary = RubyLLM::Text.summarize(long_article)
|
|
45
|
+
|
|
46
|
+
# Translate text
|
|
47
|
+
greeting = RubyLLM::Text.translate("Bonjour le monde", to: "en")
|
|
48
|
+
|
|
49
|
+
# Extract structured data
|
|
50
|
+
text = "My name is John and I am 30 years old."
|
|
51
|
+
data = RubyLLM::Text.extract(text, schema: { name: :string, age: :integer })
|
|
52
|
+
|
|
53
|
+
# Classify text
|
|
54
|
+
review = "This product is amazing!"
|
|
55
|
+
sentiment = RubyLLM::Text.classify(review, categories: ["positive", "negative", "neutral"])
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## API Reference
|
|
59
|
+
|
|
60
|
+
### Summarize
|
|
61
|
+
|
|
62
|
+
Condense text into a shorter summary.
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
RubyLLM::Text.summarize(text, length: :medium, max_words: nil, model: nil)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Parameters:**
|
|
69
|
+
- `text` (String): The text to summarize
|
|
70
|
+
- `length` (Symbol|String): Predefined length (`:short`, `:medium`, `:detailed`) or custom description
|
|
71
|
+
- `max_words` (Integer, optional): Maximum word count for summary
|
|
72
|
+
- `model` (String, optional): Specific model to use
|
|
73
|
+
|
|
74
|
+
**Examples:**
|
|
75
|
+
```ruby
|
|
76
|
+
# Basic usage
|
|
77
|
+
RubyLLM::Text.summarize("Long article text...")
|
|
78
|
+
|
|
79
|
+
# With length option
|
|
80
|
+
RubyLLM::Text.summarize(text, length: :short)
|
|
81
|
+
|
|
82
|
+
# With word limit
|
|
83
|
+
RubyLLM::Text.summarize(text, length: :medium, max_words: 50)
|
|
84
|
+
|
|
85
|
+
# Custom length description
|
|
86
|
+
RubyLLM::Text.summarize(text, length: "bullet points")
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Translate
|
|
90
|
+
|
|
91
|
+
Translate text between languages.
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
RubyLLM::Text.translate(text, to:, from: nil, model: nil)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Parameters:**
|
|
98
|
+
- `text` (String): The text to translate
|
|
99
|
+
- `to` (String): Target language (e.g., "en", "spanish", "français")
|
|
100
|
+
- `from` (String, optional): Source language for better accuracy
|
|
101
|
+
- `model` (String, optional): Specific model to use
|
|
102
|
+
|
|
103
|
+
**Examples:**
|
|
104
|
+
```ruby
|
|
105
|
+
# Basic translation
|
|
106
|
+
RubyLLM::Text.translate("Bonjour", to: "en")
|
|
107
|
+
|
|
108
|
+
# With source language specified
|
|
109
|
+
RubyLLM::Text.translate("Hola mundo", to: "en", from: "es")
|
|
110
|
+
|
|
111
|
+
# Natural language specifications
|
|
112
|
+
RubyLLM::Text.translate("Hello", to: "french")
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Extract
|
|
116
|
+
|
|
117
|
+
Extract structured data from unstructured text.
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
RubyLLM::Text.extract(text, schema:, model: nil)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**Parameters:**
|
|
124
|
+
- `text` (String): The text to extract data from
|
|
125
|
+
- `schema` (Hash): Data structure specification
|
|
126
|
+
- `model` (String, optional): Specific model to use
|
|
127
|
+
|
|
128
|
+
**Schema Types:**
|
|
129
|
+
- `:string` - Text fields
|
|
130
|
+
- `:integer`, `:number` - Numeric fields
|
|
131
|
+
- `:boolean` - True/false fields
|
|
132
|
+
- `:array` - List fields
|
|
133
|
+
|
|
134
|
+
**Examples:**
|
|
135
|
+
```ruby
|
|
136
|
+
# Extract person details
|
|
137
|
+
text = "John Smith is 30 years old and works as a software engineer in San Francisco."
|
|
138
|
+
schema = {
|
|
139
|
+
name: :string,
|
|
140
|
+
age: :integer,
|
|
141
|
+
profession: :string,
|
|
142
|
+
location: :string
|
|
143
|
+
}
|
|
144
|
+
data = RubyLLM::Text.extract(text, schema: schema)
|
|
145
|
+
|
|
146
|
+
# Extract product information
|
|
147
|
+
product_text = "iPhone 15 Pro costs $999 and has excellent reviews"
|
|
148
|
+
product_schema = {
|
|
149
|
+
name: :string,
|
|
150
|
+
price: :number,
|
|
151
|
+
currency: :string,
|
|
152
|
+
reviews: :array
|
|
153
|
+
}
|
|
154
|
+
product_data = RubyLLM::Text.extract(product_text, schema: product_schema)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Classify
|
|
158
|
+
|
|
159
|
+
Classify text into predefined categories.
|
|
160
|
+
|
|
161
|
+
```ruby
|
|
162
|
+
RubyLLM::Text.classify(text, categories:, model: nil)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**Parameters:**
|
|
166
|
+
- `text` (String): The text to classify
|
|
167
|
+
- `categories` (Array): List of possible categories
|
|
168
|
+
- `model` (String, optional): Specific model to use
|
|
169
|
+
|
|
170
|
+
**Examples:**
|
|
171
|
+
```ruby
|
|
172
|
+
# Sentiment analysis
|
|
173
|
+
review = "This product exceeded my expectations!"
|
|
174
|
+
sentiment = RubyLLM::Text.classify(review,
|
|
175
|
+
categories: ["positive", "negative", "neutral"]
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Topic classification
|
|
179
|
+
article = "The stock market reached new highs today..."
|
|
180
|
+
topic = RubyLLM::Text.classify(article,
|
|
181
|
+
categories: ["technology", "finance", "sports", "politics"]
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Priority classification
|
|
185
|
+
email = "URGENT: Server is down and customers can't access the site"
|
|
186
|
+
priority = RubyLLM::Text.classify(email,
|
|
187
|
+
categories: ["low", "medium", "high", "critical"]
|
|
188
|
+
)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Configuration
|
|
192
|
+
|
|
193
|
+
This gem uses `ruby_llm`'s configuration for API keys and default models:
|
|
194
|
+
|
|
195
|
+
```ruby
|
|
196
|
+
# Configure ruby_llm (API keys, default model, etc.)
|
|
197
|
+
RubyLLM.configure do |config|
|
|
198
|
+
config.openai_api_key = ENV["OPENAI_API_KEY"]
|
|
199
|
+
config.anthropic_api_key = ENV["ANTHROPIC_API_KEY"]
|
|
200
|
+
config.default_model = "gpt-4.1-mini"
|
|
201
|
+
end
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Optionally configure text-specific settings:
|
|
205
|
+
|
|
206
|
+
```ruby
|
|
207
|
+
RubyLLM::Text.configure do |config|
|
|
208
|
+
# Temperature for text operations (default: 0.3)
|
|
209
|
+
config.temperature = 0.3
|
|
210
|
+
|
|
211
|
+
# Method-specific model overrides (falls back to RubyLLM.config.default_model)
|
|
212
|
+
config.summarize_model = "gpt-4.1-mini"
|
|
213
|
+
config.translate_model = "claude-sonnet-4-5" # Use Claude for translation
|
|
214
|
+
config.extract_model = "gpt-4.1" # Use GPT-4 for extraction
|
|
215
|
+
config.classify_model = "gpt-4.1-mini"
|
|
216
|
+
end
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
**Per-call overrides:**
|
|
220
|
+
```ruby
|
|
221
|
+
# Override model for specific calls
|
|
222
|
+
RubyLLM::Text.summarize(text, model: "claude-sonnet-4-5")
|
|
223
|
+
|
|
224
|
+
# Pass additional options (passed through to ruby_llm)
|
|
225
|
+
RubyLLM::Text.translate(text, to: "es", temperature: 0.1)
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## String Extensions (Optional)
|
|
229
|
+
|
|
230
|
+
For a more Rails-like experience, you can enable String monkey-patching:
|
|
231
|
+
|
|
232
|
+
```ruby
|
|
233
|
+
require 'ruby_llm/text/string_ext'
|
|
234
|
+
|
|
235
|
+
# Now you can call methods directly on strings
|
|
236
|
+
"Long article text...".summarize
|
|
237
|
+
"Bonjour".translate(to: "en")
|
|
238
|
+
"John is 30".extract(schema: { name: :string, age: :integer })
|
|
239
|
+
"Great product!".classify(categories: ["positive", "negative"])
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## Integration with ruby_llm
|
|
243
|
+
|
|
244
|
+
This gem builds on top of [ruby_llm](https://github.com/crmne/ruby_llm) and inherits its configuration and model support:
|
|
245
|
+
|
|
246
|
+
- **Models**: Supports all ruby_llm models (OpenAI GPT, Anthropic Claude, etc.)
|
|
247
|
+
- **Configuration**: Uses ruby_llm's underlying configuration system
|
|
248
|
+
- **Error handling**: Inherits ruby_llm's robust error handling
|
|
249
|
+
|
|
250
|
+
## Error Handling
|
|
251
|
+
|
|
252
|
+
The gem provides clear error messages for common issues:
|
|
253
|
+
|
|
254
|
+
```ruby
|
|
255
|
+
# Missing required parameters
|
|
256
|
+
RubyLLM::Text.extract("text") # ArgumentError: schema is required
|
|
257
|
+
|
|
258
|
+
RubyLLM::Text.classify("text", categories: []) # ArgumentError: categories are required
|
|
259
|
+
|
|
260
|
+
# API errors are wrapped with context
|
|
261
|
+
RubyLLM::Text.summarize("text") # RubyLLM::Text::Error: LLM call failed: [original error]
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Development
|
|
265
|
+
|
|
266
|
+
After checking out the repo, run:
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
bin/setup # Install dependencies
|
|
270
|
+
rake test # Run tests
|
|
271
|
+
rake rubocop # Run linter
|
|
272
|
+
rake # Run tests and linter
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
## Testing
|
|
276
|
+
|
|
277
|
+
The test suite uses Mocha for mocking LLM API calls, ensuring reliable and fast tests without requiring API keys:
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
# Run all tests
|
|
281
|
+
bundle exec rake test
|
|
282
|
+
|
|
283
|
+
# Run tests with linting
|
|
284
|
+
bundle exec rake
|
|
285
|
+
|
|
286
|
+
# Run linting only
|
|
287
|
+
bundle exec rubocop
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Manual Testing
|
|
291
|
+
|
|
292
|
+
For manual testing with real API calls, use the provided test script:
|
|
293
|
+
|
|
294
|
+
```bash
|
|
295
|
+
# Set up your API key
|
|
296
|
+
export OPENAI_API_KEY="your-key"
|
|
297
|
+
# or
|
|
298
|
+
export ANTHROPIC_API_KEY="your-key"
|
|
299
|
+
|
|
300
|
+
# Run manual tests
|
|
301
|
+
bin/manual-test
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
This script tests all four methods with real LLM APIs and provides helpful output for verification.
|
|
305
|
+
|
|
306
|
+
## Contributing
|
|
307
|
+
|
|
308
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/patrols/ruby_llm-text.
|
|
309
|
+
|
|
310
|
+
## License
|
|
311
|
+
|
|
312
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module RubyLLM
|
|
2
|
+
module Text
|
|
3
|
+
module Base
|
|
4
|
+
def self.call_llm(prompt, model: nil, temperature: nil, schema: nil, **options)
|
|
5
|
+
model ||= RubyLLM.config.default_model
|
|
6
|
+
temperature ||= RubyLLM::Text.config.temperature
|
|
7
|
+
|
|
8
|
+
chat = RubyLLM.chat(model: model)
|
|
9
|
+
chat = chat.with_temperature(temperature)
|
|
10
|
+
chat = chat.with_schema(schema) if schema
|
|
11
|
+
|
|
12
|
+
# Apply any additional options
|
|
13
|
+
options.each do |key, value|
|
|
14
|
+
method_name = "with_#{key}"
|
|
15
|
+
chat = chat.send(method_name, value) if chat.respond_to?(method_name)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
response = chat.ask(prompt)
|
|
19
|
+
response.content
|
|
20
|
+
rescue => e
|
|
21
|
+
raise RubyLLM::Text::Error, "LLM call failed: #{e.message}"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class Error < StandardError; end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module RubyLLM
|
|
2
|
+
module Text
|
|
3
|
+
module Classify
|
|
4
|
+
def self.call(text, categories:, model: nil, **options)
|
|
5
|
+
model ||= RubyLLM::Text.config.model_for(:classify)
|
|
6
|
+
raise ArgumentError, "categories are required" if categories.empty?
|
|
7
|
+
|
|
8
|
+
prompt = build_prompt(text, categories)
|
|
9
|
+
Base.call_llm(prompt, model: model, **options)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def self.build_prompt(text, categories)
|
|
15
|
+
category_list = categories.map { |c| "- #{c}" }.join("\n")
|
|
16
|
+
|
|
17
|
+
<<~PROMPT
|
|
18
|
+
Classify the following text into one of these categories:
|
|
19
|
+
#{category_list}
|
|
20
|
+
|
|
21
|
+
Return only the category name, nothing else.
|
|
22
|
+
|
|
23
|
+
Text:
|
|
24
|
+
#{text}
|
|
25
|
+
PROMPT
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module RubyLLM
|
|
2
|
+
module Text
|
|
3
|
+
class Configuration
|
|
4
|
+
# Method-specific model overrides (optional)
|
|
5
|
+
# If not set, falls back to RubyLLM.config.default_model
|
|
6
|
+
attr_accessor :summarize_model, :translate_model,
|
|
7
|
+
:extract_model, :classify_model
|
|
8
|
+
|
|
9
|
+
# Default temperature for text operations
|
|
10
|
+
attr_accessor :temperature
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@temperature = 0.3
|
|
14
|
+
@summarize_model = nil
|
|
15
|
+
@translate_model = nil
|
|
16
|
+
@extract_model = nil
|
|
17
|
+
@classify_model = nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def model_for(method_name)
|
|
21
|
+
instance_variable_get("@#{method_name}_model") || RubyLLM.config.default_model
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
require "ruby_llm/schema"
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Text
|
|
5
|
+
module Extract
|
|
6
|
+
def self.call(text, schema: nil, model: nil, **options)
|
|
7
|
+
model ||= RubyLLM::Text.config.model_for(:extract)
|
|
8
|
+
raise ArgumentError, "schema is required for extraction" unless schema
|
|
9
|
+
|
|
10
|
+
# Convert simple hash schema to RubyLLM::Schema
|
|
11
|
+
schema_obj = build_schema(schema)
|
|
12
|
+
prompt = build_prompt(text, schema)
|
|
13
|
+
|
|
14
|
+
Base.call_llm(prompt, model: model, schema: schema_obj, **options)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def self.build_schema(schema)
|
|
20
|
+
# If already a schema object, return as-is
|
|
21
|
+
return schema if schema.respond_to?(:schema)
|
|
22
|
+
|
|
23
|
+
# Build dynamic schema class from hash
|
|
24
|
+
schema_class = Class.new(RubyLLM::Schema)
|
|
25
|
+
schema.each do |field, type|
|
|
26
|
+
case type
|
|
27
|
+
when :string
|
|
28
|
+
schema_class.string field
|
|
29
|
+
when :integer, :number
|
|
30
|
+
schema_class.number field
|
|
31
|
+
when :boolean
|
|
32
|
+
schema_class.boolean field
|
|
33
|
+
when :array
|
|
34
|
+
schema_class.array field
|
|
35
|
+
else
|
|
36
|
+
schema_class.string field # fallback to string
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
schema_class
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.build_prompt(text, schema)
|
|
43
|
+
fields = schema.keys.join(", ")
|
|
44
|
+
|
|
45
|
+
<<~PROMPT
|
|
46
|
+
Extract the following information from the text: #{fields}
|
|
47
|
+
Return the data as structured JSON matching the provided schema.
|
|
48
|
+
|
|
49
|
+
Text:
|
|
50
|
+
#{text}
|
|
51
|
+
PROMPT
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Optional String monkey-patching
|
|
2
|
+
class String
|
|
3
|
+
def summarize(**options)
|
|
4
|
+
RubyLLM::Text.summarize(self, **options)
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def translate(**options)
|
|
8
|
+
RubyLLM::Text.translate(self, **options)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def extract(**options)
|
|
12
|
+
RubyLLM::Text.extract(self, **options)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def classify(**options)
|
|
16
|
+
RubyLLM::Text.classify(self, **options)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module RubyLLM
|
|
2
|
+
module Text
|
|
3
|
+
module Summarize
|
|
4
|
+
LENGTHS = {
|
|
5
|
+
short: "1-2 sentences",
|
|
6
|
+
medium: "3-5 sentences",
|
|
7
|
+
detailed: "1-2 paragraphs"
|
|
8
|
+
}.freeze
|
|
9
|
+
|
|
10
|
+
def self.call(text, length: :medium, max_words: nil, model: nil, **options)
|
|
11
|
+
model ||= RubyLLM::Text.config.model_for(:summarize)
|
|
12
|
+
|
|
13
|
+
prompt = build_prompt(text, length: length, max_words: max_words)
|
|
14
|
+
Base.call_llm(prompt, model: model, **options)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def self.build_prompt(text, length:, max_words:)
|
|
20
|
+
length_instruction = LENGTHS[length] || length.to_s
|
|
21
|
+
word_limit = max_words ? " (maximum #{max_words} words)" : ""
|
|
22
|
+
|
|
23
|
+
<<~PROMPT
|
|
24
|
+
Summarize the following text.
|
|
25
|
+
Length: #{length_instruction}#{word_limit}
|
|
26
|
+
Return only the summary, no preamble or explanation.
|
|
27
|
+
|
|
28
|
+
Text:
|
|
29
|
+
#{text}
|
|
30
|
+
PROMPT
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module RubyLLM
|
|
2
|
+
module Text
|
|
3
|
+
module Translate
|
|
4
|
+
def self.call(text, to:, from: nil, model: nil, **options)
|
|
5
|
+
model ||= RubyLLM::Text.config.model_for(:translate)
|
|
6
|
+
|
|
7
|
+
prompt = build_prompt(text, to: to, from: from)
|
|
8
|
+
Base.call_llm(prompt, model: model, **options)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def self.build_prompt(text, to:, from:)
|
|
14
|
+
from_instruction = from ? "from #{from} " : ""
|
|
15
|
+
|
|
16
|
+
<<~PROMPT
|
|
17
|
+
Translate the following text #{from_instruction}to #{to}.
|
|
18
|
+
Return only the translated text, no explanation or notes.
|
|
19
|
+
|
|
20
|
+
Text:
|
|
21
|
+
#{text}
|
|
22
|
+
PROMPT
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
require "ruby_llm"
|
|
2
|
+
require_relative "text/version"
|
|
3
|
+
require_relative "text/configuration"
|
|
4
|
+
require_relative "text/base"
|
|
5
|
+
require_relative "text/summarize"
|
|
6
|
+
require_relative "text/translate"
|
|
7
|
+
require_relative "text/extract"
|
|
8
|
+
require_relative "text/classify"
|
|
9
|
+
|
|
10
|
+
module RubyLLM
|
|
11
|
+
module Text
|
|
12
|
+
class << self
|
|
13
|
+
def configure(&block)
|
|
14
|
+
config.instance_eval(&block) if block_given?
|
|
15
|
+
config
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def config
|
|
19
|
+
@config ||= Configuration.new
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Module-style API methods
|
|
23
|
+
def summarize(text, **options)
|
|
24
|
+
Summarize.call(text, **options)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def translate(text, **options)
|
|
28
|
+
Translate.call(text, **options)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def extract(text, **options)
|
|
32
|
+
Extract.call(text, **options)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def classify(text, **options)
|
|
36
|
+
Classify.call(text, **options)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ruby_llm-text
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Patrick Rendal Olsen
|
|
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: ruby_llm
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: minitest
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '5.20'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '5.20'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: mocha
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '2.0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '2.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rubocop-rails-omakase
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '1.0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '1.0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: rake
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '13.0'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '13.0'
|
|
82
|
+
description: Intuitive one-liner utility methods for common LLM tasks like summarize,
|
|
83
|
+
translate, extract, and classify.
|
|
84
|
+
email:
|
|
85
|
+
- patrick@rendal.me
|
|
86
|
+
executables: []
|
|
87
|
+
extensions: []
|
|
88
|
+
extra_rdoc_files: []
|
|
89
|
+
files:
|
|
90
|
+
- CHANGELOG.md
|
|
91
|
+
- LICENSE
|
|
92
|
+
- README.md
|
|
93
|
+
- lib/ruby_llm/text.rb
|
|
94
|
+
- lib/ruby_llm/text/base.rb
|
|
95
|
+
- lib/ruby_llm/text/classify.rb
|
|
96
|
+
- lib/ruby_llm/text/configuration.rb
|
|
97
|
+
- lib/ruby_llm/text/extract.rb
|
|
98
|
+
- lib/ruby_llm/text/string_ext.rb
|
|
99
|
+
- lib/ruby_llm/text/summarize.rb
|
|
100
|
+
- lib/ruby_llm/text/translate.rb
|
|
101
|
+
- lib/ruby_llm/text/version.rb
|
|
102
|
+
homepage: https://github.com/patrols/ruby_llm-text
|
|
103
|
+
licenses:
|
|
104
|
+
- MIT
|
|
105
|
+
metadata:
|
|
106
|
+
allowed_push_host: https://rubygems.org
|
|
107
|
+
homepage_uri: https://github.com/patrols/ruby_llm-text
|
|
108
|
+
source_code_uri: https://github.com/patrols/ruby_llm-text.git
|
|
109
|
+
changelog_uri: https://github.com/patrols/ruby_llm-text/blob/main/CHANGELOG.md
|
|
110
|
+
rdoc_options: []
|
|
111
|
+
require_paths:
|
|
112
|
+
- lib
|
|
113
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - ">="
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: 3.2.0
|
|
118
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
119
|
+
requirements:
|
|
120
|
+
- - ">="
|
|
121
|
+
- !ruby/object:Gem::Version
|
|
122
|
+
version: '0'
|
|
123
|
+
requirements: []
|
|
124
|
+
rubygems_version: 4.0.6
|
|
125
|
+
specification_version: 4
|
|
126
|
+
summary: ActiveSupport-style LLM utilities for Ruby
|
|
127
|
+
test_files: []
|