ruboty-ai_agent 0.1.0 → 0.2.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/Gemfile +10 -3
- data/Rakefile +2 -0
- data/Steepfile +1 -0
- data/lib/ruboty/ai_agent/actions/add_ai_command.rb +16 -1
- data/lib/ruboty/ai_agent/actions/chat.rb +44 -16
- data/lib/ruboty/ai_agent/actions/list_ai_commands.rb +20 -4
- 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/chat_message.rb +7 -2
- data/lib/ruboty/ai_agent/chat_thread_messages.rb +40 -0
- data/lib/ruboty/ai_agent/commands/base.rb +11 -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 +61 -0
- data/lib/ruboty/ai_agent/commands/usage.rb +2 -14
- data/lib/ruboty/ai_agent/commands.rb +3 -1
- 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 +5 -5
- data/lib/ruboty/ai_agent/mcp_clients.rb +6 -0
- 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/token_usage.rb +10 -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 +2 -2
- 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 +11 -0
- data/lib/ruboty/handlers/ai_agent.rb +27 -15
- 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/chat.rbs +6 -0
- 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 -20
- 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 +27 -0
- data/sig/generated/ruboty/ai_agent/commands/usage.rbs +2 -10
- data/sig/generated/ruboty/ai_agent/commands.rbs +2 -2
- 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_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/token_usage.rbs +4 -0
- data/sig/generated/ruboty/ai_agent/user.rbs +4 -0
- 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 +9 -0
- metadata +11 -57
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6ee903a1f588ede1c0b6e68bea298457b3ee2fbe826494f036f9c6482288475a
|
4
|
+
data.tar.gz: 9acf203be78f86b2eeaf1f6810606a4a5b0f0a05709aba6c52b7194a589ed533
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a59e8c688b63d055d59389a95b95a6a5637bac86d917e35a4bd90b7260bc2028b2defd00c6e7bd3a5b26d5cfdb22536de5e4a1428b4e597c29ae573898021922
|
7
|
+
data.tar.gz: 1daed254a4b194c16b3b9dda2180c59e21cb5597d7c965e20905c302523b1f3a9f2a2829528e7746370fa567de487389c5846b392167c7331d8e00422a45a73e
|
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/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/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
|
|
@@ -33,6 +34,7 @@ namespace :rbs do
|
|
33
34
|
|
34
35
|
desc 'Run rbs-inline to generate RBS files'
|
35
36
|
task :inline do
|
37
|
+
sh('script/clean-orphaned-rbs.rb')
|
36
38
|
sh('bundle exec rbs-inline --opt-out --output lib')
|
37
39
|
end
|
38
40
|
|
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
|
@@ -9,28 +9,58 @@ module Ruboty
|
|
9
9
|
|
10
10
|
# @rbs override
|
11
11
|
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
|
12
|
+
user.prompt_command_definitions.all_values.each do |definition|
|
13
|
+
command = Commands::PromptCommand.new(definition:, message:, chat_thread:)
|
14
|
+
if command.match?(body_param)
|
15
|
+
new_prompt = command.call(commandline: body_param)
|
16
|
+
return complete_chat(new_prompt)
|
17
|
+
end
|
18
|
+
end
|
21
19
|
|
22
|
-
|
20
|
+
builtin_commands = Commands.builtins(message:, chat_thread:)
|
21
|
+
builtin_commands.each do |command|
|
23
22
|
return command.call if command.match?(body_param)
|
24
23
|
end
|
25
24
|
|
25
|
+
complete_chat(body_param)
|
26
|
+
end
|
27
|
+
|
28
|
+
def body_param #: String
|
29
|
+
message[:body]
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
# @rbs body: String
|
35
|
+
# @rbs return: void
|
36
|
+
def complete_chat(body)
|
26
37
|
chat_thread.messages << ChatMessage.new(
|
27
38
|
role: :user,
|
28
|
-
content:
|
39
|
+
content: body
|
29
40
|
)
|
30
41
|
|
42
|
+
messages = [] #: Array[ChatMessage]
|
43
|
+
|
44
|
+
global_prompt = database.global_settings.system_prompt
|
45
|
+
messages << ChatMessage.new(role: :system, content: global_prompt) if global_prompt
|
46
|
+
|
47
|
+
user_prompt = user.system_prompt
|
48
|
+
messages << ChatMessage.new(role: :system, content: user_prompt) if user_prompt
|
49
|
+
|
50
|
+
ai_memories = user.ai_memories.all || {}
|
51
|
+
unless ai_memories.empty?
|
52
|
+
memory_content = ai_memories.map { |_idx, memory| memory }.join("\n\n")
|
53
|
+
messages << ChatMessage.new(role: :user, content: "My memories:\n#{memory_content}")
|
54
|
+
end
|
55
|
+
|
56
|
+
messages += chat_thread.messages.all_values
|
57
|
+
|
58
|
+
llm = LLM::OpenAI.new
|
59
|
+
tools = McpClients.new(user.mcp_clients).available_tools
|
60
|
+
|
31
61
|
agent = Agent.new(
|
32
62
|
llm:,
|
33
|
-
messages
|
63
|
+
messages:,
|
34
64
|
tools:
|
35
65
|
)
|
36
66
|
|
@@ -39,6 +69,8 @@ module Ruboty
|
|
39
69
|
when :new_message
|
40
70
|
chat_thread.messages << event[:message]
|
41
71
|
message.reply(event[:message].content) if event[:message].content.length.positive?
|
72
|
+
|
73
|
+
chat_thread.messages.compact(llm:) if chat_thread.messages.over_auto_compact_threshold?
|
42
74
|
when :tool_call
|
43
75
|
message.reply("Calling tool #{event[:tool].name} with arguments #{event[:tool_arguments]}",
|
44
76
|
streaming: true)
|
@@ -54,10 +86,6 @@ module Ruboty
|
|
54
86
|
message.reply("エラーが発生しました: #{e.message}")
|
55
87
|
end
|
56
88
|
end
|
57
|
-
|
58
|
-
def body_param #: String
|
59
|
-
message[:body]
|
60
|
-
end
|
61
89
|
end
|
62
90
|
end
|
63
91
|
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(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,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
|
@@ -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,29 @@ module Ruboty
|
|
6
6
|
# Base class for commands.
|
7
7
|
# @abstract
|
8
8
|
class Base
|
9
|
-
|
9
|
+
attr_reader :message #: Ruboty::Message
|
10
|
+
attr_reader :chat_thread #: Ruboty::AiAgent::ChatThread
|
10
11
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
15
17
|
|
16
|
-
|
17
|
-
matchers << Matcher.new(pattern:, description:, name:)
|
18
|
-
end
|
18
|
+
super()
|
19
19
|
end
|
20
20
|
|
21
21
|
# @rbs *args: untyped
|
22
|
-
# @rbs return:
|
22
|
+
# @rbs return: untyped
|
23
23
|
def call(*args)
|
24
24
|
raise NotImplementedError
|
25
25
|
end
|
26
26
|
|
27
27
|
# @rbs commandline: String
|
28
28
|
# @rbs return: boolish
|
29
|
+
# @abstract
|
29
30
|
def match?(commandline)
|
30
|
-
|
31
|
-
end
|
32
|
-
|
33
|
-
def matchers #: Array[Matcher]
|
34
|
-
self.class.matchers
|
31
|
+
raise NotImplementedError
|
35
32
|
end
|
36
33
|
end
|
37
34
|
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
|
@@ -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,61 @@
|
|
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 message: Ruboty::Message
|
14
|
+
# @rbs chat_thread: Ruboty::AiAgent::ChatThread
|
15
|
+
def initialize(definition:, message:, chat_thread:)
|
16
|
+
@definition = definition
|
17
|
+
|
18
|
+
super(message:, chat_thread:)
|
19
|
+
end
|
20
|
+
|
21
|
+
# @rbs commandline: String
|
22
|
+
# @rbs return: boolish
|
23
|
+
def match?(commandline)
|
24
|
+
pattern.match?(commandline)
|
25
|
+
end
|
26
|
+
|
27
|
+
def pattern #: Regexp
|
28
|
+
%r{\A\s*/#{definition.name}(?:\s+(?<args>.+))?}
|
29
|
+
end
|
30
|
+
|
31
|
+
# @rbs commandline: String
|
32
|
+
# @rbs return: String
|
33
|
+
def call(commandline:)
|
34
|
+
result = definition.prompt.dup
|
35
|
+
|
36
|
+
match = pattern.match(commandline) || (raise 'Unreachable')
|
37
|
+
args = begin
|
38
|
+
Shellwords.split(match[:args] || '')
|
39
|
+
rescue ArgumentError
|
40
|
+
# If parsing fails, treat the entire string as a single argument
|
41
|
+
[match[:args]].compact
|
42
|
+
end #: Array[String]
|
43
|
+
|
44
|
+
found = {} #: Hash[Integer, boolish]
|
45
|
+
args.each_with_index do |arg, index|
|
46
|
+
placeholder = "$#{index + 1}"
|
47
|
+
result = result.gsub(placeholder) do
|
48
|
+
found[index] = true
|
49
|
+
arg
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
rest_args = args.each_with_index.reject { |_, index| found[index] }.map(&:first)
|
54
|
+
result += "\n\n#{rest_args.join(' ')}" if rest_args.any?
|
55
|
+
|
56
|
+
result
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
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,13 +5,15 @@ 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
14
|
# @rbs message: Ruboty::Message
|
13
15
|
# @rbs chat_thread: ChatThread
|
14
|
-
# @rbs return: Array[Commands::
|
16
|
+
# @rbs return: Array[Commands::BuiltinBase]
|
15
17
|
def self.builtins(message:, chat_thread:)
|
16
18
|
[
|
17
19
|
Commands::Clear.new(
|