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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4358837ce9305dcb0965a67c957b75e8320aad2988647a9e6ae751a98e7b7009
|
4
|
+
data.tar.gz: 3b1ad1c43bc443c5d86b1ea80bd8d9a45263056bc91ade861058e600493cb1b7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
1
|
+
## Unreleased
|
2
|
+
## 0.3.0
|
2
3
|
|
3
|
-
|
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
@@ -6,7 +6,14 @@ module Ruboty
|
|
6
6
|
# AddAiCommand action for Ruboty::AiAgent
|
7
7
|
class AddAiCommand < Base
|
8
8
|
def call
|
9
|
-
|
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.
|
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(
|
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
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
-
|
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:
|
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
|
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
|
-
|
10
|
+
builtin_commands = Commands.builtins(request: Request.new(message:, chat_thread:))
|
11
11
|
|
12
|
-
|
13
|
-
|
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
|
-
|
10
|
-
|
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
|
-
|
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
|
-
|
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
|
@@ -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
|
-
|
9
|
+
attr_reader :request #: Request
|
10
10
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
end
|
11
|
+
# @rbs request: Request
|
12
|
+
def initialize(request:)
|
13
|
+
@request = request
|
15
14
|
|
16
|
-
|
17
|
-
|
18
|
-
|
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:
|
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
|
-
|
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 <
|
7
|
+
class Clear < BuiltinBase
|
8
8
|
on(%r{/clear}, name: 'clear', description: 'Clear the chat history.')
|
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
|
chat_thread.clear
|
24
12
|
message.reply('Cleared the chat history.')
|
25
13
|
end
|