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.
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
- options = ScriptOptions.new(args).parse
11
- generator_opts = options.slice(:message, :key, :ai_service, :ai_model, :number, :verbose)
12
- formatter_opts = options.slice(:format, :verbose)
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
- puts "Options: #{options.inspect}" if options[:verbose]
15
- formatter = Formats::Formatter.new(formatter_opts)
16
- message_generator = Generators::MessageGenerator.create(generator_opts)
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
- output = formatter.format(messages)
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.1.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: 2024-12-17 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
- name: ruby-openai
13
+ name: ruby_llm
15
14
  requirement: !ruby/object:Gem::Requirement
16
15
  requirements:
17
16
  - - "~>"
18
17
  - !ruby/object:Gem::Version
19
- version: '7.3'
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: '7.3'
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.5.23
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