hyrum 0.1.0 → 0.2.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 +75 -0
- data/README.md +133 -14
- data/bin/hyrum +1 -2
- data/lib/hyrum/data/fake_messages.json +282 -0
- data/lib/hyrum/formats/formatter.rb +8 -2
- data/lib/hyrum/formats/templates/java.erb +9 -0
- data/lib/hyrum/formats/templates/javascript.erb +9 -0
- data/lib/hyrum/formats/templates/json.erb +13 -0
- data/lib/hyrum/formats/templates/python.erb +9 -0
- data/lib/hyrum/formats/templates/ruby.erb +9 -0
- data/lib/hyrum/formats/templates/text.erb +9 -0
- data/lib/hyrum/generators/ai_generator.rb +102 -0
- data/lib/hyrum/generators/fake_generator.rb +22 -34
- data/lib/hyrum/generators/message_generator.rb +17 -7
- data/lib/hyrum/script_options.rb +40 -16
- data/lib/hyrum/validators/lexical_diversity.rb +46 -0
- data/lib/hyrum/validators/quality_validator.rb +107 -0
- data/lib/hyrum/validators/semantic_similarity.rb +100 -0
- data/lib/hyrum/validators/validation_result.rb +22 -0
- data/lib/hyrum/version.rb +2 -2
- data/lib/hyrum.rb +156 -8
- metadata +13 -11
- data/lib/hyrum/generators/openai_generator.rb +0 -77
data/lib/hyrum.rb
CHANGED
|
@@ -2,25 +2,173 @@
|
|
|
2
2
|
|
|
3
3
|
require 'optparse'
|
|
4
4
|
require 'zeitwerk'
|
|
5
|
+
require 'dry-struct'
|
|
6
|
+
require 'dry-validation'
|
|
7
|
+
require 'ruby_llm'
|
|
8
|
+
|
|
9
|
+
# Configure RubyLLM with environment variables
|
|
10
|
+
RubyLLM.configure do |config|
|
|
11
|
+
config.openai_api_key = ENV['OPENAI_API_KEY'] if ENV['OPENAI_API_KEY']
|
|
12
|
+
config.anthropic_api_key = ENV['ANTHROPIC_API_KEY'] if ENV['ANTHROPIC_API_KEY']
|
|
13
|
+
config.gemini_api_key = ENV['GEMINI_API_KEY'] if ENV['GEMINI_API_KEY']
|
|
14
|
+
config.mistral_api_key = ENV['MISTRAL_API_KEY'] if ENV['MISTRAL_API_KEY']
|
|
15
|
+
config.deepseek_api_key = ENV['DEEPSEEK_API_KEY'] if ENV['DEEPSEEK_API_KEY']
|
|
16
|
+
config.perplexity_api_key = ENV['PERPLEXITY_API_KEY'] if ENV['PERPLEXITY_API_KEY']
|
|
17
|
+
config.openrouter_api_key = ENV['OPENROUTER_API_KEY'] if ENV['OPENROUTER_API_KEY']
|
|
18
|
+
config.ollama_api_base = 'http://localhost:11434/v1'
|
|
19
|
+
config.ollama_api_base = ENV['OLLAMA_API_BASE'] if ENV['OLLAMA_API_BASE']
|
|
20
|
+
config.gpustack_api_base = ENV['GPUSTACK_API_BASE'] if ENV['GPUSTACK_API_BASE']
|
|
21
|
+
config.gpustack_api_key = ENV['GPUSTACK_API_KEY'] if ENV['GPUSTACK_API_KEY']
|
|
22
|
+
end
|
|
23
|
+
|
|
5
24
|
loader = Zeitwerk::Loader.for_gem
|
|
6
25
|
loader.setup
|
|
7
26
|
|
|
27
|
+
module Types
|
|
28
|
+
include Dry.Types()
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class CLIOptions < Dry::Struct
|
|
32
|
+
attribute :message, Types::String.optional
|
|
33
|
+
attribute :key, Types::Coercible::Symbol.default(:status)
|
|
34
|
+
attribute :ai_service, Types::Coercible::Symbol.default(:fake)
|
|
35
|
+
attribute :ai_model, Types::Coercible::Symbol
|
|
36
|
+
attribute :number, Types::Integer.default(5)
|
|
37
|
+
attribute :format, Types::Coercible::Symbol.default(:text)
|
|
38
|
+
attribute :verbose, Types::Bool.default(false)
|
|
39
|
+
attribute :validate, Types::Bool.default(false)
|
|
40
|
+
attribute :min_quality, Types::Integer.default(70)
|
|
41
|
+
attribute :strict, Types::Bool.default(false)
|
|
42
|
+
attribute :show_scores, Types::Bool.default(false)
|
|
43
|
+
|
|
44
|
+
def self.build_and_validate(input)
|
|
45
|
+
# apply defaults and coercions
|
|
46
|
+
cli_options = new(input)
|
|
47
|
+
|
|
48
|
+
# validate the options
|
|
49
|
+
contract_result = CLIOptionsContract.new.call(cli_options.to_h)
|
|
50
|
+
|
|
51
|
+
if contract_result.errors.any?
|
|
52
|
+
error_messages = contract_result.errors.to_h.map do |key, errors|
|
|
53
|
+
error_text = errors.is_a?(Array) ? errors.join(', ') : errors
|
|
54
|
+
"Error with #{key}: #{error_text}"
|
|
55
|
+
end
|
|
56
|
+
raise Hyrum::ScriptOptionsError, error_messages.join("\n")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
contract_result
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
class GeneratorOptions < Dry::Struct
|
|
64
|
+
attribute :message, Types::String.optional
|
|
65
|
+
attribute :key, Types::Coercible::Symbol
|
|
66
|
+
attribute :ai_service, Types::Coercible::Symbol
|
|
67
|
+
attribute :ai_model, Types::Coercible::Symbol
|
|
68
|
+
attribute :number, Types::Integer
|
|
69
|
+
attribute :verbose, Types::Bool
|
|
70
|
+
|
|
71
|
+
def self.from_parent(parent)
|
|
72
|
+
new(parent.to_h.slice(:message, :key, :ai_service, :ai_model, :number, :verbose))
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
class FormatterOptions < Dry::Struct
|
|
77
|
+
attribute :format, Types::Coercible::Symbol
|
|
78
|
+
attribute :verbose, Types::Bool
|
|
79
|
+
attribute :show_scores, Types::Bool
|
|
80
|
+
|
|
81
|
+
def self.from_parent(parent)
|
|
82
|
+
new(parent.to_h.slice(:format, :verbose, :show_scores))
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
class ValidatorOptions < Dry::Struct
|
|
87
|
+
attribute :validate, Types::Bool
|
|
88
|
+
attribute :min_quality, Types::Integer
|
|
89
|
+
attribute :strict, Types::Bool
|
|
90
|
+
attribute :ai_service, Types::Coercible::Symbol
|
|
91
|
+
attribute :ai_model, Types::Coercible::Symbol
|
|
92
|
+
|
|
93
|
+
def self.from_parent(parent)
|
|
94
|
+
new(parent.to_h.slice(:validate, :min_quality, :strict, :ai_service, :ai_model))
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
class CLIOptionsContract < Dry::Validation::Contract
|
|
99
|
+
params do
|
|
100
|
+
required(:key).value(:symbol)
|
|
101
|
+
required(:ai_service).value(:symbol)
|
|
102
|
+
required(:ai_model).value(:symbol)
|
|
103
|
+
required(:number).value(:integer)
|
|
104
|
+
required(:format).value(:symbol)
|
|
105
|
+
optional(:verbose).value(:bool)
|
|
106
|
+
optional(:message).maybe(:string)
|
|
107
|
+
optional(:validate).value(:bool)
|
|
108
|
+
optional(:min_quality).value(:integer)
|
|
109
|
+
optional(:strict).value(:bool)
|
|
110
|
+
optional(:show_scores).value(:bool)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
rule(:number) do
|
|
114
|
+
key.failure('must be > 0') if value && value <= 0
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
rule(:min_quality) do
|
|
118
|
+
key.failure('must be between 0 and 100') if value && (value < 0 || value > 100)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
8
122
|
module Hyrum
|
|
123
|
+
# rubocop:disable Metrics/MethodLength
|
|
9
124
|
def self.run(args)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
125
|
+
parsed_options = ScriptOptions.new(args).parse
|
|
126
|
+
options = CLIOptions.build_and_validate(parsed_options)
|
|
127
|
+
|
|
128
|
+
generator_options = GeneratorOptions.from_parent(options)
|
|
129
|
+
formatter_options = FormatterOptions.from_parent(options)
|
|
130
|
+
validator_options = ValidatorOptions.from_parent(options)
|
|
13
131
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
132
|
+
if options[:verbose]
|
|
133
|
+
puts "Options: #{options.inspect}"
|
|
134
|
+
puts "Generator Options: #{generator_options.inspect}"
|
|
135
|
+
puts "Formatter Options: #{formatter_options.inspect}"
|
|
136
|
+
puts "Validator Options: #{validator_options.inspect}"
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Generate messages
|
|
140
|
+
formatter = Formats::Formatter.new(formatter_options)
|
|
141
|
+
message_generator = Generators::MessageGenerator.create(generator_options)
|
|
17
142
|
messages = message_generator.generate
|
|
18
|
-
|
|
143
|
+
|
|
144
|
+
# Validate if requested
|
|
145
|
+
validation_result = nil
|
|
146
|
+
if validator_options[:validate]
|
|
147
|
+
validator = Validators::QualityValidator.new(
|
|
148
|
+
options[:message],
|
|
149
|
+
messages,
|
|
150
|
+
validator_options.to_h
|
|
151
|
+
)
|
|
152
|
+
validation_result = validator.validate
|
|
153
|
+
|
|
154
|
+
if validation_result.failed? && validator_options[:strict]
|
|
155
|
+
warn "Quality validation failed:"
|
|
156
|
+
warn " Score: #{validation_result.score}/100"
|
|
157
|
+
warn " Semantic similarity: #{validation_result.semantic_similarity}%"
|
|
158
|
+
warn " Lexical diversity: #{validation_result.lexical_diversity}%"
|
|
159
|
+
validation_result.warnings.each { |w| warn " - #{w}" }
|
|
160
|
+
exit 1
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Format and output
|
|
165
|
+
output = formatter.format(messages, validation_result)
|
|
19
166
|
puts output
|
|
20
167
|
rescue ScriptOptionsError => e
|
|
21
168
|
puts e.message
|
|
22
|
-
exit
|
|
169
|
+
exit 1
|
|
23
170
|
end
|
|
171
|
+
# rubocop:enable Metrics/MethodLength
|
|
24
172
|
end
|
|
25
173
|
|
|
26
174
|
loader.eager_load
|
metadata
CHANGED
|
@@ -1,29 +1,28 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hyrum
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tracy Atteberry
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
|
-
name:
|
|
13
|
+
name: ruby_llm
|
|
15
14
|
requirement: !ruby/object:Gem::Requirement
|
|
16
15
|
requirements:
|
|
17
16
|
- - "~>"
|
|
18
17
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '
|
|
18
|
+
version: '1.9'
|
|
20
19
|
type: :runtime
|
|
21
20
|
prerelease: false
|
|
22
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
22
|
requirements:
|
|
24
23
|
- - "~>"
|
|
25
24
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '
|
|
25
|
+
version: '1.9'
|
|
27
26
|
- !ruby/object:Gem::Dependency
|
|
28
27
|
name: zeitwerk
|
|
29
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -45,8 +44,8 @@ executables:
|
|
|
45
44
|
- hyrum
|
|
46
45
|
extensions: []
|
|
47
46
|
extra_rdoc_files:
|
|
48
|
-
- README.md
|
|
49
47
|
- CHANGELOG.md
|
|
48
|
+
- README.md
|
|
50
49
|
files:
|
|
51
50
|
- CHANGELOG.md
|
|
52
51
|
- README.md
|
|
@@ -54,6 +53,7 @@ files:
|
|
|
54
53
|
- bin/hyrum
|
|
55
54
|
- bin/setup
|
|
56
55
|
- lib/hyrum.rb
|
|
56
|
+
- lib/hyrum/data/fake_messages.json
|
|
57
57
|
- lib/hyrum/formats/formatter.rb
|
|
58
58
|
- lib/hyrum/formats/templates/java.erb
|
|
59
59
|
- lib/hyrum/formats/templates/javascript.erb
|
|
@@ -61,10 +61,14 @@ files:
|
|
|
61
61
|
- lib/hyrum/formats/templates/python.erb
|
|
62
62
|
- lib/hyrum/formats/templates/ruby.erb
|
|
63
63
|
- lib/hyrum/formats/templates/text.erb
|
|
64
|
+
- lib/hyrum/generators/ai_generator.rb
|
|
64
65
|
- lib/hyrum/generators/fake_generator.rb
|
|
65
66
|
- lib/hyrum/generators/message_generator.rb
|
|
66
|
-
- lib/hyrum/generators/openai_generator.rb
|
|
67
67
|
- lib/hyrum/script_options.rb
|
|
68
|
+
- lib/hyrum/validators/lexical_diversity.rb
|
|
69
|
+
- lib/hyrum/validators/quality_validator.rb
|
|
70
|
+
- lib/hyrum/validators/semantic_similarity.rb
|
|
71
|
+
- lib/hyrum/validators/validation_result.rb
|
|
68
72
|
- lib/hyrum/version.rb
|
|
69
73
|
homepage: https://github.com/grymoire7/hyrum
|
|
70
74
|
licenses:
|
|
@@ -72,7 +76,6 @@ licenses:
|
|
|
72
76
|
metadata:
|
|
73
77
|
rubygems_mfa_required: 'true'
|
|
74
78
|
changelog_uri: https://github.com/grymoire7/hyrum/blob/main/CHANGELOG.md
|
|
75
|
-
post_install_message:
|
|
76
79
|
rdoc_options:
|
|
77
80
|
- "--title"
|
|
78
81
|
- Hyrum - Hyrum's Law Code Generator
|
|
@@ -94,8 +97,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
94
97
|
- !ruby/object:Gem::Version
|
|
95
98
|
version: '0'
|
|
96
99
|
requirements: []
|
|
97
|
-
rubygems_version: 3.
|
|
98
|
-
signing_key:
|
|
100
|
+
rubygems_version: 3.6.8
|
|
99
101
|
specification_version: 4
|
|
100
102
|
summary: A simple Ruby gem
|
|
101
103
|
test_files: []
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'openai'
|
|
4
|
-
require 'json'
|
|
5
|
-
require 'erb'
|
|
6
|
-
|
|
7
|
-
module Hyrum
|
|
8
|
-
module Generators
|
|
9
|
-
class OpenaiGenerator
|
|
10
|
-
attr_reader :options
|
|
11
|
-
|
|
12
|
-
def initialize(options)
|
|
13
|
-
@options = options
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def generate
|
|
17
|
-
configure
|
|
18
|
-
|
|
19
|
-
response = chat_response
|
|
20
|
-
puts "OpenAI response: #{JSON.pretty_generate(response)}" if options[:verbose]
|
|
21
|
-
content = response.dig('choices', 0, 'message', 'content')
|
|
22
|
-
JSON.parse(content)
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
private
|
|
26
|
-
|
|
27
|
-
def prompt
|
|
28
|
-
prompt = <<~PROMPT
|
|
29
|
-
Please provide <%= number %> alternative status messages for the following message:
|
|
30
|
-
`<%= message %>`. The messages should be unique and informative. The messages
|
|
31
|
-
should be returned as json in the format: `{ "<%= key %>": ['list', 'of', 'messages']}`
|
|
32
|
-
The key should be `"<%= key %>"` followed by the list of messages.
|
|
33
|
-
PROMPT
|
|
34
|
-
erb_hash = { key: options[:key], message: options[:message], number: options[:number] }
|
|
35
|
-
template = ERB.new(prompt, trim_mode: '-')
|
|
36
|
-
template.result_with_hash(erb_hash)
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def chat_response
|
|
40
|
-
client = OpenAI::Client.new
|
|
41
|
-
client.chat(parameters: chat_params)
|
|
42
|
-
rescue OpenAI::Error => e
|
|
43
|
-
puts "OpenAI::Error: #{e.message}"
|
|
44
|
-
exit
|
|
45
|
-
rescue Faraday::Error => e
|
|
46
|
-
puts "Faraday::Error: #{e.message}"
|
|
47
|
-
puts "Please check that the #{options[:ai_model]} model is valid."
|
|
48
|
-
exit
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def chat_params
|
|
52
|
-
{
|
|
53
|
-
model: options[:ai_model],
|
|
54
|
-
response_format: { type: 'json_object' },
|
|
55
|
-
messages: [{ role: 'user', content: prompt}],
|
|
56
|
-
temperature: 0.7
|
|
57
|
-
}
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def configure
|
|
61
|
-
OpenAI.configure do |config|
|
|
62
|
-
config.access_token = ENV.fetch('OPENAI_ACCESS_TOKEN') if options[:ai_service] == :openai
|
|
63
|
-
# config.log_errors = true # Use for development
|
|
64
|
-
config.organization_id = ENV['OPENAI_ORGANIZATION_ID'] if ENV['OPENAI_ORGANIZATION_ID']
|
|
65
|
-
config.request_timeout = 240
|
|
66
|
-
if options[:ai_service] == :ollama
|
|
67
|
-
config.uri_base = ENV['OLLAMA_URL'] || 'http://localhost:11434'
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
rescue KeyError => e
|
|
71
|
-
puts "Error: #{e.message}"
|
|
72
|
-
puts "Please set the OPENAI_ACCESS_TOKEN environment variable."
|
|
73
|
-
exit
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
end
|