ghostest 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.idea/misc.xml +4 -0
  3. data/.idea/modules.xml +8 -0
  4. data/.idea/vcs.xml +6 -0
  5. data/.rubocop.yml +236 -0
  6. data/.ruby-version +1 -0
  7. data/README.md +31 -0
  8. data/Rakefile +4 -0
  9. data/exe/ghostest +74 -0
  10. data/ghostest.gemspec +48 -0
  11. data/lib/ghostest/attr_reader.rb +11 -0
  12. data/lib/ghostest/config/agent.rb +30 -0
  13. data/lib/ghostest/config.rb +65 -0
  14. data/lib/ghostest/config_error.rb +5 -0
  15. data/lib/ghostest/error.rb +5 -0
  16. data/lib/ghostest/languages/ruby.rb +21 -0
  17. data/lib/ghostest/logger.rb +32 -0
  18. data/lib/ghostest/manager.rb +72 -0
  19. data/lib/ghostest/test_condition.rb +24 -0
  20. data/lib/ghostest/version.rb +3 -0
  21. data/lib/ghostest.rb +58 -0
  22. data/lib/google_custom_search.rb +30 -0
  23. data/lib/i18n_translator.rb +66 -0
  24. data/lib/initializers/i18n.rb +9 -0
  25. data/lib/llm/agents/base.rb +31 -0
  26. data/lib/llm/agents/reviewer.rb +50 -0
  27. data/lib/llm/agents/test_designer.rb +43 -0
  28. data/lib/llm/agents/test_programmer.rb +45 -0
  29. data/lib/llm/clients/azure_open_ai.rb +15 -0
  30. data/lib/llm/clients/base.rb +88 -0
  31. data/lib/llm/clients/open_ai.rb +14 -0
  32. data/lib/llm/functions/add_to_memory.rb +41 -0
  33. data/lib/llm/functions/base.rb +13 -0
  34. data/lib/llm/functions/exec_rspec_test.rb +39 -0
  35. data/lib/llm/functions/fix_one_rspec_test.rb +55 -0
  36. data/lib/llm/functions/get_files_list.rb +29 -0
  37. data/lib/llm/functions/get_gem_files_list.rb +43 -0
  38. data/lib/llm/functions/make_new_file.rb +43 -0
  39. data/lib/llm/functions/overwrite_file.rb +42 -0
  40. data/lib/llm/functions/read_file.rb +43 -0
  41. data/lib/llm/functions/record_lgtm.rb +48 -0
  42. data/lib/llm/functions/report_bug.rb +34 -0
  43. data/lib/llm/functions/switch_assignee.rb +74 -0
  44. data/lib/llm/message_container.rb +63 -0
  45. data/sig/ghostest.rbs +4 -0
  46. metadata +245 -0
@@ -0,0 +1,24 @@
1
+ module Ghostest
2
+ class TestCondition
3
+ def initialize(language_klass)
4
+ @language_klass = language_klass
5
+ unless File.exist?(@language_klass.test_condition_yml_path)
6
+ FileUtils.mkdir_p(File.dirname(@language_klass.test_condition_yml_path))
7
+ File.write(@language_klass.test_condition_yml_path, YAML.dump({}))
8
+ end
9
+ @test_condition = YAML.load(File.read(@language_klass.test_condition_yml_path)) || {}
10
+ end
11
+
12
+ def save_as_updated!(source_path)
13
+ source_md5 = Digest::MD5.hexdigest(File.read(source_path))
14
+ @test_condition[source_path] = { source_md5: }
15
+
16
+ File.write(@language_klass.test_condition_yml_path, YAML.dump(@test_condition))
17
+ end
18
+
19
+ def should_update_test?(source_path)
20
+ source_md5 = Digest::MD5.hexdigest(File.read(source_path))
21
+ @test_condition[source_path].nil? || @test_condition[source_path][:source_md5] != source_md5
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,3 @@
1
+ module Ghostest
2
+ VERSION = "0.1.0"
3
+ end
data/lib/ghostest.rb ADDED
@@ -0,0 +1,58 @@
1
+ # don't need to test this file
2
+ require_relative "ghostest/version"
3
+
4
+ require 'digest'
5
+
6
+ require 'openai'
7
+ require 'html2markdown'
8
+ require 'addressable'
9
+ require 'baran'
10
+ require 'tiktoken_ruby'
11
+ require 'google-apis-customsearch_v1'
12
+ require 'colorize'
13
+ require 'pry'
14
+ require 'i18n'
15
+ unless defined?(HashWithIndifferentAccess)
16
+ require 'indifference'
17
+ end
18
+ require 'erb'
19
+ require 'bundler'
20
+
21
+ require "ghostest/attr_reader"
22
+ require "ghostest/config/agent"
23
+ require "ghostest/config"
24
+ require "ghostest/config_error"
25
+ require "ghostest/error"
26
+ require "ghostest/languages/ruby"
27
+ require "ghostest/logger"
28
+ require "ghostest/manager"
29
+ require "ghostest/test_condition"
30
+ require "ghostest/version"
31
+ require "ghostest"
32
+ require "google_custom_search"
33
+ require "i18n_translator"
34
+ require "initializers/i18n"
35
+
36
+ require "llm/message_container"
37
+ require "llm/agents/base"
38
+ require "llm/agents/reviewer"
39
+ require "llm/agents/test_designer"
40
+ require "llm/agents/test_programmer"
41
+ require "llm/clients/base"
42
+ require "llm/clients/azure_open_ai"
43
+ require "llm/clients/open_ai"
44
+ require "llm/functions/base"
45
+ require "llm/functions/add_to_memory"
46
+ require "llm/functions/exec_rspec_test"
47
+ require "llm/functions/fix_one_rspec_test"
48
+ require "llm/functions/get_files_list"
49
+ require "llm/functions/get_gem_files_list"
50
+ require "llm/functions/make_new_file"
51
+ require "llm/functions/overwrite_file"
52
+ require "llm/functions/read_file"
53
+ require "llm/functions/record_lgtm"
54
+ require "llm/functions/report_bug"
55
+ require "llm/functions/switch_assignee"
56
+
57
+ module Ghostest
58
+ end
@@ -0,0 +1,30 @@
1
+ class GoogleCustomSearch
2
+ def search(query, args = {})
3
+ service.list_cses(cx: ENV['GOOGLE_CUSTOM_SEARCH_CSE_ID'], q: query, **args)
4
+ end
5
+
6
+ def service
7
+ @service ||= begin
8
+ service = Google::Apis::CustomsearchV1::CustomSearchAPIService.new
9
+ authorizer = make_authorizer
10
+ authorizer.fetch_access_token!
11
+ service.authorization = authorizer
12
+ service
13
+ end
14
+ end
15
+
16
+ def make_authorizer
17
+ sa_key = ENV.fetch("GOOGLE_SA_PRIVATE_KEY")
18
+ key = ::OpenSSL::PKey::RSA.new(sa_key)
19
+ cred = ::Signet::OAuth2::Client.new(
20
+ token_credential_uri: "https://oauth2.googleapis.com/token",
21
+ audience: "https://oauth2.googleapis.com/token",
22
+ scope: %w[
23
+ https://www.googleapis.com/auth/cse
24
+ ],
25
+ issuer: ENV.fetch("GOOGLE_SA_CLIENT_EMAIL"),
26
+ signing_key: key
27
+ )
28
+ cred.configure_connection({})
29
+ end
30
+ end
@@ -0,0 +1,66 @@
1
+ require 'digest'
2
+ class I18nTranslator
3
+ def self.update_dictionary!(from_locale, to_locales)
4
+ from_file_paths = I18n.load_path.select { |path| path.match(/#{from_locale}\.yml$/) }
5
+ from_file_paths.each do |from_file_path|
6
+ to_locales.each do |to_locale|
7
+ from_hash = {
8
+ to_locale.to_s => YAML.load(File.read(from_file_path))[from_locale.to_s],
9
+ }
10
+ to_file_path = from_file_path.gsub(/#{from_locale}\.yml$/, "#{to_locale}.yml")
11
+ to_hash = File.exist?(to_file_path) ? YAML.load(File.read(to_file_path)) : {}
12
+ to_hash = deep_translate(from_hash, to_hash, from_locale, to_locale)
13
+ File.write(to_file_path, YAML.dump(to_hash))
14
+ end
15
+ end
16
+ end
17
+
18
+ def self.deep_translate(from_hash, to_hash, from_locale, to_locale)
19
+ ret = {}
20
+ from_hash.each do |key, val|
21
+ if val.is_a?(Hash)
22
+ h = deep_translate(val, to_hash[key] || {}, from_locale, to_locale)
23
+ ret[key] = Hash[h.sort]
24
+ else
25
+ md5_key = "#{key}_md5"
26
+ md5 = ::Digest::MD5.hexdigest(val)
27
+ if to_hash[md5_key].nil? || to_hash[md5_key] != md5
28
+ ret[md5_key] = md5
29
+ ret[key] = translate(val)
30
+ else
31
+ ret[md5_key] = to_hash[md5_key]
32
+ ret[key] = to_hash[key]
33
+ end
34
+ end
35
+ end
36
+ ret
37
+ end
38
+
39
+ def self.translator
40
+ @translator ||= ::Llm::Clients::AzureOpenAi.new
41
+ end
42
+
43
+ # don't need to test this method
44
+ def self.translate(str)
45
+ # DeepLのほうが望ましいがLLMで代用
46
+ ret = self.translator.chat(parameters: {
47
+ messages: [
48
+ {
49
+ role: "system",
50
+ content: "You are an excellent translator. We translate strings sent by users into accurate English.\n" + \
51
+ "We do not output any content other than the translation.\n" + \
52
+ "Please keep the position and number of the new line code(\\n).\n" + \
53
+ "Never omit the line feed code at the end of a sentence.",
54
+ },
55
+ {
56
+ role: "user",
57
+ content: str,
58
+ },
59
+ ],
60
+ })
61
+ translated = ret.dig("choices", 0, "message", "content")
62
+ puts("#{str} => #{translated.green}")
63
+ sleep(1)
64
+ translated
65
+ end
66
+ end
@@ -0,0 +1,9 @@
1
+
2
+ I18n.load_path += Dir[File.expand_path("config/locales") + "/**/*.yml"]
3
+
4
+ module I18n
5
+ def self.just_raise_that_exception(*args)
6
+ raise "i18n: #{args.first}"
7
+ end
8
+ end
9
+ I18n.exception_handler = :just_raise_that_exception
@@ -0,0 +1,31 @@
1
+ module Llm
2
+ module Agents
3
+ class Base
4
+ include AttrReader
5
+ attr_reader :name, :config, :agent_config
6
+
7
+ def initialize(name, config, logger)
8
+ @name = name
9
+ @config = config
10
+ @agent_config = config.agents[name]
11
+ @logger = logger
12
+ end
13
+
14
+ def name_with_type
15
+ "#{self.name}(#{self.agent_config.role})"
16
+ end
17
+
18
+ def say(message, color: nil)
19
+ if color.nil?
20
+ @logger.info("#{self.name}: #{message}".send(self.agent_config.color))
21
+ else
22
+ if color
23
+ @logger.info("#{self.name}: #{message}".send(color))
24
+ else
25
+ @logger.info("#{self.name}: #{message}")
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,50 @@
1
+ module Llm
2
+ module Agents
3
+ class Reviewer < Base
4
+ def initialize(name, config, logger)
5
+ super(name, config, logger)
6
+ @record_lgtm_function = Llm::Functions::RecordLgtm.new
7
+ end
8
+
9
+ def lgtm?
10
+ @record_lgtm_function.lgtm?
11
+ end
12
+
13
+ # skip test for this method
14
+ def work(
15
+ source_path: raise("source_path is required"),
16
+ test_path: nil,
17
+ switch_assignee_function: raise("switch_assignee_function is required"))
18
+ say("start to work for #{source_path}")
19
+
20
+ message_container = Llm::MessageContainer.new
21
+ test_path ||= self.config.language_klass.convert_source_path_to_test_path(source_path)
22
+ message_container.add_system_message(self.agent_config.system_prompt.gsub("%{source_path}", source_path).gsub("%{test_path}", test_path))
23
+
24
+ if switch_assignee_function.messages.size > 0
25
+ message_container.add_system_message(I18n.t('ghostest.agents.reviewer.last_assignee_comment',
26
+ last_assignee: switch_assignee_function.last_assignee,
27
+ comment: switch_assignee_function.last_message))
28
+ end
29
+
30
+ azure_open_ai = Llm::Clients::AzureOpenAi.new
31
+ io = azure_open_ai.chat_with_function_calling_loop(
32
+ messages: message_container,
33
+ functions: [
34
+ @record_lgtm_function,
35
+ Llm::Functions::GetFilesList.new,
36
+ Llm::Functions::ReadFile.new,
37
+ Llm::Functions::AddToMemory.new(message_container),
38
+ switch_assignee_function,
39
+
40
+ ] + self.config.language_klass.create_functions,
41
+ agent: self,
42
+ )
43
+
44
+ comment = io.rewind && io.read
45
+ say(comment)
46
+ comment
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,43 @@
1
+ module Llm
2
+ module Agents
3
+ class TestDesigner < Base
4
+
5
+ # skip test for this method
6
+ def work(
7
+ source_path: raise("source_path is required"),
8
+ test_path: nil,
9
+ switch_assignee_function: raise("switch_assignee_function is required"))
10
+
11
+ say("start to work for #{source_path}")
12
+
13
+ message_container = Llm::MessageContainer.new
14
+ test_path ||= self.config.language_klass.convert_source_path_to_test_path(source_path)
15
+ message_container.add_system_message(self.agent_config.system_prompt.gsub("%{source_path}", source_path).gsub("%{test_path}", test_path))
16
+
17
+ if switch_assignee_function.messages.size > 0
18
+ message_container.add_system_message(I18n.t('ghostest.agents.test_designer.last_assignee_comment',
19
+ last_assignee: switch_assignee_function.last_assignee,
20
+ comment: switch_assignee_function.last_message))
21
+ end
22
+
23
+ azure_open_ai = Llm::Clients::AzureOpenAi.new
24
+ io = azure_open_ai.chat_with_function_calling_loop(
25
+ messages: message_container,
26
+ functions: [
27
+ Llm::Functions::GetFilesList.new,
28
+ Llm::Functions::ReadFile.new,
29
+ Llm::Functions::AddToMemory.new(message_container),
30
+ Llm::Functions::ReportBug.new,
31
+ switch_assignee_function,
32
+
33
+ ] + self.config.language_klass.create_functions,
34
+ agent: self,
35
+ )
36
+
37
+ comment = io.rewind && io.read
38
+ say(comment)
39
+ comment
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,45 @@
1
+ module Llm
2
+ module Agents
3
+ class TestProgrammer < Base
4
+
5
+ # skip test for this method
6
+ def work(
7
+ source_path: raise("source_path is required"),
8
+ test_path: nil,
9
+ switch_assignee_function: raise("switch_assignee_function is required"))
10
+
11
+ say("start to work for #{source_path}")
12
+
13
+ message_container = Llm::MessageContainer.new
14
+ test_path ||= self.config.language_klass.convert_source_path_to_test_path(source_path)
15
+ message_container.add_system_message(self.agent_config.system_prompt.gsub("%{source_path}", source_path).gsub("%{test_path}", test_path))
16
+
17
+ if switch_assignee_function.messages.size > 0
18
+ message_container.add_system_message(I18n.t('ghostest.agents.test_programmer.last_assignee_comment',
19
+ last_assignee: switch_assignee_function.last_assignee,
20
+ comment: switch_assignee_function.last_message))
21
+ end
22
+
23
+ azure_open_ai = Llm::Clients::AzureOpenAi.new
24
+ io = azure_open_ai.chat_with_function_calling_loop(
25
+ messages: message_container,
26
+ functions: [
27
+ Llm::Functions::GetFilesList.new,
28
+ Llm::Functions::ReadFile.new,
29
+ Llm::Functions::OverwriteFile.new,
30
+ Llm::Functions::MakeNewFile.new,
31
+ Llm::Functions::AddToMemory.new(message_container),
32
+ Llm::Functions::ReportBug.new,
33
+ switch_assignee_function,
34
+
35
+ ] + self.config.language_klass.create_functions,
36
+ agent: self,
37
+ )
38
+
39
+ comment = io.rewind && io.read
40
+ say(comment)
41
+ comment
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,15 @@
1
+ module Llm
2
+ module Clients
3
+ class AzureOpenAi < Llm::Clients::Base
4
+ def initialize(timeout: 30000)
5
+ @client = OpenAI::Client.new(
6
+ api_type: :azure,
7
+ api_version: ENV.fetch("AZURE_API_VERSION"),
8
+ access_token: ENV.fetch("AZURE_OPENAI_API_KEY"),
9
+ uri_base: "#{ENV.fetch("AZURE_API_BASE")}/openai/deployments/#{ENV.fetch("AZURE_DEPLOYMENT_NAME")}",
10
+ request_timeout: timeout,
11
+ )
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,88 @@
1
+ require "ghostest/attr_reader"
2
+ module Llm
3
+ module Clients
4
+ class Base
5
+ include AttrReader
6
+ attr_reader :client
7
+
8
+ def chat(parameters: {})
9
+ parameters = parameters.with_indifferent_access
10
+ if parameters[:messages].nil? || parameters[:messages].empty?
11
+ raise 'messages is required.'
12
+ end
13
+ parameters[:temperature] ||= 0.5
14
+ parameters[:top_p] ||= 1
15
+ parameters[:frequency_penalty] ||= 0
16
+ parameters[:presence_penalty] ||= 0
17
+
18
+ self.client.chat(parameters:)
19
+ end
20
+
21
+ def chat_with_function_calling_loop(**args)
22
+ agent = args.delete(:agent) || (raise 'agent is required.')
23
+ chat_message_io = StringIO.new
24
+
25
+ if args[:messages].is_a?(Llm::MessageContainer)
26
+ message_container = args[:messages]
27
+ else
28
+ message_container = Llm::MessageContainer.new
29
+ message_container.add_raw_messages(args[:messages])
30
+ end
31
+
32
+ i = 0
33
+ while (i += 1) < 20
34
+ ret = self.chat(parameters: args.merge({
35
+ messages: message_container.to_capped_messages,
36
+ functions: args[:functions].map { |f| f.definition },
37
+ }))
38
+ if ret.dig("choices", 0, "finish_reason") != 'function_call'
39
+ break
40
+ end
41
+
42
+ # Function calling
43
+ message = ret.dig("choices", 0, "message")
44
+ function = args[:functions].detect { |f| f.function_name == message['function_call']['name'].to_sym }
45
+ message_container.add_raw_message(message.merge({ content: nil }))
46
+
47
+ function_args = (JSON.parse(message.dig('function_call', 'arguments')) || {}).with_indifferent_access
48
+ agent.say(function.function_name)
49
+ agent.say(function_args, color: false)
50
+
51
+ function_result = function.execute_and_generate_message(function_args)
52
+ message_container.add_raw_message({
53
+ role: "function",
54
+ name: function.function_name,
55
+ content: JSON.dump(function_result),
56
+ })
57
+
58
+ if function.stop_llm_call?
59
+ chat_message_io.write(function_result)
60
+ return chat_message_io
61
+ end
62
+ end
63
+
64
+ # メッセージ表示
65
+ if content = ret.dig("choices", 0, "message", "content")
66
+ chat_message_io.write(content)
67
+ else
68
+ # エラー発生か、function callingの回数が多すぎる場合は、他エージェントに相談する
69
+ functions = args[:functions].select do |f| %w[
70
+ switch_assignee
71
+ report_bug
72
+ ].include?(f.function_name.to_s) end
73
+ ret = self.chat(parameters: args.merge({
74
+ messages: message_container.to_capped_messages,
75
+ functions: functions.map { |f| f.definition },
76
+ }))
77
+ if content = ret.dig("choices", 0, "message", "content")
78
+ chat_message_io.write(content)
79
+ else
80
+ puts ret
81
+ exit 1
82
+ end
83
+ end
84
+ chat_message_io
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,14 @@
1
+ module Llm
2
+ module Clients
3
+ class OpenAi < Llm::Clients::Base
4
+ def initialize(timeout: 300)
5
+ @client = OpenAI::Client.new(
6
+ api_version: ENV.fetch("OPENAI_API_VERSION"),
7
+ access_token: ENV.fetch("OPENAI_API_KEY"),
8
+ uri_base: "https://openai.com/openai/deployments/chat/completions",
9
+ request_timeout: timeout,
10
+ )
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,41 @@
1
+ module Llm
2
+ module Functions
3
+ class AddToMemory < Base
4
+ def function_name
5
+ :add_to_memory
6
+ end
7
+
8
+ def initialize(message_container)
9
+ @message_container = message_container
10
+ end
11
+
12
+ def definition
13
+ return @definition unless @definition.nil?
14
+
15
+ @definition = {
16
+ name: self.function_name,
17
+ description: I18n.t("ghostest.functions.#{self.function_name}.description"),
18
+ parameters: {
19
+ type: :object,
20
+ properties: {
21
+ contents_to_memory: {
22
+ type: :string,
23
+ description: I18n.t("ghostest.functions.#{self.function_name}.parameters.contents_to_memory"),
24
+ },
25
+ },
26
+ required: [:contents_to_memory],
27
+ },
28
+ }
29
+ @definition
30
+ end
31
+
32
+ def execute_and_generate_message(args)
33
+ if args[:contents_to_memory].nil? || args[:contents_to_memory].empty?
34
+ raise "contents_to_memory is required"
35
+ end
36
+ @message_container.add_system_message(I18n.t("ghostest.functions.#{self.function_name}.system_message_prefix", contents_to_memory: args[:contents_to_memory]))
37
+ { result: 'success' }
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,13 @@
1
+ module Llm
2
+ module Functions
3
+ class Base
4
+ def function_name
5
+ nil
6
+ end
7
+
8
+ def stop_llm_call?
9
+ false
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,39 @@
1
+ module Llm
2
+ module Functions
3
+ class ExecRspecTest < Base
4
+ def function_name
5
+ :exec_rspec_test
6
+ end
7
+
8
+ def definition
9
+ return @definition unless @definition.nil?
10
+
11
+ @definition = {
12
+ name: self.function_name,
13
+ description: I18n.t("ghostest.functions.#{self.function_name}.description"),
14
+ parameters: {
15
+ type: :object,
16
+ properties: {
17
+ file_or_dir_path: {
18
+ type: :string,
19
+ description: I18n.t("ghostest.functions.#{self.function_name}.parameters.file_or_dir_path"),
20
+ },
21
+ },
22
+ required: [:file_or_dir_path],
23
+ },
24
+ }
25
+ @definition
26
+ end
27
+
28
+ def execute_and_generate_message(args)
29
+ if args[:file_or_dir_path].nil? || args[:file_or_dir_path].empty?
30
+ raise Ghostest::Error.new("Please specify the file or directory path.")
31
+ end
32
+ script = "bundle exec rspec '#{args['file_or_dir_path']}'"
33
+ stdout, stderr, status = Open3.capture3(script)
34
+
35
+ { stdout:, stderr:, exit_status: status.exitstatus }
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,55 @@
1
+ module Llm
2
+ module Functions
3
+ class FixOneRspecTest < Base
4
+ def function_name
5
+ :fix_one_rspec_test
6
+ end
7
+
8
+ def definition
9
+ return @definition unless @definition.nil?
10
+
11
+ @definition = {
12
+ name: self.function_name,
13
+ description: I18n.t("ghostest.functions.#{self.function_name}.description"),
14
+ parameters: {
15
+ type: :object,
16
+ properties: {
17
+ file_path: {
18
+ type: :string,
19
+ description: I18n.t("ghostest.functions.#{self.function_name}.parameters.file_path"),
20
+ },
21
+ line_num: {
22
+ type: :string,
23
+ description: I18n.t("ghostest.functions.#{self.function_name}.parameters.line_num"),
24
+ },
25
+ },
26
+ required: [:file_path, :line_num],
27
+ },
28
+ }
29
+ @definition
30
+ end
31
+
32
+ def execute_and_generate_message(args)
33
+ if args[:file_path].nil? || args[:file_path].empty? || !File.exist?(args[:file_path])
34
+ raise Ghostest::Error.new("Please specify the file path.")
35
+ end
36
+ line_num = args[:line_num].to_i
37
+ if line_num < 1
38
+ raise Ghostest::Error.new("Please specify the line num. #{args[:line_num]}")
39
+ end
40
+
41
+ n = 0
42
+ while n < 5
43
+ n += 1
44
+ script = "bundle exec rspec '#{args['file_path']}:#{args['line_num']}'"
45
+ stdout, stderr, status = Open3.capture3(script)
46
+ if status.exitstatus != 0
47
+
48
+ end
49
+ end
50
+
51
+ { stdout:, stderr:, exit_status: status.exitstatus }
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,29 @@
1
+ module Llm
2
+ module Functions
3
+ class GetFilesList < Base
4
+ def function_name
5
+ :get_files_list
6
+ end
7
+
8
+ def definition
9
+ return @definition unless @definition.nil?
10
+
11
+ @definition = {
12
+ name: self.function_name,
13
+ description: I18n.t("ghostest.functions.#{self.function_name}.description"),
14
+ parameters: {
15
+ type: :object,
16
+ properties: {},
17
+ },
18
+ }
19
+ @definition
20
+ end
21
+
22
+ def execute_and_generate_message(args)
23
+ files_list = Dir.glob("**/*.{rb,yml}")
24
+
25
+ { files_list: }
26
+ end
27
+ end
28
+ end
29
+ end