rbs_goose 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/.env.sample +1 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +42 -0
  5. data/.ruby-version +1 -0
  6. data/CHANGELOG.md +5 -0
  7. data/CODE_OF_CONDUCT.md +84 -0
  8. data/Gemfile +20 -0
  9. data/Gemfile.lock +190 -0
  10. data/LICENSE.txt +21 -0
  11. data/README-EN-US.md +78 -0
  12. data/README.md +78 -0
  13. data/Rakefile +61 -0
  14. data/Steepfile +14 -0
  15. data/assets/logo.svg +1 -0
  16. data/lib/rbs_goose/configuration.rb +49 -0
  17. data/lib/rbs_goose/examples/rbs_samples/Steepfile +6 -0
  18. data/lib/rbs_goose/examples/rbs_samples/lib/email.rb +16 -0
  19. data/lib/rbs_goose/examples/rbs_samples/lib/person.rb +19 -0
  20. data/lib/rbs_goose/examples/rbs_samples/lib/phone.rb +22 -0
  21. data/lib/rbs_goose/examples/rbs_samples/refined/sig/email.rbs +11 -0
  22. data/lib/rbs_goose/examples/rbs_samples/refined/sig/person.rbs +13 -0
  23. data/lib/rbs_goose/examples/rbs_samples/refined/sig/phone.rbs +15 -0
  24. data/lib/rbs_goose/examples/rbs_samples/sig/email.rbs +11 -0
  25. data/lib/rbs_goose/examples/rbs_samples/sig/person.rbs +13 -0
  26. data/lib/rbs_goose/examples/rbs_samples/sig/phone.rbs +15 -0
  27. data/lib/rbs_goose/io/example.rb +27 -0
  28. data/lib/rbs_goose/io/example_group.rb +45 -0
  29. data/lib/rbs_goose/io/file.rb +64 -0
  30. data/lib/rbs_goose/io/target_group.rb +21 -0
  31. data/lib/rbs_goose/io/typed_ruby.rb +33 -0
  32. data/lib/rbs_goose/io.rb +12 -0
  33. data/lib/rbs_goose/templates/default_template.rb +49 -0
  34. data/lib/rbs_goose/templates.rb +8 -0
  35. data/lib/rbs_goose/type_inferrer.rb +13 -0
  36. data/lib/rbs_goose/version.rb +5 -0
  37. data/lib/rbs_goose.rb +38 -0
  38. data/rbs_collection.lock.yaml +188 -0
  39. data/rbs_collection.yaml +19 -0
  40. data/renovate.json +15 -0
  41. data/sig/rbs_goose/configuration.rbs +14 -0
  42. data/sig/rbs_goose/error.rbs +2 -0
  43. data/sig/rbs_goose/io/example.rbs +9 -0
  44. data/sig/rbs_goose/io/example_group.rbs +8 -0
  45. data/sig/rbs_goose/io/file.rbs +19 -0
  46. data/sig/rbs_goose/io/target_group.rbs +3 -0
  47. data/sig/rbs_goose/io/typed_ruby.rbs +9 -0
  48. data/sig/rbs_goose/io.rbs +2 -0
  49. data/sig/rbs_goose/templates/default_template.rbs +10 -0
  50. data/sig/rbs_goose/templates.rbs +2 -0
  51. data/sig/rbs_goose/type_inferrer.rbs +3 -0
  52. data/sig/rbs_goose.rbs +12 -0
  53. data/sig_ext/class.rbs +3 -0
  54. data/sig_ext/langchain/llm.rbs +19 -0
  55. metadata +128 -0
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openai'
4
+ require 'langchain'
5
+
6
+ module RbsGoose
7
+ class Configuration
8
+ def initialize(&block)
9
+ self.instruction = default_instruction
10
+ self.example_groups = default_example_groups
11
+ self.template_class = default_template_class
12
+ instance_eval(&block) if block_given?
13
+ end
14
+
15
+ attr_accessor :llm, :instruction, :example_groups, :template_class
16
+
17
+ def use_open_ai(open_ai_access_token, default_options: {})
18
+ @llm = ::Langchain::LLM::OpenAI.new(
19
+ api_key: open_ai_access_token,
20
+ default_options: {
21
+ completion_model_name: 'gpt-3.5-turbo-0613',
22
+ chat_completion_model_name: 'gpt-3.5-turbo-0613'
23
+ }.merge(default_options)
24
+ )
25
+ end
26
+
27
+ def template
28
+ @template ||= template_class.new(instruction: instruction, example_groups: example_groups)
29
+ end
30
+
31
+ private
32
+
33
+ def default_template_class
34
+ Templates::DefaultTemplate
35
+ end
36
+
37
+ def default_instruction
38
+ <<~INSTRUCTION
39
+ Act as Ruby type inferrer.
40
+ When ruby source codes and RBS type signatures are given, refine each RBS type signatures. Each file should be split in markdown code format.
41
+ Use class names, variable names, etc., to infer type.
42
+ INSTRUCTION
43
+ end
44
+
45
+ def default_example_groups
46
+ [RbsGoose::IO::ExampleGroup.default_examples[:rbs_samples]]
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,6 @@
1
+ target :lib do
2
+ # signature "sig"
3
+ signature "refined/sig"
4
+
5
+ check "lib"
6
+ end
@@ -0,0 +1,16 @@
1
+ class Email
2
+ # @dynamic address
3
+ attr_reader :address
4
+
5
+ def initialize(address:)
6
+ @address = address
7
+ end
8
+
9
+ def ==(other)
10
+ other.is_a?(self.class) && other.address == address
11
+ end
12
+
13
+ def hash
14
+ self.class.hash ^ address.hash
15
+ end
16
+ end
@@ -0,0 +1,19 @@
1
+ class Person
2
+ # @dynamic name, contacts
3
+ attr_reader :name
4
+ attr_reader :contacts
5
+
6
+ def initialize(name:)
7
+ @name = name
8
+ @contacts = []
9
+ end
10
+
11
+ def guess_country()
12
+ contacts.map do |contact|
13
+ case contact
14
+ when Phone
15
+ contact.country
16
+ end
17
+ end.compact.first
18
+ end
19
+ end
@@ -0,0 +1,22 @@
1
+ class Phone
2
+ # @dynamic country, number
3
+ attr_reader :country, :number
4
+
5
+ def initialize(country:, number:)
6
+ @country = country
7
+ @number = number
8
+ end
9
+
10
+ def ==(other)
11
+ if other.is_a?(Phone)
12
+ # @type var other: Phone
13
+ other.country == country && other.number == number
14
+ else
15
+ false
16
+ end
17
+ end
18
+
19
+ def hash
20
+ self.class.hash ^ country.hash ^ number.hash
21
+ end
22
+ end
@@ -0,0 +1,11 @@
1
+ class Email
2
+ @address: String
3
+
4
+ attr_reader address: String
5
+
6
+ def initialize: (address: String) -> void
7
+
8
+ def ==: (untyped other) -> bool
9
+
10
+ def hash: () -> Integer
11
+ end
@@ -0,0 +1,13 @@
1
+ class Person
2
+ @name: String
3
+
4
+ @contacts: Array[Email | Phone]
5
+
6
+ attr_reader name: String
7
+
8
+ attr_reader contacts: Array[Email | Phone]
9
+
10
+ def initialize: (name: String) -> void
11
+
12
+ def guess_country: () -> (String | nil)
13
+ end
@@ -0,0 +1,15 @@
1
+ class Phone
2
+ @country: String
3
+
4
+ @number: String
5
+
6
+ attr_reader country: String
7
+
8
+ attr_reader number: String
9
+
10
+ def initialize: (country: String, number: String) -> void
11
+
12
+ def ==: (untyped other) -> (bool | nil)
13
+
14
+ def hash: () -> Integer
15
+ end
@@ -0,0 +1,11 @@
1
+ class Email
2
+ @address: untyped
3
+
4
+ attr_reader address: untyped
5
+
6
+ def initialize: (address: untyped) -> void
7
+
8
+ def ==: (untyped other) -> untyped
9
+
10
+ def hash: () -> untyped
11
+ end
@@ -0,0 +1,13 @@
1
+ class Person
2
+ @name: untyped
3
+
4
+ @contacts: untyped
5
+
6
+ attr_reader name: untyped
7
+
8
+ attr_reader contacts: untyped
9
+
10
+ def initialize: (name: untyped) -> void
11
+
12
+ def guess_country: () -> untyped
13
+ end
@@ -0,0 +1,15 @@
1
+ class Phone
2
+ @country: untyped
3
+
4
+ @number: untyped
5
+
6
+ attr_reader country: untyped
7
+
8
+ attr_reader number: untyped
9
+
10
+ def initialize: (country: untyped, number: untyped) -> void
11
+
12
+ def ==: (untyped other) -> (untyped | nil)
13
+
14
+ def hash: () -> untyped
15
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RbsGoose
4
+ module IO
5
+ class Example
6
+ class << self
7
+ def from_path(ruby_path:, rbs_path:, refined_rbs_dir:, base_path:)
8
+ Example.new(
9
+ typed_ruby: TypedRuby.from_path(ruby_path: ruby_path, rbs_path: rbs_path, base_path: base_path),
10
+ refined_rbs: File.new(path: rbs_path, base_path: ::File.join(base_path, refined_rbs_dir))
11
+ )
12
+ end
13
+ end
14
+
15
+ def initialize(typed_ruby:, refined_rbs:)
16
+ @typed_ruby = typed_ruby
17
+ @refined_rbs = refined_rbs
18
+ end
19
+
20
+ def to_h
21
+ { typed_ruby: typed_ruby, refined_rbs: refined_rbs }
22
+ end
23
+
24
+ attr_reader :typed_ruby, :refined_rbs
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RbsGoose
4
+ module IO
5
+ class ExampleGroup < Array
6
+ class << self
7
+ def load_from(base_path, code_dir: 'lib', sig_dir: 'sig', refined_dir: 'refined')
8
+ new.tap do |group|
9
+ Dir.glob('**/*.rb', base: ::File.join(base_path, code_dir)).each do |path|
10
+ group << Example.from_path(
11
+ ruby_path: ::File.join(code_dir, path),
12
+ rbs_path: to_rbs_path(path, sig_dir),
13
+ refined_rbs_dir: refined_dir,
14
+ base_path: base_path
15
+ )
16
+ end
17
+ end
18
+ end
19
+
20
+ def default_examples
21
+ example_dir = ::File.join(__dir__.to_s, '../examples')
22
+ @default_examples ||= Dir.glob('*', base: example_dir).to_h do |dir|
23
+ [dir.to_sym, load_from(::File.join(example_dir, dir))]
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def to_rbs_path(path, sig_dir)
30
+ ::File.join(sig_dir, "#{path}s")
31
+ end
32
+ end
33
+
34
+ def to_target_group
35
+ TargetGroup.new.tap do |g|
36
+ each { g << _1.typed_ruby }
37
+ end
38
+ end
39
+
40
+ def to_refined_rbs_list
41
+ map(&:refined_rbs)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RbsGoose
4
+ module IO
5
+ class File
6
+ MARKDOWN_REGEXP = /\A```(?<type>ruby|rbs):(?<path>.+)\n(?<content>[\s\S]+)```\Z/
7
+
8
+ class << self
9
+ def from_markdown(markdown)
10
+ parsed = markdown.match(MARKDOWN_REGEXP)
11
+ raise ArgumentError, "Ruby or RBS Markdown parsing failed.\n#{markdown}" unless parsed
12
+
13
+ new(path: parsed[:path].to_s, content: parsed[:content].to_s)
14
+ end
15
+ end
16
+
17
+ def initialize(path:, content: nil, base_path: nil)
18
+ @path = path
19
+ @base_path = base_path
20
+ @content = content&.strip
21
+ load_content if @content.nil?
22
+ end
23
+
24
+ def load_content
25
+ @content = ::File.read(absolute_path).strip
26
+ end
27
+
28
+ def absolute_path
29
+ base_path ? ::File.join(base_path, path) : path
30
+ end
31
+
32
+ def type
33
+ @type ||= case path
34
+ in /\.rb\z/
35
+ :ruby
36
+ in /\.rbs\z/
37
+ :rbs
38
+ else
39
+ raise ArgumentError, "Unknown file type: #{path}"
40
+ end
41
+ end
42
+
43
+ def to_s
44
+ "```#{type}:#{path}\n#{content}\n```\n"
45
+ end
46
+
47
+ def content=(content)
48
+ @content = content.strip
49
+ end
50
+
51
+ def write
52
+ ::File.write(path, content)
53
+ end
54
+
55
+ def ==(other)
56
+ self.class == other.class &&
57
+ @path == other.path &&
58
+ @content == other.content
59
+ end
60
+
61
+ attr_reader :path, :content, :base_path
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RbsGoose
4
+ module IO
5
+ class TargetGroup < Array
6
+ class << self
7
+ def load_from(base_path, code_dir: 'lib', sig_dir: 'sig')
8
+ new.tap do |group|
9
+ Dir.glob('**/*.rb', base: ::File.join(base_path, code_dir)).each do |path|
10
+ group << TypedRuby.from_path(
11
+ ruby_path: ::File.join(code_dir, path),
12
+ rbs_path: ::File.join(sig_dir, "#{path}s"),
13
+ base_path: base_path
14
+ )
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RbsGoose
4
+ module IO
5
+ class TypedRuby
6
+ class << self
7
+ def from_path(ruby_path:, rbs_path:, base_path:)
8
+ ruby = File.new(path: ruby_path, base_path: base_path)
9
+ rbs = begin
10
+ File.new(path: rbs_path, base_path: base_path)
11
+ rescue StandardError
12
+ nil
13
+ end
14
+ new(ruby: ruby, rbs: rbs)
15
+ end
16
+ end
17
+
18
+ def initialize(ruby:, rbs:)
19
+ raise ArgumentError, 'ruby must have ".rb" extension' unless ruby.type == :ruby
20
+ raise ArgumentError, 'rbs must have ".rbs" extension' if !rbs.nil? && rbs.type != :rbs
21
+
22
+ @ruby = ruby
23
+ @rbs = rbs
24
+ end
25
+
26
+ def to_s
27
+ rbs.nil? ? ruby.to_s : "#{ruby}\n#{rbs}"
28
+ end
29
+
30
+ attr_reader :ruby, :rbs
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'io/example'
4
+ require_relative 'io/example_group'
5
+ require_relative 'io/file'
6
+ require_relative 'io/target_group'
7
+ require_relative 'io/typed_ruby'
8
+
9
+ module RbsGoose
10
+ module IO
11
+ end
12
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'langchain'
4
+
5
+ module RbsGoose
6
+ module Templates
7
+ class DefaultTemplate
8
+ def initialize(instruction:, example_groups:)
9
+ @template = Langchain::Prompt::FewShotPromptTemplate.new(
10
+ prefix: instruction,
11
+ suffix: "#{input_template_string}\n",
12
+ example_prompt: example_prompt,
13
+ examples: example_groups.map { transform_example_group(_1) },
14
+ input_variables: %w[typed_ruby_list]
15
+ )
16
+ end
17
+
18
+ def format(typed_ruby_list)
19
+ template.format(typed_ruby_list: typed_ruby_list.join("\n"))
20
+ end
21
+
22
+ def parse_result(result)
23
+ result.scan(/```.+?```/m).map { IO::File.from_markdown(_1) }
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :template
29
+
30
+ def example_prompt
31
+ Langchain::Prompt::PromptTemplate.new(
32
+ template: "#{input_template_string}\n{refined_rbs_list}",
33
+ input_variables: %w[typed_ruby_list refined_rbs_list]
34
+ )
35
+ end
36
+
37
+ def input_template_string
38
+ "========Input========\n{typed_ruby_list}\n\n========Output========"
39
+ end
40
+
41
+ def transform_example_group(example_group)
42
+ {
43
+ typed_ruby_list: example_group.map(&:typed_ruby).join("\n"),
44
+ refined_rbs_list: example_group.map(&:refined_rbs).join("\n")
45
+ }
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'templates/default_template'
4
+
5
+ module RbsGoose
6
+ module Templates
7
+ end
8
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'templates'
4
+
5
+ module RbsGoose
6
+ class TypeInferrer
7
+ def infer(target_group)
8
+ template = RbsGoose.configuration.template
9
+ result = RbsGoose.llm.complete(prompt: template.format(target_group)).completion
10
+ template.parse_result(result)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RbsGoose
4
+ VERSION = '0.1.0'
5
+ end
data/lib/rbs_goose.rb ADDED
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'rbs_goose/configuration'
4
+ require_relative 'rbs_goose/io'
5
+ require_relative 'rbs_goose/type_inferrer'
6
+ require_relative 'rbs_goose/version'
7
+
8
+ require 'forwardable'
9
+
10
+ module RbsGoose
11
+ class Error < StandardError; end
12
+
13
+ class << self
14
+ extend Forwardable
15
+
16
+ def configure(&block)
17
+ @configuration = Configuration.new(&block)
18
+ end
19
+
20
+ def reset_configuration
21
+ @configuration = nil
22
+ end
23
+
24
+ def run(code_dir: 'lib', sig_dir: 'sig', base_path: ::Dir.pwd)
25
+ puts "Run RbsGoose.(Code Directory: #{code_dir}, Signature Directory: #{sig_dir})"
26
+ target_group = RbsGoose::IO::TargetGroup.load_from(base_path, code_dir: code_dir, sig_dir: sig_dir)
27
+ RbsGoose::TypeInferrer.new.infer(target_group).each do |refined_rbs|
28
+ puts "write refined rbs to #{refined_rbs.path}\n"
29
+ refined_rbs.write
30
+ puts "done.\n\n"
31
+ end
32
+ end
33
+
34
+ attr_reader :configuration
35
+
36
+ def_delegators :configuration, :llm, :instruction, :example_groups
37
+ end
38
+ end