rubocop-prompt 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.
@@ -0,0 +1,235 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubocop"
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Prompt
8
+ # Checks that temperature values are appropriate for the task type.
9
+ #
10
+ # This cop identifies code in classes, modules, or methods with "prompt" in their names
11
+ # and ensures that when temperature > 0.7, it's not being used for precision tasks.
12
+ # High temperature values (> 0.7) should be avoided for tasks requiring accuracy,
13
+ # consistency, or factual correctness.
14
+ #
15
+ # @example
16
+ # # bad (high temperature for precision task)
17
+ # OpenAI::Client.new.chat(
18
+ # parameters: {
19
+ # temperature: 0.9,
20
+ # messages: [{ role: "system", content: "Analyze this data accurately" }]
21
+ # }
22
+ # )
23
+ #
24
+ # # bad (high temperature with precision keywords)
25
+ # client.chat(
26
+ # temperature: 0.8,
27
+ # messages: [{ role: "user", content: "Calculate the exact result" }]
28
+ # )
29
+ #
30
+ # # good (low temperature for precision)
31
+ # OpenAI::Client.new.chat(
32
+ # parameters: {
33
+ # temperature: 0.3,
34
+ # messages: [{ role: "system", content: "Analyze this data accurately" }]
35
+ # }
36
+ # )
37
+ #
38
+ # # good (high temperature for creative task)
39
+ # OpenAI::Client.new.chat(
40
+ # parameters: {
41
+ # temperature: 0.9,
42
+ # messages: [{ role: "user", content: "Write a creative story" }]
43
+ # }
44
+ # )
45
+ class TemperatureRange < RuboCop::Cop::Base
46
+ MSG = "High temperature (%.1f > 0.7) should not be used for precision tasks. " \
47
+ "Consider using temperature <= 0.7 for tasks requiring accuracy."
48
+
49
+ # Temperature threshold above which we check for precision tasks
50
+ TEMPERATURE_THRESHOLD = 0.7
51
+
52
+ # Keywords that indicate precision/accuracy tasks
53
+ PRECISION_KEYWORDS = [
54
+ # Analysis and accuracy
55
+ "accurate", "accuracy", "precise", "precision", "exact", "exactly",
56
+ "analyze", "analysis", "calculate", "computation", "compute",
57
+ "measure", "measurement", "count", "sum", "total",
58
+ # Factual and data tasks
59
+ "fact", "factual", "data", "information", "correct", "verify",
60
+ "validate", "check", "review", "audit", "inspect",
61
+ # Classification and categorization
62
+ "classify", "classification", "categorize", "category",
63
+ "sort", "organize", "structure", "parse", "extract",
64
+ # Technical and code tasks
65
+ "code", "programming", "syntax", "debug", "error", "fix",
66
+ "technical", "documentation", "specification", "format"
67
+ ].freeze
68
+
69
+ def on_send(node)
70
+ return unless in_prompt_context?(node)
71
+ return unless chat_method?(node)
72
+
73
+ temperature_value = extract_temperature(node)
74
+ return unless temperature_value && temperature_value > TEMPERATURE_THRESHOLD
75
+
76
+ messages = extract_messages(node)
77
+ return unless messages && precision_task?(messages)
78
+
79
+ add_offense(
80
+ node,
81
+ message: format(MSG, temperature_value)
82
+ )
83
+ end
84
+
85
+ private
86
+
87
+ def in_prompt_context?(node)
88
+ # Check if we're inside a class, module, or method that contains "prompt"
89
+ node.each_ancestor(:class, :module, :def, :defs) do |ancestor|
90
+ return true if has_prompt_in_name?(ancestor)
91
+ end
92
+ false
93
+ end
94
+
95
+ def has_prompt_in_name?(node)
96
+ case node.type
97
+ when :class, :module
98
+ name_node = node.children[0]
99
+ if name_node.type == :const
100
+ name_node.children[1].to_s.downcase.include?("prompt")
101
+ else
102
+ false
103
+ end
104
+ when :def, :defs
105
+ node.method_name.to_s.downcase.include?("prompt")
106
+ else
107
+ false
108
+ end
109
+ end
110
+
111
+ def chat_method?(node)
112
+ return false unless node.type == :send
113
+
114
+ # Check for methods like chat, complete, or similar
115
+ method_name = node.method_name.to_s
116
+ %w[chat complete completion].include?(method_name)
117
+ end
118
+
119
+ def extract_temperature(node)
120
+ # Look for temperature in hash arguments
121
+ node.arguments.each do |arg|
122
+ next unless arg.type == :hash
123
+
124
+ temperature = find_temperature_in_hash(arg)
125
+ return temperature if temperature
126
+ end
127
+
128
+ nil
129
+ end
130
+
131
+ def find_temperature_in_hash(hash_node)
132
+ hash_node.pairs.each do |pair|
133
+ key = pair.key
134
+ value = pair.value
135
+
136
+ # Handle direct temperature key
137
+ return extract_numeric_value(value) if key_matches?(key, "temperature")
138
+
139
+ # Handle nested parameters hash
140
+ return find_temperature_in_hash(value) if key_matches?(key, "parameters") && value.type == :hash
141
+ end
142
+
143
+ nil
144
+ end
145
+
146
+ def key_matches?(key_node, target_key)
147
+ case key_node.type
148
+ when :sym
149
+ key_node.children[0].to_s == target_key
150
+ when :str
151
+ key_node.children[0] == target_key
152
+ else
153
+ false
154
+ end
155
+ end
156
+
157
+ def extract_numeric_value(value_node)
158
+ case value_node.type
159
+ when :float
160
+ value_node.children[0]
161
+ when :int
162
+ value_node.children[0].to_f
163
+ end
164
+ end
165
+
166
+ def extract_messages(node)
167
+ # Look for messages in hash arguments
168
+ node.arguments.each do |arg|
169
+ next unless arg.type == :hash
170
+
171
+ messages = find_messages_in_hash(arg)
172
+ return messages if messages
173
+ end
174
+
175
+ nil
176
+ end
177
+
178
+ def find_messages_in_hash(hash_node)
179
+ hash_node.pairs.each do |pair|
180
+ key = pair.key
181
+ value = pair.value
182
+
183
+ # Handle direct messages key
184
+ return extract_messages_content(value) if key_matches?(key, "messages")
185
+
186
+ # Handle nested parameters hash
187
+ return find_messages_in_hash(value) if key_matches?(key, "parameters") && value.type == :hash
188
+ end
189
+
190
+ nil
191
+ end
192
+
193
+ def extract_messages_content(messages_node)
194
+ return nil unless messages_node.type == :array
195
+
196
+ content_strings = []
197
+ messages_node.children.each do |message|
198
+ next unless message.type == :hash
199
+
200
+ message.pairs.each do |pair|
201
+ if key_matches?(pair.key, "content")
202
+ content = extract_string_content(pair.value)
203
+ content_strings << content if content
204
+ end
205
+ end
206
+ end
207
+
208
+ content_strings.join(" ")
209
+ end
210
+
211
+ def extract_string_content(value_node)
212
+ case value_node.type
213
+ when :str
214
+ value_node.children[0]
215
+ when :dstr
216
+ # Handle heredoc and interpolated strings
217
+ value_node.children.filter_map do |child|
218
+ child.children[0] if child.type == :str
219
+ end.join
220
+ end
221
+ end
222
+
223
+ def precision_task?(messages_content)
224
+ return false if messages_content.nil? || messages_content.strip.empty?
225
+
226
+ # Convert to lowercase for case-insensitive matching
227
+ content_lower = messages_content.downcase
228
+
229
+ # Check if any precision keywords are present
230
+ PRECISION_KEYWORDS.any? { |keyword| content_lower.include?(keyword) }
231
+ end
232
+ end
233
+ end
234
+ end
235
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lint_roller"
4
+
5
+ module RuboCop
6
+ module Prompt
7
+ # A plugin that integrates RuboCop Prompt with RuboCop's plugin system.
8
+ class Plugin < LintRoller::Plugin
9
+ def about
10
+ LintRoller::About.new(
11
+ name: "rubocop-prompt",
12
+ version: VERSION,
13
+ homepage: "https://github.com/your-username/rubocop-prompt",
14
+ description: "A RuboCop extension for analyzing and improving AI prompt quality in Ruby code."
15
+ )
16
+ end
17
+
18
+ def supported?(context)
19
+ context.engine == :rubocop
20
+ end
21
+
22
+ def rules(_context)
23
+ LintRoller::Rules.new(
24
+ type: :path,
25
+ config_format: :rubocop,
26
+ value: Pathname.new(__dir__).join("../../../config/default.yml")
27
+ )
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Prompt
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "prompt/version"
4
+ require_relative "prompt/plugin"
5
+ require_relative "cop/prompt/invalid_format"
6
+ require_relative "cop/prompt/critical_first_last"
7
+ require_relative "cop/prompt/system_injection"
8
+ require_relative "cop/prompt/max_tokens"
9
+ require_relative "cop/prompt/missing_stop"
10
+ require_relative "cop/prompt/temperature_range"
11
+
12
+ module RuboCop
13
+ module Prompt
14
+ class Error < StandardError; end
15
+ end
16
+ end
@@ -0,0 +1,6 @@
1
+ module Rubocop
2
+ module Prompt
3
+ VERSION: String
4
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+ end
6
+ end
metadata ADDED
@@ -0,0 +1,138 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rubocop-prompt
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Masumi Kawasaki
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: lint_roller
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: rubocop
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 1.72.0
33
+ - - "<"
34
+ - !ruby/object:Gem::Version
35
+ version: '2.0'
36
+ type: :runtime
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: 1.72.0
43
+ - - "<"
44
+ - !ruby/object:Gem::Version
45
+ version: '2.0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rubocop-ast
48
+ requirement: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: 1.44.0
53
+ - - "<"
54
+ - !ruby/object:Gem::Version
55
+ version: '2.0'
56
+ type: :runtime
57
+ prerelease: false
58
+ version_requirements: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: 1.44.0
63
+ - - "<"
64
+ - !ruby/object:Gem::Version
65
+ version: '2.0'
66
+ - !ruby/object:Gem::Dependency
67
+ name: tiktoken_ruby
68
+ requirement: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - "~>"
71
+ - !ruby/object:Gem::Version
72
+ version: 0.0.7
73
+ type: :runtime
74
+ prerelease: false
75
+ version_requirements: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - "~>"
78
+ - !ruby/object:Gem::Version
79
+ version: 0.0.7
80
+ description: This gem provides static analysis for common prompt engineering anti-patterns,
81
+ helping developers write better prompts for LLM interactions.
82
+ email:
83
+ - geeknees@gmail.com
84
+ executables: []
85
+ extensions: []
86
+ extra_rdoc_files: []
87
+ files:
88
+ - ".rspec"
89
+ - ".rubocop.yml"
90
+ - AGENTS.md
91
+ - CHANGELOG.md
92
+ - CODE_OF_CONDUCT.md
93
+ - LICENSE.txt
94
+ - README.md
95
+ - Rakefile
96
+ - config/default.yml
97
+ - docs/development-guidelines.md
98
+ - docs/project-overview.md
99
+ - docs/rubocop-integration.md
100
+ - lib/rubocop/cop/prompt/critical_first_last.rb
101
+ - lib/rubocop/cop/prompt/invalid_format.rb
102
+ - lib/rubocop/cop/prompt/max_tokens.rb
103
+ - lib/rubocop/cop/prompt/missing_stop.rb
104
+ - lib/rubocop/cop/prompt/system_injection.rb
105
+ - lib/rubocop/cop/prompt/temperature_range.rb
106
+ - lib/rubocop/prompt.rb
107
+ - lib/rubocop/prompt/plugin.rb
108
+ - lib/rubocop/prompt/version.rb
109
+ - sig/rubocop/prompt.rbs
110
+ homepage: https://github.com/geeknees/rubocop-prompt
111
+ licenses:
112
+ - MIT
113
+ metadata:
114
+ default_lint_roller_plugin: RuboCop::Prompt::Plugin
115
+ homepage_uri: https://github.com/geeknees/rubocop-prompt
116
+ source_code_uri: https://github.com/geeknees/rubocop-prompt
117
+ changelog_uri: https://github.com/geeknees/rubocop-prompt/blob/main/CHANGELOG.md
118
+ documentation_uri: https://github.com/geeknees/rubocop-prompt/blob/main/README.md
119
+ rubygems_mfa_required: 'true'
120
+ rdoc_options: []
121
+ require_paths:
122
+ - lib
123
+ required_ruby_version: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - ">="
126
+ - !ruby/object:Gem::Version
127
+ version: 3.1.0
128
+ required_rubygems_version: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
133
+ requirements: []
134
+ rubygems_version: 3.6.7
135
+ specification_version: 4
136
+ summary: A RuboCop extension for analyzing and improving AI prompt quality in Ruby
137
+ code.
138
+ test_files: []