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
@@ -4,30 +4,16 @@ module Ruboty
4
4
  module AiAgent
5
5
  module Commands
6
6
  # Compact chat history by summarizing it
7
- class Compact < Base
7
+ class Compact < BuiltinBase
8
8
  on(%r{/compact}, name: 'compact', description: 'Compact the chat history by summarizing it.')
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
23
- messages = chat_thread.messages.all_values
24
-
25
- if messages.empty?
10
+ def call(*) #: void
11
+ if chat_thread.messages.empty?
26
12
  message.reply('No chat history to compact.')
27
13
  return
28
14
  end
29
15
 
30
- summary = generate_summary(messages)
16
+ summary = chat_thread.messages.summarize(llm: LLM::OpenAI.new)
31
17
 
32
18
  chat_thread.clear
33
19
  chat_thread.messages.add(
@@ -45,35 +31,6 @@ module Ruboty
45
31
  message.reply("エラーが発生しました: #{e.message}")
46
32
  end
47
33
  end
48
-
49
- private
50
-
51
- # @rbs messages: Array[ChatMessage]
52
- # @rbs return: String
53
- def generate_summary(messages)
54
- llm = LLM::OpenAI.new(
55
- client: OpenAI::Client.new(
56
- api_key: ENV.fetch('OPENAI_API_KEY', nil)
57
- ),
58
- model: ENV.fetch('OPENAI_MODEL', 'gpt-5-nano')
59
- )
60
-
61
- summary_prompt = ChatMessage.new(
62
- role: :user,
63
- content: "Please summarize the following conversation in a concise manner, capturing the key topics, decisions, and context that would be helpful for continuing the conversation:\n\n#{format_messages_for_summary(messages)}"
64
- )
65
-
66
- response = llm.complete(messages: [summary_prompt])
67
- response.message.content
68
- end
69
-
70
- # @rbs messages: Array[ChatMessage]
71
- # @rbs return: String
72
- def format_messages_for_summary(messages)
73
- messages.map do |msg|
74
- "#{msg.role}: #{msg.content}"
75
- end.join("\n")
76
- end
77
34
  end
78
35
  end
79
36
  end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'shellwords'
4
+
5
+ module Ruboty
6
+ module AiAgent
7
+ module Commands
8
+ # Compact chat history by summarizing it
9
+ class PromptCommand < Base
10
+ attr_reader :definition #: Ruboty::AiAgent::PromptCommandDefinition
11
+
12
+ # @rbs definition: Ruboty::AiAgent::PromptCommandDefinition
13
+ # @rbs request: Ruboty::AiAgent::Request
14
+ def initialize(definition:, request:)
15
+ @definition = definition
16
+
17
+ super(request:)
18
+ end
19
+
20
+ # @rbs commandline: String
21
+ # @rbs return: boolish
22
+ def match?(commandline)
23
+ pattern.match?(commandline)
24
+ end
25
+
26
+ def pattern #: Regexp
27
+ %r{\A\s*/#{definition.name}(?:\s+(?<args>.+))?}
28
+ end
29
+
30
+ # @rbs commandline: String
31
+ # @rbs return: String
32
+ def call(commandline:)
33
+ result = definition.prompt.dup
34
+
35
+ match = pattern.match(commandline) || (raise 'Unreachable')
36
+ args = begin
37
+ Shellwords.split(match[:args] || '')
38
+ rescue ArgumentError
39
+ # If parsing fails, treat the entire string as a single argument
40
+ [match[:args]].compact
41
+ end #: Array[String]
42
+
43
+ found = {} #: Hash[Integer, boolish]
44
+ args.each_with_index do |arg, index|
45
+ placeholder = "$#{index + 1}"
46
+ result = result.gsub(placeholder) do
47
+ found[index] = true
48
+ arg
49
+ end
50
+ end
51
+
52
+ rest_args = args.each_with_index.reject { |_, index| found[index] }.map(&:first)
53
+ result += "\n\n#{rest_args.join(' ')}" if rest_args.any?
54
+
55
+ result
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -4,22 +4,10 @@ module Ruboty
4
4
  module AiAgent
5
5
  module Commands
6
6
  # Show token usage information for the latest AI response
7
- class Usage < Base
7
+ class Usage < BuiltinBase
8
8
  on(%r{/usage}, name: 'show_usage', description: 'Show token usage information for the latest AI response')
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
  latest_message = chat_thread.messages.all_values.find(&:token_usage)
24
12
 
25
13
  token_usage = latest_message&.token_usage
@@ -5,28 +5,20 @@ module Ruboty
5
5
  # Interaction commands (a.k.a. Slash commands, Prompts, etc)
6
6
  module Commands
7
7
  autoload :Base, 'ruboty/ai_agent/commands/base'
8
+ autoload :BuiltinBase, 'ruboty/ai_agent/commands/builtin_base'
8
9
  autoload :Clear, 'ruboty/ai_agent/commands/clear'
9
10
  autoload :Compact, 'ruboty/ai_agent/commands/compact'
10
11
  autoload :Usage, 'ruboty/ai_agent/commands/usage'
12
+ autoload :PromptCommand, 'ruboty/ai_agent/commands/prompt_command'
11
13
 
12
- # @rbs message: Ruboty::Message
13
- # @rbs chat_thread: ChatThread
14
- # @rbs return: Array[Commands::Base]
15
- def self.builtins(message:, chat_thread:)
14
+ # @rbs request: Request
15
+ # @rbs return: Array[Commands::BuiltinBase]
16
+ def self.builtins(request:)
16
17
  [
17
- Commands::Clear.new(
18
- message:,
19
- chat_thread:
20
- ),
21
- Commands::Compact.new(
22
- message:,
23
- chat_thread:
24
- ),
25
- Commands::Usage.new(
26
- message:,
27
- chat_thread:
28
- )
29
- ]
18
+ Commands::Clear,
19
+ Commands::Compact,
20
+ Commands::Usage
21
+ ].map { |cmd_class| cmd_class.new(request: request) }
30
22
  end
31
23
  end
32
24
  end
@@ -7,27 +7,49 @@ module Ruboty
7
7
  # interface _WithData
8
8
  # def data: -> Hash[keynable, untyped]
9
9
  # end
10
+ #
11
+ # type query_key = keynable | Symbol | nil
10
12
 
11
13
  # @rbs module-self _WithData
12
14
  module QueryMethods
13
- # @rbs *keys: keynable
15
+ # @rbs keys: Array[query_key]
16
+ # @rbs return: Array[keynable]
17
+ def self.keys_to_keynable(keys)
18
+ keys.map do |k|
19
+ case k
20
+ when String, Symbol
21
+ k.to_s
22
+ when Integer
23
+ k
24
+ when NilClass
25
+ ''
26
+ else
27
+ raise ArgumentError, "Invalid key type: #{k.class}"
28
+ end
29
+ end
30
+ end
31
+
32
+ # @rbs *keys: query_key
14
33
  # @rbs return: untyped
15
34
  def fetch(*keys)
35
+ keys = QueryMethods.keys_to_keynable(keys)
16
36
  item = data.dig(*keys)
17
37
 
18
38
  Recordable.instantiate_recursively(item)
19
39
  end
20
40
 
21
- # @rbs *keys: keynable
41
+ # @rbs *keys: query_key
22
42
  # @rbs return: Integer
23
43
  def len(*keys)
44
+ keys = QueryMethods.keys_to_keynable(keys)
24
45
  item = data.dig(*keys)
25
46
  item.respond_to?(:length) ? item.length : 0
26
47
  end
27
48
 
28
- # @rbs *keys: keynable
49
+ # @rbs *keys: query_key
29
50
  # @rbs return: void
30
51
  def delete(*keys)
52
+ keys = QueryMethods.keys_to_keynable(keys)
31
53
  namespace_keys = keys[0..-2] || []
32
54
  key = keys[-1]
33
55
 
@@ -40,9 +62,10 @@ module Ruboty
40
62
  end
41
63
  end
42
64
 
43
- # @rbs *keys: keynable
65
+ # @rbs *keys: query_key
44
66
  # @rbs return: Array[keynable]
45
67
  def keys(*keys)
68
+ keys = QueryMethods.keys_to_keynable(keys)
46
69
  namespace = keys.empty? ? data : data.dig(*keys) #: top
47
70
  case namespace
48
71
  when Hash
@@ -54,9 +77,10 @@ module Ruboty
54
77
  end
55
78
  end
56
79
 
57
- # @rbs *keys: keynable
80
+ # @rbs *keys: query_key
58
81
  # @rbs return: boolish
59
82
  def key?(*keys)
83
+ keys = QueryMethods.keys_to_keynable(keys)
60
84
  namespace_keys = keys[0..-2] || []
61
85
  key = keys[-1]
62
86
 
@@ -64,10 +88,11 @@ module Ruboty
64
88
  namespace&.key?(key)
65
89
  end
66
90
 
67
- # @rbs at: Array[keynable]
91
+ # @rbs at: Array[query_key]
68
92
  # @rbs value: untyped
69
93
  # @rbs return: void
70
94
  def store(value, at:)
95
+ at = QueryMethods.keys_to_keynable(at)
71
96
  namespace_keys = at[0..-2] || []
72
97
  key = at[-1]
73
98
 
@@ -5,7 +5,7 @@ module Ruboty
5
5
  # Memorize and retrieve information using Ruboty's brain.
6
6
  class Database
7
7
  # @rbs!
8
- # type keynable = Symbol | String | Integer
8
+ # type keynable = String | Integer
9
9
 
10
10
  autoload :QueryMethods, 'ruboty/ai_agent/database/query_methods'
11
11
 
@@ -32,6 +32,7 @@ module Ruboty
32
32
  ChatThread.find_or_create(database: self, id: id)
33
33
  end
34
34
 
35
+ # @rbs %a{memorized}
35
36
  def global_settings #: GlobalSettings
36
37
  @global_settings ||= GlobalSettings.find_or_create(database: self)
37
38
  end
@@ -51,7 +51,7 @@ module Ruboty
51
51
 
52
52
  # @rbs name: String
53
53
  # @rbs &block: ? (Hash[String, untyped]) -> void
54
- def call_tool(name, arguments = {}, &block)
54
+ def call_tool(name, arguments = {}, &)
55
55
  ensure_initialized
56
56
  results = send_request(
57
57
  method: 'tools/call',
@@ -59,7 +59,7 @@ module Ruboty
59
59
  name: name,
60
60
  arguments: arguments
61
61
  },
62
- &block
62
+ &
63
63
  )
64
64
 
65
65
  results.flat_map { |res| res.dig('result', 'content') || [] }
@@ -166,6 +166,9 @@ module Ruboty
166
166
  else
167
167
  handle_response(response, &block)
168
168
  end
169
+ rescue Net::HTTPExceptions, SystemCallError => e
170
+ error_message = e.is_a?(StandardError) ? e.message : "Unknown error (#{e.class.name})"
171
+ raise Error, "HTTP request failed: #{error_message}"
169
172
  end
170
173
 
171
174
  # @rbs response: Net::HTTPResponse
@@ -11,11 +11,11 @@ module Ruboty
11
11
  attr_reader :client #: OpenAI::Client
12
12
  attr_reader :model #: String
13
13
 
14
- # @rbs client: OpenAI::Client
15
- # @rbs model: String
16
- def initialize(client:, model:)
17
- @client = client
18
- @model = model
14
+ # @rbs ?client: OpenAI::Client
15
+ # @rbs ?model: String
16
+ def initialize(client: nil, model: nil)
17
+ @client = client || ::OpenAI::Client.new(api_key: ENV.fetch('OPENAI_API_KEY', nil))
18
+ @model = model || ENV.fetch('OPENAI_MODEL', 'gpt-5-nano')
19
19
  end
20
20
 
21
21
  # @rbs %a{memorized}
@@ -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']
@@ -25,20 +26,12 @@ module Ruboty
25
26
  client.call_tool(tool_def['name'], params).to_json
26
27
  end
27
28
  end
29
+ rescue HttpMcpClient::Error => e
30
+ warn "Failed to list tools for MCP client: #{e.message}"
31
+ []
28
32
  end
29
33
  end
30
34
 
31
- # @rbs function_name: String
32
- # @rbs arguments: Hash[String, untyped]
33
- # @rbs return: untyped
34
- def execute_tool(function_name, arguments)
35
- clients.each do |mcp_client|
36
- tools = mcp_client.list_tools
37
- return mcp_client.call_tool(function_name, arguments) if tools.any? { |t| t['name'] == function_name }
38
- end
39
- nil
40
- end
41
-
42
35
  # @rbs return: bool
43
36
  def any?
44
37
  @clients.any?
@@ -4,6 +4,7 @@ module Ruboty
4
4
  module AiAgent
5
5
  # @rbs!
6
6
  # type transports = :http | :websocket
7
+ # type transports_str = "http" | "websocket"
7
8
 
8
9
  McpConfiguration = Data.define(
9
10
  :name, #: String
@@ -17,12 +18,12 @@ module Ruboty
17
18
  include Recordable
18
19
 
19
20
  # @rbs name: String
20
- # @rbs transport: transports
21
+ # @rbs transport: transports | transports_str
21
22
  # @rbs url: String
22
23
  # @rbs headers: Hash[String, String]?
23
24
  def initialize(name:, transport:, url:, headers: {})
24
25
  # No superclass method `initialize` in RBS.
25
- super(name:, transport:, headers:, url:) # steep:ignore UnexpectedKeywordArgument
26
+ super(name:, transport: transport.to_sym, headers:, url:) # steep:ignore UnexpectedKeywordArgument
26
27
  end
27
28
 
28
29
  register_record_type :mcp_configuration
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruboty
4
+ module AiAgent
5
+ PromptCommandDefinition = Data.define(
6
+ :name, #: String
7
+ :prompt #: String
8
+ )
9
+
10
+ # User-defined command model
11
+ class PromptCommandDefinition
12
+ include Recordable
13
+
14
+ register_record_type :user_defined_command
15
+ end
16
+ end
17
+ end
@@ -11,10 +11,14 @@ module Ruboty
11
11
  @database = database
12
12
  end
13
13
 
14
- def namespace_keys #: Array[Database::keynable]
14
+ def namespace_keys #: Array[Database::query_key]
15
15
  raise NotImplementedError, 'Subclasses must implement the namespace_keys method'
16
16
  end
17
17
 
18
+ def empty? #: boolish
19
+ length.zero?
20
+ end
21
+
18
22
  def length #: Integer
19
23
  database.len(*namespace_keys)
20
24
  end
@@ -38,26 +42,26 @@ module Ruboty
38
42
  database.keys(*namespace_keys)
39
43
  end
40
44
 
41
- # @rbs key: Database::keynable
45
+ # @rbs key: Database::query_key
42
46
  # @rbs return: Record | nil
43
47
  def fetch(key)
44
48
  database.fetch(*namespace_keys, key)
45
49
  end
46
50
 
47
- # @rbs key: Database::keynable
51
+ # @rbs key: Database::query_key
48
52
  # @rbs record: Record
49
53
  # @rbs return: void
50
54
  def store(record, key:)
51
55
  database.store(record, at: [*namespace_keys, key])
52
56
  end
53
57
 
54
- # @rbs key: Database::keynable
58
+ # @rbs key: Database::query_key
55
59
  # @rbs return: void
56
60
  def remove(key)
57
61
  database.delete(*namespace_keys, key)
58
62
  end
59
63
 
60
- # @rbs key: Database::keynable
64
+ # @rbs key: Database::query_key
61
65
  # @rbs return: boolish
62
66
  def key?(key)
63
67
  database.key?(*namespace_keys, key)
@@ -4,7 +4,7 @@ module Ruboty
4
4
  module AiAgent
5
5
  # @rbs!
6
6
  # interface _WithToH
7
- # def to_h: () -> Hash[Database::keynable, untyped]
7
+ # def to_h: () -> Hash[Symbol, untyped]
8
8
  # end
9
9
 
10
10
  # Convertable between Hash and Recordable bidirectionally.
@@ -22,12 +22,12 @@ module Ruboty
22
22
  @record_types ||= {}
23
23
  end
24
24
 
25
- # @rbs hash: Hash[Symbol, untyped]?
25
+ # @rbs hash: Hash[Symbol | String, untyped]?
26
26
  # @rbs return: bool
27
27
  def convertable?(hash)
28
28
  return false unless hash.is_a?(Hash)
29
29
 
30
- type = hash[:record_type]
30
+ type = (hash[:record_type] || hash['record_type'])&.to_sym
31
31
  type && record_types.include?(type)
32
32
  end
33
33
 
@@ -54,9 +54,9 @@ module Ruboty
54
54
  def hashify_recursively(value)
55
55
  case value
56
56
  when Recordable
57
- hashify_recursively(value.to_h)
57
+ hashify_recursively(record_to_hash(value))
58
58
  when Hash
59
- value.transform_values { |v| hashify_recursively(v) }
59
+ value.transform_values { |v| hashify_recursively(v) }.transform_keys(&:to_s)
60
60
  when Array
61
61
  value.map { |v| hashify_recursively(v) }
62
62
  else
@@ -67,13 +67,15 @@ module Ruboty
67
67
  # @rbs record: Recordable
68
68
  # @rbs return: Hash[Database::keynable, untyped]
69
69
  def record_to_hash(record)
70
- record.to_h
70
+ record.to_h.transform_keys(&:to_s)
71
71
  end
72
72
 
73
- # @rbs hash: Hash[Symbol, untyped]
73
+ # @rbs hash: Hash[Symbol | String, untyped]
74
74
  # @rbs return: Recordable
75
75
  def record_from_hash(hash)
76
- type = hash[:record_type]
76
+ hash = hash.transform_keys(&:to_sym)
77
+
78
+ type = hash[:record_type]&.to_sym
77
79
  klass = record_types[type]
78
80
  raise "Unknown record type: #{type}" unless klass
79
81
 
@@ -98,7 +100,7 @@ module Ruboty
98
100
 
99
101
  # @rbs module-self Recordable::ClassMethods.instance
100
102
  module PrependMethods
101
- def to_h #: Hash[Database::keynable, untyped]
103
+ def to_h #: Hash[Symbol, untyped]
102
104
  {
103
105
  record_type: record_type,
104
106
  **super
@@ -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
@@ -32,6 +32,16 @@ module Ruboty
32
32
  (total_tokens.to_f / token_limit * 100).round(2).to_f
33
33
  end
34
34
 
35
+ # Check if usage percentage exceeds auto compact threshold
36
+ # @rbs return: bool
37
+ def over_auto_compact_threshold?
38
+ percentage = usage_percentage
39
+ return false unless percentage
40
+
41
+ threshold = ENV.fetch('AUTO_COMPACT_THRESHOLD', 80).to_f
42
+ percentage >= threshold
43
+ end
44
+
35
45
  def to_h #: Hash[Symbol, untyped]
36
46
  {
37
47
  prompt_tokens:,
@@ -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