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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +18 -0
- data/AGENTS.md +37 -0
- data/CHANGELOG.md +23 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +604 -0
- data/Rakefile +12 -0
- data/config/default.yml +31 -0
- data/docs/development-guidelines.md +31 -0
- data/docs/project-overview.md +32 -0
- data/docs/rubocop-integration.md +26 -0
- data/lib/rubocop/cop/prompt/critical_first_last.rb +146 -0
- data/lib/rubocop/cop/prompt/invalid_format.rb +95 -0
- data/lib/rubocop/cop/prompt/max_tokens.rb +111 -0
- data/lib/rubocop/cop/prompt/missing_stop.rb +145 -0
- data/lib/rubocop/cop/prompt/system_injection.rb +99 -0
- data/lib/rubocop/cop/prompt/temperature_range.rb +235 -0
- data/lib/rubocop/prompt/plugin.rb +31 -0
- data/lib/rubocop/prompt/version.rb +7 -0
- data/lib/rubocop/prompt.rb +16 -0
- data/sig/rubocop/prompt.rbs +6 -0
- metadata +138 -0
@@ -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,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
|
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: []
|