ruboty-ai_agent 0.1.0 → 0.3.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 (89) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +19 -0
  3. data/AGENTS.md +8 -0
  4. data/CHANGELOG.md +14 -2
  5. data/Gemfile +10 -3
  6. data/README.md +2 -0
  7. data/Rakefile +4 -0
  8. data/Steepfile +1 -0
  9. data/lib/ruboty/ai_agent/actions/add_ai_command.rb +16 -1
  10. data/lib/ruboty/ai_agent/actions/add_mcp.rb +7 -2
  11. data/lib/ruboty/ai_agent/actions/base.rb +11 -1
  12. data/lib/ruboty/ai_agent/actions/chat.rb +56 -19
  13. data/lib/ruboty/ai_agent/actions/list_ai_commands.rb +20 -4
  14. data/lib/ruboty/ai_agent/actions/list_mcp.rb +50 -3
  15. data/lib/ruboty/ai_agent/actions/remove_ai_command.rb +7 -1
  16. data/lib/ruboty/ai_agent/actions/show_system_prompt.rb +1 -1
  17. data/lib/ruboty/ai_agent/agent.rb +3 -3
  18. data/lib/ruboty/ai_agent/chat_message.rb +7 -2
  19. data/lib/ruboty/ai_agent/chat_thread_messages.rb +40 -0
  20. data/lib/ruboty/ai_agent/commands/base.rb +16 -14
  21. data/lib/ruboty/ai_agent/commands/builtin_base.rb +39 -0
  22. data/lib/ruboty/ai_agent/commands/clear.rb +2 -14
  23. data/lib/ruboty/ai_agent/commands/compact.rb +4 -47
  24. data/lib/ruboty/ai_agent/commands/prompt_command.rb +60 -0
  25. data/lib/ruboty/ai_agent/commands/usage.rb +2 -14
  26. data/lib/ruboty/ai_agent/commands.rb +9 -17
  27. data/lib/ruboty/ai_agent/database/query_methods.rb +31 -6
  28. data/lib/ruboty/ai_agent/database.rb +2 -1
  29. data/lib/ruboty/ai_agent/http_mcp_client.rb +5 -2
  30. data/lib/ruboty/ai_agent/llm/openai.rb +6 -6
  31. data/lib/ruboty/ai_agent/mcp_clients.rb +5 -12
  32. data/lib/ruboty/ai_agent/mcp_configuration.rb +3 -2
  33. data/lib/ruboty/ai_agent/prompt_command_definition.rb +17 -0
  34. data/lib/ruboty/ai_agent/record_set.rb +9 -5
  35. data/lib/ruboty/ai_agent/recordable.rb +11 -9
  36. data/lib/ruboty/ai_agent/request.rb +17 -0
  37. data/lib/ruboty/ai_agent/token_usage.rb +10 -0
  38. data/lib/ruboty/ai_agent/tool.rb +11 -3
  39. data/lib/ruboty/ai_agent/tool_definitions/base.rb +84 -0
  40. data/lib/ruboty/ai_agent/tool_definitions/think.rb +41 -0
  41. data/lib/ruboty/ai_agent/tool_definitions.rb +19 -0
  42. data/lib/ruboty/ai_agent/user.rb +5 -0
  43. data/lib/ruboty/ai_agent/user_mcp_caches.rb +2 -2
  44. data/lib/ruboty/ai_agent/user_mcp_client.rb +5 -4
  45. data/lib/ruboty/ai_agent/user_prompt_command_definitions.rb +17 -0
  46. data/lib/ruboty/ai_agent/version.rb +1 -1
  47. data/lib/ruboty/ai_agent.rb +13 -0
  48. data/lib/ruboty/handlers/ai_agent.rb +28 -16
  49. data/rbs_collection.yaml +1 -0
  50. data/ruboty-ai_agent.gemspec +2 -6
  51. data/script/clean-orphaned-rbs.rb +105 -0
  52. data/script/generate-concern-rbs.rb +5 -5
  53. data/script/generate-data-rbs.rb +3 -5
  54. data/script/generate-memorized-ivar-rbs.rb +6 -11
  55. data/sig/generated/ruboty/ai_agent/actions/add_ai_command.rbs +4 -0
  56. data/sig/generated/ruboty/ai_agent/actions/base.rbs +4 -0
  57. data/sig/generated/ruboty/ai_agent/actions/chat.rbs +10 -0
  58. data/sig/generated/ruboty/ai_agent/actions/list_mcp.rbs +11 -0
  59. data/sig/generated/ruboty/ai_agent/agent.rbs +1 -1
  60. data/sig/generated/ruboty/ai_agent/chat_message.rbs +5 -2
  61. data/sig/generated/ruboty/ai_agent/chat_thread_messages.rbs +14 -0
  62. data/sig/generated/ruboty/ai_agent/commands/base.rbs +8 -19
  63. data/sig/generated/ruboty/ai_agent/commands/builtin_base.rbs +40 -0
  64. data/sig/generated/ruboty/ai_agent/commands/clear.rbs +2 -10
  65. data/sig/generated/ruboty/ai_agent/commands/compact.rbs +2 -20
  66. data/sig/generated/ruboty/ai_agent/commands/prompt_command.rbs +26 -0
  67. data/sig/generated/ruboty/ai_agent/commands/usage.rbs +2 -10
  68. data/sig/generated/ruboty/ai_agent/commands.rbs +3 -4
  69. data/sig/generated/ruboty/ai_agent/database/query_methods.rbs +18 -12
  70. data/sig/generated/ruboty/ai_agent/database.rbs +3 -1
  71. data/sig/generated/ruboty/ai_agent/llm/openai.rbs +3 -3
  72. data/sig/generated/ruboty/ai_agent/mcp_clients.rbs +0 -5
  73. data/sig/generated/ruboty/ai_agent/mcp_configuration.rbs +4 -2
  74. data/sig/generated/ruboty/ai_agent/prompt_command_definition.rbs +23 -0
  75. data/sig/generated/ruboty/ai_agent/record_set.rbs +11 -9
  76. data/sig/generated/ruboty/ai_agent/recordable.rbs +6 -6
  77. data/sig/generated/ruboty/ai_agent/request.rbs +23 -0
  78. data/sig/generated/ruboty/ai_agent/token_usage.rbs +4 -0
  79. data/sig/generated/ruboty/ai_agent/tool.rbs +9 -3
  80. data/sig/generated/ruboty/ai_agent/tool_definitions/base.rbs +52 -0
  81. data/sig/generated/ruboty/ai_agent/tool_definitions/think.rbs +17 -0
  82. data/sig/generated/ruboty/ai_agent/tool_definitions.rbs +12 -0
  83. data/sig/generated/ruboty/ai_agent/user.rbs +4 -0
  84. data/sig/generated/ruboty/ai_agent/user_mcp_client.rbs +4 -2
  85. data/sig/generated/ruboty/ai_agent/user_prompt_command_definitions.rbs +12 -0
  86. data/sig/generated/ruboty/handlers/ai_agent.rbs +12 -12
  87. data/sig/generated-by-scripts/concerns.rbs +5 -0
  88. data/sig/generated-by-scripts/memorized_ivars.rbs +19 -0
  89. metadata +19 -57
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1782e855675403948a771dd5abe6fdba6a754cfc6e15bebad40f24bf3c7569f6
4
- data.tar.gz: 2142af0fb45049c4e1b2397def8009248db8860e43a9fa2ae21f938a31e3af82
3
+ metadata.gz: 4358837ce9305dcb0965a67c957b75e8320aad2988647a9e6ae751a98e7b7009
4
+ data.tar.gz: 3b1ad1c43bc443c5d86b1ea80bd8d9a45263056bc91ade861058e600493cb1b7
5
5
  SHA512:
6
- metadata.gz: 1a1b7fc8e8e41150055e87d2227e9b0ae8570c69482cf72249af38abef0978058419eed73f778a0f4d9c00343a55ce96be0cd62febdb4a20cd8b7976a6852415
7
- data.tar.gz: 73aff06e4a6d7fb3601b9c95393b9c1496fd61eddaaabf0fe742e7701faad638a36c3919462e65847dab47d0450579c8047ba492beb4d51c81181b5b66307aef
6
+ metadata.gz: fb0b30fad316d595d3e088d9262b46b32af67467c23b28494a7214f83093bb2a939953005c19b84e907eae81c4d1ba00a2dd84ca96e8cc85fdffe016d687e036
7
+ data.tar.gz: 586e425b1c2f806d663e861d18fa57579649b6d29135576a1677f825f637fc017d5cb910e6b1edc1bc2e1a7d5c8eb4e40d6c7795c170e69f418434b84750c0f8
data/.rubocop.yml CHANGED
@@ -11,9 +11,11 @@
11
11
  plugins:
12
12
  - rubocop-rake
13
13
  - rubocop-rbs_inline
14
+ - rubocop-rspec
14
15
 
15
16
  AllCops:
16
17
  TargetRubyVersion: 3.1
18
+ NewCops: enable
17
19
 
18
20
  Naming/AccessorMethodName:
19
21
  Exclude:
@@ -33,6 +35,23 @@ Layout/LeadingCommentSpace:
33
35
  Metrics:
34
36
  Enabled: false
35
37
 
38
+ RSpec/ExampleLength:
39
+ Max: 7
40
+ CountAsOne: ["array", "heredoc", "method_call"]
41
+
42
+ RSpec/MultipleMemoizedHelpers:
43
+ Enabled: false
44
+
45
+ RSpec/MultipleExpectations:
46
+ Enabled: false
47
+
48
+ RSpec/NestedGroups:
49
+ Enabled: false
50
+
51
+ RSpec/SpecFilePathFormat:
52
+ CustomTransform:
53
+ OpenAI: openai
54
+
36
55
  Style/CommentedKeyword:
37
56
  Enabled: false # because of RBS annotations
38
57
 
data/AGENTS.md CHANGED
@@ -20,3 +20,11 @@
20
20
  ## PR instructions
21
21
 
22
22
  - Always run `rake autocorrect` before committing.
23
+
24
+ ## CHANGELOG instructions
25
+
26
+ - When you make changes, please update `CHANGELOG.md` accordingly.
27
+ - Add to the `Unreleased` section at the top.
28
+ - Follow the format used in previous entries.
29
+ - Use English for the changelog entries
30
+
data/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
- # Changelog
1
+ ## Unreleased
2
+ ## 0.3.0
2
3
 
3
- All notable changes to this project will be documented in this file.
4
+ - Add think tool.
5
+ - Different agent threads are prepared for different slack threads.
6
+
7
+ ## 0.2.0
8
+
9
+ - Support for user defined commends.
10
+ - Agent uses user defined system prompts and memories.
11
+ - Support automatic prompt compaction.
12
+
13
+ ## 0.1.0
14
+
15
+ - Initial release with basic features and functionality.
data/Gemfile CHANGED
@@ -7,8 +7,15 @@ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
7
7
  # Specify your gem's dependencies in ruboty-ai_agent.gemspec
8
8
  gemspec
9
9
 
10
+ gem 'bump', '~> 0.10.0'
11
+ gem 'bundler', '~> 2'
12
+ gem 'rake', '~> 13'
13
+ gem 'rspec', '~> 3.0'
14
+ gem 'webmock', '~> 3.0'
15
+
10
16
  gem 'rbs-inline', require: false
11
- gem 'rubocop'
12
- gem 'rubocop-rake'
13
- gem 'rubocop-rbs_inline'
17
+ gem 'rubocop', '~> 1.74.0'
18
+ gem 'rubocop-rake', '~> 0.7.0'
19
+ gem 'rubocop-rbs_inline', '~> 1.4.0'
20
+ gem 'rubocop-rspec', '~> 3.7.0'
14
21
  gem 'steep', require: false
data/README.md CHANGED
@@ -57,9 +57,11 @@ MCP (Model Context Protocol):
57
57
  - `add mcp <NAME> <OPTIONS> <URL>` — Add an MCP server
58
58
  - Example (HTTP transport with auth header):
59
59
  - `add mcp search --transport http --header 'Authorization: Bearer xxx' https://example.com/mcp`
60
+ - `add mcp search --transport http --bearer-token xxx https://example.com/mcp`
60
61
  - Options:
61
62
  - `--transport http|sse` (currently only `http` implemented; `sse` is not yet implemented)
62
63
  - `--header 'Key: Value'` (repeatable)
64
+ - `--bearer-token <TOKEN>` (shorthand for `--header 'Authorization: Bearer <TOKEN>'`)
63
65
  - `remove mcp <NAME>` — Remove an MCP server
64
66
  - `list mcp` / `list mcps` — List configured MCP servers
65
67
 
data/Rakefile CHANGED
@@ -4,6 +4,7 @@ require 'bundler/gem_tasks'
4
4
  require 'rspec/core/rake_task'
5
5
  require 'rubocop/rake_task'
6
6
  require 'steep/rake_task'
7
+ require 'bump/tasks'
7
8
 
8
9
  RSpec::Core::RakeTask.new(:spec)
9
10
 
@@ -16,6 +17,8 @@ Steep::RakeTask.new do |t|
16
17
  t.watch.verbose
17
18
  end
18
19
 
20
+ Bump.changelog = true
21
+
19
22
  task default: %i[rubocop steep spec]
20
23
  task autocorrect: %i[rubocop:autocorrect rbs steep spec]
21
24
 
@@ -33,6 +36,7 @@ namespace :rbs do
33
36
 
34
37
  desc 'Run rbs-inline to generate RBS files'
35
38
  task :inline do
39
+ sh('script/clean-orphaned-rbs.rb')
36
40
  sh('bundle exec rbs-inline --opt-out --output lib')
37
41
  end
38
42
 
data/Steepfile CHANGED
@@ -9,4 +9,5 @@ target :lib do
9
9
  library 'json'
10
10
  library 'net-http'
11
11
  library 'optparse'
12
+ library 'shellwords'
12
13
  end
@@ -6,7 +6,14 @@ module Ruboty
6
6
  # AddAiCommand action for Ruboty::AiAgent
7
7
  class AddAiCommand < Base
8
8
  def call
9
- message.reply("TODO: Implement AddAiCommand action - name: #{name_param}, prompt: #{prompt_param}")
9
+ if user.prompt_command_definitions.key?(name_param)
10
+ message.reply("Command '/#{name_param}' already exists. Use a different name or remove the existing command first.")
11
+ return
12
+ end
13
+
14
+ new_command = PromptCommandDefinition.new(name: name_param, prompt: undump_string(prompt_param))
15
+ user.prompt_command_definitions.add(new_command)
16
+ message.reply("Command '/#{name_param}' has been added successfully!")
10
17
  end
11
18
 
12
19
  def name_param #: String
@@ -16,6 +23,14 @@ module Ruboty
16
23
  def prompt_param #: String
17
24
  message[:prompt]
18
25
  end
26
+
27
+ # @rbs str: String
28
+ # @rbs return: String
29
+ def undump_string(str)
30
+ str.undump
31
+ rescue StandardError
32
+ str
33
+ end
19
34
  end
20
35
  end
21
36
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'optparse'
4
+ require 'shellwords'
4
5
 
5
6
  module Ruboty
6
7
  module AiAgent
@@ -58,11 +59,11 @@ module Ruboty
58
59
  args: []
59
60
  } #: config
60
61
 
61
- args = config_param.split(/\s+(?=-)/)
62
+ args = config_param.shellsplit
62
63
 
63
64
  parser = OptionParser.new do |opts|
64
65
  opts.on('--transport TYPE', %w[http sse], 'Transport type (http or sse)') do |t|
65
- options[:transport] = t
66
+ options[:transport] = t.to_sym
66
67
  end
67
68
 
68
69
  opts.on('--header VALUE', 'Add a header (can be specified multiple times)') do |h|
@@ -74,6 +75,10 @@ module Ruboty
74
75
 
75
76
  options[:headers][key] = value
76
77
  end
78
+
79
+ opts.on('--bearer-token TOKEN', 'Set Authorization Bearer token (shorthand for --header "Authorization: Bearer TOKEN")') do |token|
80
+ options[:headers]['Authorization'] = "Bearer #{token}"
81
+ end
77
82
  end
78
83
 
79
84
  options[:args] = parser.parse(args)
@@ -35,7 +35,17 @@ module Ruboty
35
35
 
36
36
  # @rbs %a{memorized}
37
37
  def chat_thread #: Ruboty::AiAgent::ChatThread
38
- @chat_thread ||= database.chat_thread(message.from || 'default')
38
+ @chat_thread ||= database.chat_thread(thread_id)
39
+ end
40
+
41
+ private
42
+
43
+ def thread_id #: String
44
+ if message.respond_to?(:original) && message.original&.dig(:thread_ts)
45
+ message.original[:thread_ts]
46
+ else
47
+ message.from || 'default'
48
+ end
39
49
  end
40
50
  end
41
51
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
4
+
3
5
  module Ruboty
4
6
  module AiAgent
5
7
  module Actions
@@ -9,28 +11,66 @@ module Ruboty
9
11
 
10
12
  # @rbs override
11
13
  def call
12
- llm = LLM::OpenAI.new(
13
- client: OpenAI::Client.new(
14
- api_key: ENV.fetch('OPENAI_API_KEY', nil)
15
- ),
16
- model: ENV.fetch('OPENAI_MODEL', 'gpt-5-nano')
17
- )
18
-
19
- commands = Commands.builtins(message:, chat_thread:)
20
- tools = McpClients.new(user.mcp_clients).available_tools
14
+ user.prompt_command_definitions.all_values.each do |definition|
15
+ command = Commands::PromptCommand.new(definition:, request:)
16
+ if command.match?(body_param)
17
+ new_prompt = command.call(commandline: body_param)
18
+ return complete_chat(new_prompt)
19
+ end
20
+ end
21
21
 
22
- commands.each do |command|
22
+ builtin_commands = Commands.builtins(request:)
23
+ builtin_commands.each do |command|
23
24
  return command.call if command.match?(body_param)
24
25
  end
25
26
 
27
+ complete_chat(body_param)
28
+ end
29
+
30
+ def body_param #: String
31
+ message[:body]
32
+ end
33
+
34
+ # @rbs %a{memorized}
35
+ def request #: Request
36
+ @request ||= Request.new(message:, chat_thread:)
37
+ end
38
+
39
+ private
40
+
41
+ # @rbs body: String
42
+ # @rbs return: void
43
+ def complete_chat(body)
26
44
  chat_thread.messages << ChatMessage.new(
27
45
  role: :user,
28
- content: body_param
46
+ content: body
29
47
  )
30
48
 
49
+ messages = [] #: Array[ChatMessage]
50
+
51
+ global_prompt = database.global_settings.system_prompt
52
+ messages << ChatMessage.new(role: :system, content: global_prompt) if global_prompt
53
+
54
+ user_prompt = user.system_prompt
55
+ messages << ChatMessage.new(role: :system, content: user_prompt) if user_prompt
56
+
57
+ ai_memories = user.ai_memories.all || {}
58
+ unless ai_memories.empty?
59
+ memory_content = ai_memories.map { |_idx, memory| memory }.join("\n\n")
60
+ messages << ChatMessage.new(role: :user, content: "My memories:\n#{memory_content}")
61
+ end
62
+
63
+ messages += chat_thread.messages.all_values
64
+
65
+ llm = LLM::OpenAI.new
66
+ tools = [
67
+ *McpClients.new(user.mcp_clients).available_tools,
68
+ *ToolDefinitions.builtins(request:).map(&:to_tool)
69
+ ]
70
+
31
71
  agent = Agent.new(
32
72
  llm:,
33
- messages: chat_thread.messages.all_values,
73
+ messages:,
34
74
  tools:
35
75
  )
36
76
 
@@ -39,12 +79,13 @@ module Ruboty
39
79
  when :new_message
40
80
  chat_thread.messages << event[:message]
41
81
  message.reply(event[:message].content) if event[:message].content.length.positive?
82
+
83
+ chat_thread.messages.compact(llm:) if chat_thread.messages.over_auto_compact_threshold?
42
84
  when :tool_call
43
- message.reply("Calling tool #{event[:tool].name} with arguments #{event[:tool_arguments]}",
44
- streaming: true)
85
+ message.reply("Calling tool #{event[:tool].name} with arguments #{event[:tool_arguments]&.to_json}") unless event[:tool].silent?
45
86
  when :tool_response
46
87
  chat_thread.messages << event[:message]
47
- message.reply("Tool response: #{event[:tool_response].slice(0..100)}")
88
+ message.reply("Tool response: #{event[:tool_response].slice(0..100)}") unless event[:tool].silent?
48
89
  end
49
90
  end
50
91
  rescue StandardError => e
@@ -54,10 +95,6 @@ module Ruboty
54
95
  message.reply("エラーが発生しました: #{e.message}")
55
96
  end
56
97
  end
57
-
58
- def body_param #: String
59
- message[:body]
60
- end
61
98
  end
62
99
  end
63
100
  end
@@ -7,11 +7,27 @@ module Ruboty
7
7
  class ListAiCommands < Base
8
8
  # @rbs override
9
9
  def call
10
- commands = Commands.builtins(message:, chat_thread:)
10
+ builtin_commands = Commands.builtins(request: Request.new(message:, chat_thread:))
11
11
 
12
- message.reply(
13
- commands.flat_map(&:matchers).map { |matcher| "#{matcher.pattern.inspect} - #{matcher.description}" }.join("\n")
14
- )
12
+ builtin_list = builtin_commands.flat_map(&:matchers)
13
+ .map { |matcher| "#{matcher.pattern.inspect} - #{matcher.description}" }
14
+
15
+ user_defined_list = user.prompt_command_definitions.all_values
16
+ .map { |cmd| "/#{cmd.name} - #{cmd.prompt}" }
17
+
18
+ content = <<~TEXT
19
+ # User-Defined Commands
20
+ #{user_defined_list.empty? ? 'No user-defined commands available.' : user_defined_list.join("\n")}
21
+
22
+ # Built-in Commands
23
+ #{builtin_list.empty? ? 'No built-in commands available.' : builtin_list.join("\n")}
24
+ TEXT
25
+
26
+ if builtin_commands.empty? && user_defined_list.empty?
27
+ message.reply('No commands available.')
28
+ else
29
+ message.reply(content)
30
+ end
15
31
  end
16
32
  end
17
33
  end
@@ -6,11 +6,58 @@ module Ruboty
6
6
  # ListMcp action for Ruboty::AiAgent
7
7
  class ListMcp < Base
8
8
  def call
9
- mcp_configurations = (user.mcp_configurations.all || {}).map do |name, mcp_configuration|
10
- "#{name}: #{mcp_configuration.to_h.except(:record_type).to_json}"
9
+ clients = user.mcp_clients
10
+
11
+ if clients.empty?
12
+ message.reply('No MCP servers found.')
13
+ return
14
+ end
15
+
16
+ show_headers = !message[:with_headers].nil?
17
+ output = clients.map do |mcp_client|
18
+ format_mcp_client(mcp_client, show_headers: show_headers)
19
+ end.join("\n\n")
20
+
21
+ message.reply(output)
22
+ end
23
+
24
+ private
25
+
26
+ # @rbs client: UserMcpClient
27
+ # @rbs show_headers: bool
28
+ # @rbs return: String
29
+ def format_mcp_client(client, show_headers: false)
30
+ configuration = client.configuration
31
+
32
+ tools_info = format_tools(client)
33
+ mcp_info = <<~TEXT
34
+ #{configuration.name}:
35
+ Transport: #{configuration.transport}
36
+ URL: #{configuration.url}
37
+ TEXT
38
+
39
+ mcp_info += " Headers: #{configuration.headers.to_json}\n" if show_headers
40
+
41
+ "#{mcp_info}#{tools_info}".chomp
42
+ end
43
+
44
+ # @rbs client: UserMcpClient
45
+ # @rbs return: String
46
+ def format_tools(client)
47
+ tools = client.list_tools
48
+ return '' if tools.empty?
49
+
50
+ tools_output = tools.map do |tool|
51
+ tool_name = tool['name'] || 'unnamed'
52
+ description = tool['description'] || 'No description'
53
+ truncated_description = description.length > 100 ? "#{description[0, 100]}..." : description
54
+ " - #{tool_name}: #{truncated_description}"
11
55
  end.join("\n")
12
56
 
13
- message.reply(mcp_configurations.empty? ? 'No memories found.' : mcp_configurations)
57
+ " Tools:\n#{tools_output}\n"
58
+ rescue HttpMcpClient::Error => e
59
+ warn "Failed to list tools for MCP client: #{e.message}"
60
+ ''
14
61
  end
15
62
  end
16
63
  end
@@ -6,7 +6,13 @@ module Ruboty
6
6
  # RemoveAiCommand action for Ruboty::AiAgent
7
7
  class RemoveAiCommand < Base
8
8
  def call
9
- message.reply("TODO: Implement RemoveAiCommand action for name: #{name_param}")
9
+ unless user.prompt_command_definitions.key?(name_param)
10
+ message.reply("Command '/#{name_param}' does not exist.")
11
+ return
12
+ end
13
+
14
+ user.prompt_command_definitions.remove(name_param)
15
+ message.reply("Command '/#{name_param}' has been removed successfully!")
10
16
  end
11
17
 
12
18
  def name_param #: String
@@ -9,7 +9,7 @@ module Ruboty
9
9
  user_prompt = user.system_prompt
10
10
  global_prompt = database.global_settings.system_prompt
11
11
 
12
- reply = []
12
+ reply = [] #: Array[String]
13
13
  reply << if user_prompt
14
14
  "User system prompt: #{user_prompt}"
15
15
  else
@@ -41,7 +41,7 @@ module Ruboty
41
41
  tool_arguments: response.tool_arguments,
42
42
  tool_response:
43
43
  )
44
- on_tool_response(tool_response:, message: tool_response_message, &)
44
+ on_tool_response(tool: response.tool, tool_response:, message: tool_response_message, &)
45
45
  messages << tool_response_message
46
46
  else
47
47
  messages << response.message
@@ -59,8 +59,8 @@ module Ruboty
59
59
  callback&.call({ type: :tool_call, tool:, tool_arguments: })
60
60
  end
61
61
 
62
- def on_tool_response(tool_response:, message:, &callback)
63
- callback&.call({ type: :tool_response, tool_response:, message: })
62
+ def on_tool_response(tool:, tool_response:, message:, &callback)
63
+ callback&.call({ type: :tool_response, tool:, tool_response:, message: })
64
64
  end
65
65
 
66
66
  def on_response(response, &callback)
@@ -15,14 +15,14 @@ module Ruboty
15
15
  attr_reader :tool_arguments #: Hash[Symbol | String, untyped]?
16
16
  attr_reader :token_usage #: TokenUsage?
17
17
 
18
- # @rbs role: Symbol
18
+ # @rbs role: Symbol | String
19
19
  # @rbs content: String
20
20
  # @rbs ?tool_call_id: String?
21
21
  # @rbs ?tool_name: String?
22
22
  # @rbs ?tool_arguments: Hash[Symbol | String, untyped]?
23
23
  # @rbs ?token_usage: TokenUsage?
24
24
  def initialize(role:, content:, tool_call_id: nil, tool_name: nil, tool_arguments: nil, token_usage: nil)
25
- @role = role
25
+ @role = role.to_sym
26
26
  @content = content
27
27
  @tool_call_id = tool_call_id
28
28
  @tool_name = tool_name
@@ -41,6 +41,11 @@ module Ruboty
41
41
  }
42
42
  end
43
43
 
44
+ # @rbs return: bool
45
+ def tool_call?
46
+ !!(tool_call_id && !tool_call_id.empty?)
47
+ end
48
+
44
49
  def self.from_llm_response(
45
50
  tool:,
46
51
  tool_call_id:,
@@ -12,6 +12,46 @@ module Ruboty
12
12
  end
13
13
 
14
14
  alias << add
15
+
16
+ # Check if any message's token usage exceeds auto compact threshold
17
+ # @rbs return: bool
18
+ def over_auto_compact_threshold?
19
+ all_values.any? { |message| message.token_usage&.over_auto_compact_threshold? }
20
+ end
21
+
22
+ # Compact chat messages by summarizing them
23
+ # @rbs llm: LLM::OpenAI
24
+ # @rbs return: void
25
+ def compact(llm:)
26
+ messages = all_values
27
+ return if messages.empty?
28
+
29
+ last_assistant_message = messages.reverse.find { |msg| msg.role == :assistant }
30
+
31
+ summary = summarize(llm:)
32
+
33
+ clear
34
+ add(ChatMessage.new(
35
+ role: :system,
36
+ content: "Previous conversation summary: #{summary}"
37
+ ))
38
+ add(last_assistant_message) if last_assistant_message&.tool_call?
39
+ end
40
+
41
+ # Generate summary of chat messages
42
+ # @rbs llm: LLM::OpenAI
43
+ # @rbs return: String
44
+ def summarize(llm:)
45
+ summary_prompt = ChatMessage.new(
46
+ role: :system,
47
+ content: <<~TEXT
48
+ Please summarize the following conversation in a concise manner, capturing the key topics, decisions, and context that would be helpful for continuing the conversation:
49
+ TEXT
50
+ )
51
+
52
+ response = llm.complete(messages: [summary_prompt, *all_values])
53
+ response.message.content
54
+ end
15
55
  end
16
56
  end
17
57
  end
@@ -6,32 +6,34 @@ module Ruboty
6
6
  # Base class for commands.
7
7
  # @abstract
8
8
  class Base
9
- Matcher = Data.define(:pattern, :description, :name)
9
+ attr_reader :request #: Request
10
10
 
11
- class << self
12
- def matchers #: Array[Matcher]
13
- @matchers ||= []
14
- end
11
+ # @rbs request: Request
12
+ def initialize(request:)
13
+ @request = request
15
14
 
16
- def on(pattern, name:, description:)
17
- matchers << Matcher.new(pattern:, description:, name:)
18
- end
15
+ super()
16
+ end
17
+
18
+ def message #: Ruboty::Message
19
+ request.message
20
+ end
21
+
22
+ def chat_thread #: Ruboty::AiAgent::ChatThread
23
+ request.chat_thread
19
24
  end
20
25
 
21
26
  # @rbs *args: untyped
22
- # @rbs return: void
27
+ # @rbs return: untyped
23
28
  def call(*args)
24
29
  raise NotImplementedError
25
30
  end
26
31
 
27
32
  # @rbs commandline: String
28
33
  # @rbs return: boolish
34
+ # @abstract
29
35
  def match?(commandline)
30
- matchers.any? { |matcher| /\A\s*#{matcher.pattern}/.match?(commandline) }
31
- end
32
-
33
- def matchers #: Array[Matcher]
34
- self.class.matchers
36
+ raise NotImplementedError
35
37
  end
36
38
  end
37
39
  end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruboty
4
+ module AiAgent
5
+ module Commands
6
+ # Base class for commands.
7
+ # @abstract
8
+ class BuiltinBase < Base
9
+ Matcher = Data.define(:pattern, :description, :name)
10
+
11
+ class << self
12
+ def matchers #: Array[Matcher]
13
+ @matchers ||= []
14
+ end
15
+
16
+ def on(pattern, name:, description:)
17
+ matchers << Matcher.new(pattern:, description:, name:)
18
+ end
19
+ end
20
+
21
+ # @rbs *args: untyped
22
+ # @rbs return: void
23
+ def call(*args)
24
+ raise NotImplementedError
25
+ end
26
+
27
+ # @rbs commandline: String
28
+ # @rbs return: boolish
29
+ def match?(commandline)
30
+ matchers.any? { |matcher| /\A\s*#{matcher.pattern}/.match?(commandline) }
31
+ end
32
+
33
+ def matchers #: Array[Matcher]
34
+ self.class.matchers
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -4,22 +4,10 @@ module Ruboty
4
4
  module AiAgent
5
5
  module Commands
6
6
  # Clear histories of the chat thread.
7
- class Clear < Base
7
+ class Clear < BuiltinBase
8
8
  on(%r{/clear}, name: 'clear', description: 'Clear the chat history.')
9
9
 
10
- attr_reader :message #: Ruboty::Message
11
- attr_reader :chat_thread #: Ruboty::AiAgent::ChatThread
12
-
13
- # @rbs message: Ruboty::Message
14
- # @rbs chat_thread: Ruboty::AiAgent::ChatThread
15
- def initialize(message:, chat_thread:)
16
- @message = message
17
- @chat_thread = chat_thread
18
-
19
- super()
20
- end
21
-
22
- def call #: void
10
+ def call(*) #: void
23
11
  chat_thread.clear
24
12
  message.reply('Cleared the chat history.')
25
13
  end