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.
- checksums.yaml +4 -4
- data/.rubocop.yml +19 -0
- data/AGENTS.md +8 -0
- data/CHANGELOG.md +14 -2
- data/Gemfile +10 -3
- data/README.md +2 -0
- data/Rakefile +4 -0
- data/Steepfile +1 -0
- data/lib/ruboty/ai_agent/actions/add_ai_command.rb +16 -1
- data/lib/ruboty/ai_agent/actions/add_mcp.rb +7 -2
- data/lib/ruboty/ai_agent/actions/base.rb +11 -1
- data/lib/ruboty/ai_agent/actions/chat.rb +56 -19
- data/lib/ruboty/ai_agent/actions/list_ai_commands.rb +20 -4
- data/lib/ruboty/ai_agent/actions/list_mcp.rb +50 -3
- data/lib/ruboty/ai_agent/actions/remove_ai_command.rb +7 -1
- data/lib/ruboty/ai_agent/actions/show_system_prompt.rb +1 -1
- data/lib/ruboty/ai_agent/agent.rb +3 -3
- data/lib/ruboty/ai_agent/chat_message.rb +7 -2
- data/lib/ruboty/ai_agent/chat_thread_messages.rb +40 -0
- data/lib/ruboty/ai_agent/commands/base.rb +16 -14
- data/lib/ruboty/ai_agent/commands/builtin_base.rb +39 -0
- data/lib/ruboty/ai_agent/commands/clear.rb +2 -14
- data/lib/ruboty/ai_agent/commands/compact.rb +4 -47
- data/lib/ruboty/ai_agent/commands/prompt_command.rb +60 -0
- data/lib/ruboty/ai_agent/commands/usage.rb +2 -14
- data/lib/ruboty/ai_agent/commands.rb +9 -17
- data/lib/ruboty/ai_agent/database/query_methods.rb +31 -6
- data/lib/ruboty/ai_agent/database.rb +2 -1
- data/lib/ruboty/ai_agent/http_mcp_client.rb +5 -2
- data/lib/ruboty/ai_agent/llm/openai.rb +6 -6
- data/lib/ruboty/ai_agent/mcp_clients.rb +5 -12
- data/lib/ruboty/ai_agent/mcp_configuration.rb +3 -2
- data/lib/ruboty/ai_agent/prompt_command_definition.rb +17 -0
- data/lib/ruboty/ai_agent/record_set.rb +9 -5
- data/lib/ruboty/ai_agent/recordable.rb +11 -9
- data/lib/ruboty/ai_agent/request.rb +17 -0
- data/lib/ruboty/ai_agent/token_usage.rb +10 -0
- data/lib/ruboty/ai_agent/tool.rb +11 -3
- data/lib/ruboty/ai_agent/tool_definitions/base.rb +84 -0
- data/lib/ruboty/ai_agent/tool_definitions/think.rb +41 -0
- data/lib/ruboty/ai_agent/tool_definitions.rb +19 -0
- data/lib/ruboty/ai_agent/user.rb +5 -0
- data/lib/ruboty/ai_agent/user_mcp_caches.rb +2 -2
- data/lib/ruboty/ai_agent/user_mcp_client.rb +5 -4
- data/lib/ruboty/ai_agent/user_prompt_command_definitions.rb +17 -0
- data/lib/ruboty/ai_agent/version.rb +1 -1
- data/lib/ruboty/ai_agent.rb +13 -0
- data/lib/ruboty/handlers/ai_agent.rb +28 -16
- data/rbs_collection.yaml +1 -0
- data/ruboty-ai_agent.gemspec +2 -6
- data/script/clean-orphaned-rbs.rb +105 -0
- data/script/generate-concern-rbs.rb +5 -5
- data/script/generate-data-rbs.rb +3 -5
- data/script/generate-memorized-ivar-rbs.rb +6 -11
- data/sig/generated/ruboty/ai_agent/actions/add_ai_command.rbs +4 -0
- data/sig/generated/ruboty/ai_agent/actions/base.rbs +4 -0
- data/sig/generated/ruboty/ai_agent/actions/chat.rbs +10 -0
- data/sig/generated/ruboty/ai_agent/actions/list_mcp.rbs +11 -0
- data/sig/generated/ruboty/ai_agent/agent.rbs +1 -1
- data/sig/generated/ruboty/ai_agent/chat_message.rbs +5 -2
- data/sig/generated/ruboty/ai_agent/chat_thread_messages.rbs +14 -0
- data/sig/generated/ruboty/ai_agent/commands/base.rbs +8 -19
- data/sig/generated/ruboty/ai_agent/commands/builtin_base.rbs +40 -0
- data/sig/generated/ruboty/ai_agent/commands/clear.rbs +2 -10
- data/sig/generated/ruboty/ai_agent/commands/compact.rbs +2 -20
- data/sig/generated/ruboty/ai_agent/commands/prompt_command.rbs +26 -0
- data/sig/generated/ruboty/ai_agent/commands/usage.rbs +2 -10
- data/sig/generated/ruboty/ai_agent/commands.rbs +3 -4
- data/sig/generated/ruboty/ai_agent/database/query_methods.rbs +18 -12
- data/sig/generated/ruboty/ai_agent/database.rbs +3 -1
- data/sig/generated/ruboty/ai_agent/llm/openai.rbs +3 -3
- data/sig/generated/ruboty/ai_agent/mcp_clients.rbs +0 -5
- data/sig/generated/ruboty/ai_agent/mcp_configuration.rbs +4 -2
- data/sig/generated/ruboty/ai_agent/prompt_command_definition.rbs +23 -0
- data/sig/generated/ruboty/ai_agent/record_set.rbs +11 -9
- data/sig/generated/ruboty/ai_agent/recordable.rbs +6 -6
- data/sig/generated/ruboty/ai_agent/request.rbs +23 -0
- data/sig/generated/ruboty/ai_agent/token_usage.rbs +4 -0
- data/sig/generated/ruboty/ai_agent/tool.rbs +9 -3
- data/sig/generated/ruboty/ai_agent/tool_definitions/base.rbs +52 -0
- data/sig/generated/ruboty/ai_agent/tool_definitions/think.rbs +17 -0
- data/sig/generated/ruboty/ai_agent/tool_definitions.rbs +12 -0
- data/sig/generated/ruboty/ai_agent/user.rbs +4 -0
- data/sig/generated/ruboty/ai_agent/user_mcp_client.rbs +4 -2
- data/sig/generated/ruboty/ai_agent/user_prompt_command_definitions.rbs +12 -0
- data/sig/generated/ruboty/handlers/ai_agent.rbs +12 -12
- data/sig/generated-by-scripts/concerns.rbs +5 -0
- data/sig/generated-by-scripts/memorized_ivars.rbs +19 -0
- 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 <
|
7
|
+
class Compact < BuiltinBase
|
8
8
|
on(%r{/compact}, name: 'compact', description: 'Compact the chat history by summarizing it.')
|
9
9
|
|
10
|
-
|
11
|
-
|
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 =
|
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 <
|
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
|
-
|
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
|
13
|
-
# @rbs
|
14
|
-
|
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
|
18
|
-
|
19
|
-
|
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
|
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:
|
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:
|
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:
|
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:
|
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[
|
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 =
|
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 = {}, &
|
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
|
-
&
|
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
|
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
|
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:
|
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
|
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::
|
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::
|
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::
|
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::
|
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::
|
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[
|
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
|
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
|
-
|
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[
|
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:,
|
data/lib/ruboty/ai_agent/tool.rb
CHANGED
@@ -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 :
|
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
|
16
|
-
|
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
|