ghostest 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.
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