ruboty-ai_agent 0.2.0 → 0.4.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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +8 -0
  3. data/CHANGELOG.md +21 -2
  4. data/Gemfile +2 -0
  5. data/README.md +2 -0
  6. data/Rakefile +2 -0
  7. data/bin/ruboty +2 -0
  8. data/lib/ruboty/ai_agent/actions/add_mcp.rb +7 -2
  9. data/lib/ruboty/ai_agent/actions/base.rb +11 -1
  10. data/lib/ruboty/ai_agent/actions/chat.rb +27 -6
  11. data/lib/ruboty/ai_agent/actions/list_ai_commands.rb +1 -1
  12. data/lib/ruboty/ai_agent/actions/list_mcp.rb +50 -3
  13. data/lib/ruboty/ai_agent/agent.rb +3 -3
  14. data/lib/ruboty/ai_agent/chat_thread_messages.rb +6 -3
  15. data/lib/ruboty/ai_agent/commands/base.rb +12 -7
  16. data/lib/ruboty/ai_agent/commands/prompt_command.rb +3 -4
  17. data/lib/ruboty/ai_agent/commands/usage.rb +1 -3
  18. data/lib/ruboty/ai_agent/commands.rb +6 -16
  19. data/lib/ruboty/ai_agent/database/query_methods.rb +1 -1
  20. data/lib/ruboty/ai_agent/llm/openai/model.rb +1 -1
  21. data/lib/ruboty/ai_agent/llm/openai.rb +1 -1
  22. data/lib/ruboty/ai_agent/mcp_clients.rb +2 -15
  23. data/lib/ruboty/ai_agent/request.rb +17 -0
  24. data/lib/ruboty/ai_agent/settings.rb +28 -0
  25. data/lib/ruboty/ai_agent/tool.rb +11 -3
  26. data/lib/ruboty/ai_agent/tool_definitions/base.rb +88 -0
  27. data/lib/ruboty/ai_agent/tool_definitions/bot_help.rb +89 -0
  28. data/lib/ruboty/ai_agent/tool_definitions/fetch.rb +142 -0
  29. data/lib/ruboty/ai_agent/tool_definitions/think.rb +41 -0
  30. data/lib/ruboty/ai_agent/tool_definitions.rb +23 -0
  31. data/lib/ruboty/ai_agent/user_mcp_client.rb +3 -2
  32. data/lib/ruboty/ai_agent/version.rb +1 -1
  33. data/lib/ruboty/ai_agent.rb +5 -0
  34. data/lib/ruboty/handlers/ai_agent.rb +4 -1
  35. data/script/generate-memorized-ivar-rbs.rb +27 -4
  36. data/sig/generated/ruboty/ai_agent/actions/base.rbs +4 -0
  37. data/sig/generated/ruboty/ai_agent/actions/chat.rbs +8 -0
  38. data/sig/generated/ruboty/ai_agent/actions/list_mcp.rbs +11 -0
  39. data/sig/generated/ruboty/ai_agent/agent.rbs +1 -1
  40. data/sig/generated/ruboty/ai_agent/chat_thread_messages.rbs +3 -2
  41. data/sig/generated/ruboty/ai_agent/commands/base.rbs +6 -5
  42. data/sig/generated/ruboty/ai_agent/commands/prompt_command.rbs +2 -3
  43. data/sig/generated/ruboty/ai_agent/commands.rbs +2 -3
  44. data/sig/generated/ruboty/ai_agent/mcp_clients.rbs +0 -5
  45. data/sig/generated/ruboty/ai_agent/request.rbs +23 -0
  46. data/sig/generated/ruboty/ai_agent/settings.rbs +21 -0
  47. data/sig/generated/ruboty/ai_agent/tool.rbs +9 -3
  48. data/sig/generated/ruboty/ai_agent/tool_definitions/base.rbs +54 -0
  49. data/sig/generated/ruboty/ai_agent/tool_definitions/bot_help.rbs +23 -0
  50. data/sig/generated/ruboty/ai_agent/tool_definitions/fetch.rbs +43 -0
  51. data/sig/generated/ruboty/ai_agent/tool_definitions/think.rbs +17 -0
  52. data/sig/generated/ruboty/ai_agent/tool_definitions.rbs +12 -0
  53. data/sig/generated/ruboty/ai_agent/user_mcp_client.rbs +4 -2
  54. data/sig/generated/ruboty/ai_agent.rbs +2 -0
  55. data/sig/generated-by-scripts/memorized_ivars.rbs +18 -0
  56. metadata +15 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6ee903a1f588ede1c0b6e68bea298457b3ee2fbe826494f036f9c6482288475a
4
- data.tar.gz: 9acf203be78f86b2eeaf1f6810606a4a5b0f0a05709aba6c52b7194a589ed533
3
+ metadata.gz: ab07f82523eaf799691d9b03457bfa01751d0b0d1864e69bbbfc8661d34a0fef
4
+ data.tar.gz: 9e51ee2e35c00010df295626b8749019feefc67eab4d5fe396913eb1f468e433
5
5
  SHA512:
6
- metadata.gz: a59e8c688b63d055d59389a95b95a6a5637bac86d917e35a4bd90b7260bc2028b2defd00c6e7bd3a5b26d5cfdb22536de5e4a1428b4e597c29ae573898021922
7
- data.tar.gz: 1daed254a4b194c16b3b9dda2180c59e21cb5597d7c965e20905c302523b1f3a9f2a2829528e7746370fa567de487389c5846b392167c7331d8e00422a45a73e
6
+ metadata.gz: beda5fe21c74e2def519c4e7d9bef74144fffb5daa7ea2334debc065218ab319eead0be9db7a36fda3ed4909bd16b54dab8f289dfd94da77b971c20b20f5a40e
7
+ data.tar.gz: 1b7a5ff752355974c23a807c6360d2a8454d19e0afc7e0546e253c99e9ed79a2731aa32a02963471449052c81f7e1635babf7e16d093cc8708ba71889638ae4c
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,22 @@
1
- # Changelog
1
+ ## Unreleased
2
+ ## 0.4.0
2
3
 
3
- All notable changes to this project will be documented in this file.
4
+ - Add `bot_help` builtin tool to retrieve Ruboty's help information.
5
+ - Add `fetch` builtin tool for fetching web content.
6
+ - Fix `/usage` command to show token usage of last message.
7
+ - Format tool call logs for better readability.
8
+
9
+ ## 0.3.0
10
+
11
+ - Add think tool.
12
+ - Different agent threads are prepared for different slack threads.
13
+
14
+ ## 0.2.0
15
+
16
+ - Support for user defined commends.
17
+ - Agent uses user defined system prompts and memories.
18
+ - Support automatic prompt compaction.
19
+
20
+ ## 0.1.0
21
+
22
+ - Initial release with basic features and functionality.
data/Gemfile CHANGED
@@ -19,3 +19,5 @@ gem 'rubocop-rake', '~> 0.7.0'
19
19
  gem 'rubocop-rbs_inline', '~> 1.4.0'
20
20
  gem 'rubocop-rspec', '~> 3.7.0'
21
21
  gem 'steep', require: false
22
+
23
+ gem 'ruby-readability'
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
@@ -17,6 +17,8 @@ Steep::RakeTask.new do |t|
17
17
  t.watch.verbose
18
18
  end
19
19
 
20
+ Bump.changelog = true
21
+
20
22
  task default: %i[rubocop steep spec]
21
23
  task autocorrect: %i[rubocop:autocorrect rbs steep spec]
22
24
 
data/bin/ruboty CHANGED
@@ -17,6 +17,8 @@ gemfile do
17
17
  gem 'ruboty'
18
18
  gem 'ruboty-ai_agent', path: '../'
19
19
 
20
+ gem 'ruby-readability'
21
+
20
22
  # You can add other ruboty plugins here for testing.
21
23
  # gem 'ruboty-echo'
22
24
  # gem 'ruboty-alias'
@@ -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
@@ -10,14 +12,14 @@ module Ruboty
10
12
  # @rbs override
11
13
  def call
12
14
  user.prompt_command_definitions.all_values.each do |definition|
13
- command = Commands::PromptCommand.new(definition:, message:, chat_thread:)
15
+ command = Commands::PromptCommand.new(definition:, request:)
14
16
  if command.match?(body_param)
15
17
  new_prompt = command.call(commandline: body_param)
16
18
  return complete_chat(new_prompt)
17
19
  end
18
20
  end
19
21
 
20
- builtin_commands = Commands.builtins(message:, chat_thread:)
22
+ builtin_commands = Commands.builtins(request:)
21
23
  builtin_commands.each do |command|
22
24
  return command.call if command.match?(body_param)
23
25
  end
@@ -29,6 +31,11 @@ module Ruboty
29
31
  message[:body]
30
32
  end
31
33
 
34
+ # @rbs %a{memorized}
35
+ def request #: Request
36
+ @request ||= Request.new(message:, chat_thread:)
37
+ end
38
+
32
39
  private
33
40
 
34
41
  # @rbs body: String
@@ -56,7 +63,10 @@ module Ruboty
56
63
  messages += chat_thread.messages.all_values
57
64
 
58
65
  llm = LLM::OpenAI.new
59
- tools = McpClients.new(user.mcp_clients).available_tools
66
+ tools = [
67
+ *McpClients.new(user.mcp_clients).available_tools,
68
+ *ToolDefinitions.builtins(request:).map(&:to_tool)
69
+ ]
60
70
 
61
71
  agent = Agent.new(
62
72
  llm:,
@@ -72,11 +82,10 @@ module Ruboty
72
82
 
73
83
  chat_thread.messages.compact(llm:) if chat_thread.messages.over_auto_compact_threshold?
74
84
  when :tool_call
75
- message.reply("Calling tool #{event[:tool].name} with arguments #{event[:tool_arguments]}",
76
- streaming: true)
85
+ message.reply(indent_with_quotation("Calling tool #{event[:tool].name} with arguments #{truncate(event[:tool_arguments]&.to_json, max: 100)}")) unless event[:tool].silent?
77
86
  when :tool_response
78
87
  chat_thread.messages << event[:message]
79
- message.reply("Tool response: #{event[:tool_response].slice(0..100)}")
88
+ message.reply(indent_with_quotation("Tool response: #{truncate(event[:tool_response], max: 100)}")) unless event[:tool].silent?
80
89
  end
81
90
  end
82
91
  rescue StandardError => e
@@ -86,6 +95,18 @@ module Ruboty
86
95
  message.reply("エラーが発生しました: #{e.message}")
87
96
  end
88
97
  end
98
+
99
+ def truncate(text, max:)
100
+ if text.length > max
101
+ "#{text.slice(0..max)}..."
102
+ else
103
+ text
104
+ end
105
+ end
106
+
107
+ def indent_with_quotation(text, quota = '> ')
108
+ text.lines.map { |line| "#{quota}#{line}" }.join
109
+ end
89
110
  end
90
111
  end
91
112
  end
@@ -7,7 +7,7 @@ module Ruboty
7
7
  class ListAiCommands < Base
8
8
  # @rbs override
9
9
  def call
10
- builtin_commands = Commands.builtins(message:, chat_thread:)
10
+ builtin_commands = Commands.builtins(request: Request.new(message:, chat_thread:))
11
11
 
12
12
  builtin_list = builtin_commands.flat_map(&:matchers)
13
13
  .map { |matcher| "#{matcher.pattern.inspect} - #{matcher.description}" }
@@ -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
@@ -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)
@@ -11,12 +11,15 @@ module Ruboty
11
11
  store(message, key: (keys.last.to_s.to_i || -1) + 1)
12
12
  end
13
13
 
14
+ def token_usage #: TokenUsage?
15
+ all_values.reverse_each.find(&:token_usage)&.token_usage
16
+ end
17
+
14
18
  alias << add
15
19
 
16
20
  # 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? }
21
+ def over_auto_compact_threshold? #: boolish
22
+ token_usage&.over_auto_compact_threshold?
20
23
  end
21
24
 
22
25
  # Compact chat messages by summarizing them
@@ -6,18 +6,23 @@ module Ruboty
6
6
  # Base class for commands.
7
7
  # @abstract
8
8
  class Base
9
- attr_reader :message #: Ruboty::Message
10
- attr_reader :chat_thread #: Ruboty::AiAgent::ChatThread
9
+ attr_reader :request #: Request
11
10
 
12
- # @rbs message: Ruboty::Message
13
- # @rbs chat_thread: Ruboty::AiAgent::ChatThread
14
- def initialize(message:, chat_thread:)
15
- @message = message
16
- @chat_thread = chat_thread
11
+ # @rbs request: Request
12
+ def initialize(request:)
13
+ @request = request
17
14
 
18
15
  super()
19
16
  end
20
17
 
18
+ def message #: Ruboty::Message
19
+ request.message
20
+ end
21
+
22
+ def chat_thread #: Ruboty::AiAgent::ChatThread
23
+ request.chat_thread
24
+ end
25
+
21
26
  # @rbs *args: untyped
22
27
  # @rbs return: untyped
23
28
  def call(*args)
@@ -10,12 +10,11 @@ module Ruboty
10
10
  attr_reader :definition #: Ruboty::AiAgent::PromptCommandDefinition
11
11
 
12
12
  # @rbs definition: Ruboty::AiAgent::PromptCommandDefinition
13
- # @rbs message: Ruboty::Message
14
- # @rbs chat_thread: Ruboty::AiAgent::ChatThread
15
- def initialize(definition:, message:, chat_thread:)
13
+ # @rbs request: Ruboty::AiAgent::Request
14
+ def initialize(definition:, request:)
16
15
  @definition = definition
17
16
 
18
- super(message:, chat_thread:)
17
+ super(request:)
19
18
  end
20
19
 
21
20
  # @rbs commandline: String
@@ -8,9 +8,7 @@ module Ruboty
8
8
  on(%r{/usage}, name: 'show_usage', description: 'Show token usage information for the latest AI response')
9
9
 
10
10
  def call(*) #: void
11
- latest_message = chat_thread.messages.all_values.find(&:token_usage)
12
-
13
- token_usage = latest_message&.token_usage
11
+ token_usage = chat_thread.messages.token_usage
14
12
 
15
13
  if token_usage
16
14
  usage_text = "Token usage: #{format_number(token_usage.prompt_tokens)} (prompt) + #{format_number(token_usage.completion_tokens)} (completion) = #{format_number(token_usage.total_tokens)} (total)"
@@ -11,24 +11,14 @@ module Ruboty
11
11
  autoload :Usage, 'ruboty/ai_agent/commands/usage'
12
12
  autoload :PromptCommand, 'ruboty/ai_agent/commands/prompt_command'
13
13
 
14
- # @rbs message: Ruboty::Message
15
- # @rbs chat_thread: ChatThread
14
+ # @rbs request: Request
16
15
  # @rbs return: Array[Commands::BuiltinBase]
17
- def self.builtins(message:, chat_thread:)
16
+ def self.builtins(request:)
18
17
  [
19
- Commands::Clear.new(
20
- message:,
21
- chat_thread:
22
- ),
23
- Commands::Compact.new(
24
- message:,
25
- chat_thread:
26
- ),
27
- Commands::Usage.new(
28
- message:,
29
- chat_thread:
30
- )
31
- ]
18
+ Commands::Clear,
19
+ Commands::Compact,
20
+ Commands::Usage
21
+ ].map { |cmd_class| cmd_class.new(request:) }
32
22
  end
33
23
  end
34
24
  end
@@ -22,7 +22,7 @@ module Ruboty
22
22
  when Integer
23
23
  k
24
24
  when NilClass
25
- nil
25
+ ''
26
26
  else
27
27
  raise ArgumentError, "Invalid key type: #{k.class}"
28
28
  end
@@ -19,7 +19,7 @@ module Ruboty
19
19
  if name.include?('gpt-5')
20
20
  400_000
21
21
  else
22
- 128_000
22
+ AiAgent.settings.max_tokens || 128_000
23
23
  end
24
24
  end
25
25
  end
@@ -158,7 +158,7 @@ module Ruboty
158
158
  tool_call #: OpenAI::Models::Chat::ChatCompletionMessageFunctionToolCall
159
159
  .function.arguments
160
160
 
161
- JSON.parse(arguments, { symbolize_names: true })
161
+ JSON.parse(arguments)
162
162
  end
163
163
 
164
164
  Response.new(
@@ -16,8 +16,9 @@ module Ruboty
16
16
  clients.flat_map do |client|
17
17
  tool_defs = client.list_tools
18
18
  tool_defs.map do |tool_def|
19
+ tool_name = "mcp_#{client.mcp_name}__#{tool_def['name']}"
19
20
  Tool.new(
20
- name: tool_def['name'],
21
+ name: tool_name,
21
22
  title: tool_def['title'] || '',
22
23
  description: tool_def['description'] || '',
23
24
  input_schema: tool_def['inputSchema']
@@ -31,20 +32,6 @@ module Ruboty
31
32
  end
32
33
  end
33
34
 
34
- # @rbs function_name: String
35
- # @rbs arguments: Hash[String, untyped]
36
- # @rbs return: untyped
37
- def execute_tool(function_name, arguments)
38
- clients.each do |mcp_client|
39
- tools = mcp_client.list_tools
40
- return mcp_client.call_tool(function_name, arguments) if tools.any? { |t| t['name'] == function_name }
41
- end
42
- nil
43
- rescue HttpMcpClient::Error => e
44
- warn "Failed to execute tool '#{function_name}': #{e.message}"
45
- nil
46
- end
47
-
48
35
  # @rbs return: bool
49
36
  def any?
50
37
  @clients.any?
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruboty
4
+ module AiAgent
5
+ Request = Data.define(
6
+ :message, #: Ruboty::Message
7
+ :chat_thread #: Ruboty::AiAgent::ChatThread
8
+ )
9
+
10
+ # Request for chat action from user.
11
+ class Request
12
+ def message_body #: String
13
+ message[:body]
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruboty
4
+ module AiAgent
5
+ # Provide library-wide settings.
6
+ class Settings
7
+ # Provide access to settings instance.
8
+ module Accessor
9
+ def settings #: Ruboty::AiAgent::Settings
10
+ Settings.instance
11
+ end
12
+ end
13
+
14
+ # @rbs %a{memorized}
15
+ def self.instance #: Ruboty::AiAgent::Settings
16
+ @instance ||= Settings.new
17
+ end
18
+
19
+ def max_tokens #: Integer?
20
+ ENV['AI_AGENT_MAX_TOKENS']&.to_i
21
+ end
22
+
23
+ def auto_compact_threshold #: Float
24
+ ENV.fetch('AI_AGENT_AUTO_COMPACT_THRESHOLD', '80').to_f
25
+ end
26
+ end
27
+ end
28
+ end
@@ -6,21 +6,29 @@ module Ruboty
6
6
  class Tool
7
7
  attr_reader :name, :title, :description #: String
8
8
  attr_reader :input_schema #: Hash[untyped, untyped]?
9
- attr_reader :on_call #: (^(Hash[String, untyped]) -> String )?
9
+ attr_reader :silent #: boolish
10
+ attr_reader :on_call #: (^(Hash[String, untyped]) -> String? )?
10
11
 
11
12
  # @rbs name: String
12
13
  # @rbs title: String
13
14
  # @rbs description: String
14
15
  # @rbs input_schema: Hash[untyped, untyped]?
15
- # @rbs &on_call: ? (Hash[String, untyped]) -> String
16
- def initialize(name:, title:, description:, input_schema:, &on_call) #: void
16
+ # @rbs ?silent: boolish?
17
+ # @rbs &on_call: ? (Hash[String, untyped]) -> String?
18
+ def initialize(name:, title:, description:, input_schema:, silent: false, &on_call) #: void
17
19
  @name = name
18
20
  @title = title
19
21
  @description = description
20
22
  @input_schema = input_schema
23
+ @silent = silent
21
24
  @on_call = on_call
22
25
  end
23
26
 
27
+ # Returns true if the tool should be called silently (without notifying the user).
28
+ def silent? #: boolish
29
+ silent
30
+ end
31
+
24
32
  def call(params) #: String?
25
33
  on_call&.call(params)
26
34
  end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruboty
4
+ module AiAgent
5
+ module ToolDefinitions
6
+ # Base class for tool definitions
7
+ # @abstract
8
+ class Base
9
+ # @rbs!
10
+ # type input_schema = Hash[String | Symbol, untyped]
11
+
12
+ # @rbs!
13
+ # def self.tool_name: () -> String?
14
+ # def self.tool_name=: (String) -> String
15
+ # def self.tool_title: () -> String?
16
+ # def self.tool_title=: (String) -> String
17
+ # def self.tool_description: () -> String?
18
+ # def self.tool_description=: (String) -> String
19
+ # def self.tool_input_schema: () -> input_schema?
20
+ # def self.tool_input_schema=: (input_schema) -> input_schema
21
+
22
+ class << self
23
+ def available? #: boolish
24
+ true
25
+ end
26
+
27
+ # @rbs skip
28
+ attr_accessor :tool_name
29
+
30
+ # @rbs skip
31
+ attr_accessor :tool_title
32
+
33
+ # @rbs skip
34
+ attr_accessor :tool_description
35
+
36
+ # @rbs skip
37
+ attr_accessor :tool_input_schema
38
+ end
39
+
40
+ def tool_name #: String
41
+ self.class.tool_name || (raise NotImplementedError, "Subclasses must define 'tool_name'")
42
+ end
43
+
44
+ def tool_title #: String
45
+ self.class.tool_title || ''
46
+ end
47
+
48
+ def tool_description #: String
49
+ self.class.tool_description || ''
50
+ end
51
+
52
+ def tool_input_schema #: input_schema
53
+ self.class.tool_input_schema || {} #: input_schema
54
+ end
55
+
56
+ # Return true if you want the tool not to produce call logs in the chat.
57
+ def silent? #: boolish
58
+ false
59
+ end
60
+
61
+ attr_reader :request #: Request
62
+
63
+ # @rbs request: Request
64
+ def initialize(request:)
65
+ @request = request
66
+ end
67
+
68
+ # @abstract
69
+ # @rbs arguments: Hash[String, untyped]
70
+ # @rbs return: String?
71
+ def call(arguments)
72
+ raise NotImplementedError, "Subclasses must implement the 'call' method"
73
+ end
74
+
75
+ def to_tool #: Tool
76
+ Tool.new(
77
+ name: tool_name,
78
+ title: tool_title,
79
+ description: tool_description,
80
+ input_schema: tool_input_schema,
81
+ silent: silent?,
82
+ &method(:call) #: ^ (Hash[String, untyped]) -> String?
83
+ )
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end