ruboty-ai_agent 0.1.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 +7 -0
- data/.gitignore +19 -0
- data/.rspec +3 -0
- data/.rubocop.yml +45 -0
- data/AGENTS.md +22 -0
- data/CHANGELOG.md +3 -0
- data/CLAUDE.md +1 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +14 -0
- data/README.md +118 -0
- data/Rakefile +47 -0
- data/Steepfile +12 -0
- data/bin/console +15 -0
- data/bin/ruboty +34 -0
- data/bin/setup +8 -0
- data/lib/ruboty/ai_agent/actions/add_ai_command.rb +22 -0
- data/lib/ruboty/ai_agent/actions/add_ai_memory.rb +20 -0
- data/lib/ruboty/ai_agent/actions/add_mcp.rb +94 -0
- data/lib/ruboty/ai_agent/actions/base.rb +43 -0
- data/lib/ruboty/ai_agent/actions/chat.rb +64 -0
- data/lib/ruboty/ai_agent/actions/list_ai_commands.rb +19 -0
- data/lib/ruboty/ai_agent/actions/list_ai_memories.rb +18 -0
- data/lib/ruboty/ai_agent/actions/list_mcp.rb +18 -0
- data/lib/ruboty/ai_agent/actions/remove_ai_command.rb +18 -0
- data/lib/ruboty/ai_agent/actions/remove_ai_memory.rb +25 -0
- data/lib/ruboty/ai_agent/actions/remove_mcp.rb +24 -0
- data/lib/ruboty/ai_agent/actions/set_system_prompt.rb +31 -0
- data/lib/ruboty/ai_agent/actions/show_system_prompt.rb +30 -0
- data/lib/ruboty/ai_agent/actions.rb +22 -0
- data/lib/ruboty/ai_agent/agent.rb +71 -0
- data/lib/ruboty/ai_agent/cached_value.rb +43 -0
- data/lib/ruboty/ai_agent/chat_message.rb +60 -0
- data/lib/ruboty/ai_agent/chat_thread.rb +31 -0
- data/lib/ruboty/ai_agent/chat_thread_associations.rb +34 -0
- data/lib/ruboty/ai_agent/chat_thread_messages.rb +17 -0
- data/lib/ruboty/ai_agent/commands/base.rb +39 -0
- data/lib/ruboty/ai_agent/commands/clear.rb +29 -0
- data/lib/ruboty/ai_agent/commands/compact.rb +80 -0
- data/lib/ruboty/ai_agent/commands/usage.rb +52 -0
- data/lib/ruboty/ai_agent/commands.rb +33 -0
- data/lib/ruboty/ai_agent/database/query_methods.rb +84 -0
- data/lib/ruboty/ai_agent/database.rb +40 -0
- data/lib/ruboty/ai_agent/global_settings.rb +33 -0
- data/lib/ruboty/ai_agent/http_mcp_client.rb +215 -0
- data/lib/ruboty/ai_agent/llm/openai/model.rb +29 -0
- data/lib/ruboty/ai_agent/llm/openai.rb +181 -0
- data/lib/ruboty/ai_agent/llm/response.rb +21 -0
- data/lib/ruboty/ai_agent/llm.rb +11 -0
- data/lib/ruboty/ai_agent/mcp_clients.rb +48 -0
- data/lib/ruboty/ai_agent/mcp_configuration.rb +31 -0
- data/lib/ruboty/ai_agent/record_set.rb +71 -0
- data/lib/ruboty/ai_agent/recordable.rb +116 -0
- data/lib/ruboty/ai_agent/token_usage.rb +45 -0
- data/lib/ruboty/ai_agent/tool.rb +29 -0
- data/lib/ruboty/ai_agent/user.rb +52 -0
- data/lib/ruboty/ai_agent/user_ai_memories.rb +17 -0
- data/lib/ruboty/ai_agent/user_associations.rb +34 -0
- data/lib/ruboty/ai_agent/user_mcp_caches.rb +90 -0
- data/lib/ruboty/ai_agent/user_mcp_client.rb +93 -0
- data/lib/ruboty/ai_agent/user_mcp_configurations.rb +15 -0
- data/lib/ruboty/ai_agent/user_mcp_tools_caches.rb +14 -0
- data/lib/ruboty/ai_agent/version.rb +7 -0
- data/lib/ruboty/ai_agent.rb +40 -0
- data/lib/ruboty/handlers/ai_agent.rb +84 -0
- data/rbs_collection.yaml +23 -0
- data/ruboty-ai_agent.gemspec +49 -0
- data/script/generate-concern-rbs.rb +351 -0
- data/script/generate-data-rbs.rb +250 -0
- data/script/generate-memorized-ivar-rbs.rb +292 -0
- data/sig/generated/ruboty/ai_agent/actions/add_ai_command.rbs +16 -0
- data/sig/generated/ruboty/ai_agent/actions/add_ai_memory.rbs +14 -0
- data/sig/generated/ruboty/ai_agent/actions/add_mcp.rbs +26 -0
- data/sig/generated/ruboty/ai_agent/actions/base.rbs +34 -0
- data/sig/generated/ruboty/ai_agent/actions/chat.rbs +17 -0
- data/sig/generated/ruboty/ai_agent/actions/list_ai_commands.rbs +13 -0
- data/sig/generated/ruboty/ai_agent/actions/list_ai_memories.rbs +12 -0
- data/sig/generated/ruboty/ai_agent/actions/list_mcp.rbs +12 -0
- data/sig/generated/ruboty/ai_agent/actions/remove_ai_command.rbs +14 -0
- data/sig/generated/ruboty/ai_agent/actions/remove_ai_memory.rbs +14 -0
- data/sig/generated/ruboty/ai_agent/actions/remove_mcp.rbs +14 -0
- data/sig/generated/ruboty/ai_agent/actions/set_system_prompt.rbs +16 -0
- data/sig/generated/ruboty/ai_agent/actions/show_system_prompt.rbs +12 -0
- data/sig/generated/ruboty/ai_agent/actions.rbs +9 -0
- data/sig/generated/ruboty/ai_agent/agent.rbs +29 -0
- data/sig/generated/ruboty/ai_agent/cached_value.rbs +28 -0
- data/sig/generated/ruboty/ai_agent/chat_message.rbs +34 -0
- data/sig/generated/ruboty/ai_agent/chat_thread.rbs +22 -0
- data/sig/generated/ruboty/ai_agent/chat_thread_associations.rbs +21 -0
- data/sig/generated/ruboty/ai_agent/chat_thread_messages.rbs +13 -0
- data/sig/generated/ruboty/ai_agent/commands/base.rbs +40 -0
- data/sig/generated/ruboty/ai_agent/commands/clear.rbs +20 -0
- data/sig/generated/ruboty/ai_agent/commands/compact.rbs +30 -0
- data/sig/generated/ruboty/ai_agent/commands/usage.rbs +26 -0
- data/sig/generated/ruboty/ai_agent/commands.rbs +13 -0
- data/sig/generated/ruboty/ai_agent/database/query_methods.rbs +39 -0
- data/sig/generated/ruboty/ai_agent/database.rbs +27 -0
- data/sig/generated/ruboty/ai_agent/global_settings.rbs +23 -0
- data/sig/generated/ruboty/ai_agent/http_mcp_client.rbs +62 -0
- data/sig/generated/ruboty/ai_agent/llm/openai/model.rbs +21 -0
- data/sig/generated/ruboty/ai_agent/llm/openai.rbs +54 -0
- data/sig/generated/ruboty/ai_agent/llm/response.rbs +29 -0
- data/sig/generated/ruboty/ai_agent/llm.rbs +9 -0
- data/sig/generated/ruboty/ai_agent/mcp_clients.rbs +24 -0
- data/sig/generated/ruboty/ai_agent/mcp_configuration.rbs +35 -0
- data/sig/generated/ruboty/ai_agent/record_set.rbs +42 -0
- data/sig/generated/ruboty/ai_agent/recordable.rbs +56 -0
- data/sig/generated/ruboty/ai_agent/token_usage.rbs +30 -0
- data/sig/generated/ruboty/ai_agent/tool.rbs +27 -0
- data/sig/generated/ruboty/ai_agent/user.rbs +35 -0
- data/sig/generated/ruboty/ai_agent/user_ai_memories.rbs +11 -0
- data/sig/generated/ruboty/ai_agent/user_associations.rbs +21 -0
- data/sig/generated/ruboty/ai_agent/user_mcp_caches.rbs +44 -0
- data/sig/generated/ruboty/ai_agent/user_mcp_client.rbs +58 -0
- data/sig/generated/ruboty/ai_agent/user_mcp_configurations.rbs +11 -0
- data/sig/generated/ruboty/ai_agent/user_mcp_tools_caches.rbs +11 -0
- data/sig/generated/ruboty/ai_agent/version.rbs +7 -0
- data/sig/generated/ruboty/ai_agent.rbs +9 -0
- data/sig/generated/ruboty/handlers/ai_agent.rbs +32 -0
- data/sig/generated-by-scripts/concerns.rbs +27 -0
- data/sig/generated-by-scripts/memorized_ivars.rbs +42 -0
- data/sig-lib/event_stream_parser/event_stream_parser.rbs +21 -0
- data/sig-lib/mem/mem.rbs +19 -0
- data/sig-lib/ruboty/ruboty.rbs +421 -0
- metadata +263 -0
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ruboty
|
4
|
+
module AiAgent
|
5
|
+
module Actions
|
6
|
+
# ListAiCommands action for Ruboty::AiAgent
|
7
|
+
class ListAiCommands < Base
|
8
|
+
# @rbs override
|
9
|
+
def call
|
10
|
+
commands = Commands.builtins(message:, chat_thread:)
|
11
|
+
|
12
|
+
message.reply(
|
13
|
+
commands.flat_map(&:matchers).map { |matcher| "#{matcher.pattern.inspect} - #{matcher.description}" }.join("\n")
|
14
|
+
)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ruboty
|
4
|
+
module AiAgent
|
5
|
+
module Actions
|
6
|
+
# ListAiMemories action for Ruboty::AiAgent
|
7
|
+
class ListAiMemories < Base
|
8
|
+
def call
|
9
|
+
memories = (user.ai_memories.all || {}).map do |idx, memory|
|
10
|
+
"Memory #{idx}: #{memory}"
|
11
|
+
end.join("\n")
|
12
|
+
|
13
|
+
message.reply(memories.empty? ? 'No memories found.' : memories)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ruboty
|
4
|
+
module AiAgent
|
5
|
+
module Actions
|
6
|
+
# ListMcp action for Ruboty::AiAgent
|
7
|
+
class ListMcp < Base
|
8
|
+
def call
|
9
|
+
mcp_configurations = (user.mcp_configurations.all || {}).map do |name, mcp_configuration|
|
10
|
+
"#{name}: #{mcp_configuration.to_h.except(:record_type).to_json}"
|
11
|
+
end.join("\n")
|
12
|
+
|
13
|
+
message.reply(mcp_configurations.empty? ? 'No memories found.' : mcp_configurations)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ruboty
|
4
|
+
module AiAgent
|
5
|
+
module Actions
|
6
|
+
# RemoveAiCommand action for Ruboty::AiAgent
|
7
|
+
class RemoveAiCommand < Base
|
8
|
+
def call
|
9
|
+
message.reply("TODO: Implement RemoveAiCommand action for name: #{name_param}")
|
10
|
+
end
|
11
|
+
|
12
|
+
def name_param #: String
|
13
|
+
message[:name]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ruboty
|
4
|
+
module AiAgent
|
5
|
+
module Actions
|
6
|
+
# RemoveAiMemory action for Ruboty::AiAgent
|
7
|
+
class RemoveAiMemory < Base
|
8
|
+
def call
|
9
|
+
message.reply("TODO: Implement RemoveAiMemory action for index: #{index_param}")
|
10
|
+
if user.ai_memories.key?(index_param)
|
11
|
+
user.ai_memories.remove(index_param)
|
12
|
+
|
13
|
+
message.reply("Removed memory #{index_param}.")
|
14
|
+
else
|
15
|
+
message.reply("Memory #{index_param} does not exist.")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def index_param #: String
|
20
|
+
message[:index]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ruboty
|
4
|
+
module AiAgent
|
5
|
+
module Actions
|
6
|
+
# RemoveMcp action for Ruboty::AiAgent
|
7
|
+
class RemoveMcp < Base
|
8
|
+
def call
|
9
|
+
if user.mcp_configurations.key?(name_param)
|
10
|
+
user.mcp_configurations.remove(name_param)
|
11
|
+
|
12
|
+
message.reply("Removed MCP configuration #{name_param}.")
|
13
|
+
else
|
14
|
+
message.reply("MCP configuration #{name_param} does not exist.")
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def name_param #: String
|
19
|
+
message[:name]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ruboty
|
4
|
+
module AiAgent
|
5
|
+
module Actions
|
6
|
+
# SetSystemPrompt action for Ruboty::AiAgent
|
7
|
+
class SetSystemPrompt < Base
|
8
|
+
def call
|
9
|
+
case scope_param.to_sym
|
10
|
+
when :user
|
11
|
+
user.system_prompt = prompt_param
|
12
|
+
message.reply("Set user system prompt: #{prompt_param}")
|
13
|
+
when :global
|
14
|
+
database.global_settings.system_prompt = prompt_param
|
15
|
+
message.reply("Set global system prompt: #{prompt_param}")
|
16
|
+
else
|
17
|
+
message.reply("Error: Invalid scope '#{scope_param}'. Use 'user' or 'global'.")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def prompt_param #: String
|
22
|
+
message[:prompt]
|
23
|
+
end
|
24
|
+
|
25
|
+
def scope_param #: String
|
26
|
+
message[:scope] || 'user'
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ruboty
|
4
|
+
module AiAgent
|
5
|
+
module Actions
|
6
|
+
# ShowSystemPrompt action for Ruboty::AiAgent
|
7
|
+
class ShowSystemPrompt < Base
|
8
|
+
def call
|
9
|
+
user_prompt = user.system_prompt
|
10
|
+
global_prompt = database.global_settings.system_prompt
|
11
|
+
|
12
|
+
reply = []
|
13
|
+
reply << if user_prompt
|
14
|
+
"User system prompt: #{user_prompt}"
|
15
|
+
else
|
16
|
+
'User system prompt is not set.'
|
17
|
+
end
|
18
|
+
|
19
|
+
reply << if global_prompt
|
20
|
+
"Global system prompt: #{global_prompt}"
|
21
|
+
else
|
22
|
+
'Global system prompt is not set.'
|
23
|
+
end
|
24
|
+
|
25
|
+
message.reply(reply.join("\n"))
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ruboty
|
4
|
+
module AiAgent
|
5
|
+
# Ruboty AI Agent actions.
|
6
|
+
module Actions
|
7
|
+
autoload :AddAiCommand, 'ruboty/ai_agent/actions/add_ai_command'
|
8
|
+
autoload :AddAiMemory, 'ruboty/ai_agent/actions/add_ai_memory'
|
9
|
+
autoload :AddMcp, 'ruboty/ai_agent/actions/add_mcp'
|
10
|
+
autoload :Base, 'ruboty/ai_agent/actions/base'
|
11
|
+
autoload :Chat, 'ruboty/ai_agent/actions/chat'
|
12
|
+
autoload :ListAiCommands, 'ruboty/ai_agent/actions/list_ai_commands'
|
13
|
+
autoload :ListAiMemories, 'ruboty/ai_agent/actions/list_ai_memories'
|
14
|
+
autoload :ListMcp, 'ruboty/ai_agent/actions/list_mcp'
|
15
|
+
autoload :RemoveAiCommand, 'ruboty/ai_agent/actions/remove_ai_command'
|
16
|
+
autoload :RemoveAiMemory, 'ruboty/ai_agent/actions/remove_ai_memory'
|
17
|
+
autoload :RemoveMcp, 'ruboty/ai_agent/actions/remove_mcp'
|
18
|
+
autoload :SetSystemPrompt, 'ruboty/ai_agent/actions/set_system_prompt'
|
19
|
+
autoload :ShowSystemPrompt, 'ruboty/ai_agent/actions/show_system_prompt'
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ruboty
|
4
|
+
module AiAgent
|
5
|
+
# Agent class to interact with LLM and manage conversations.
|
6
|
+
class Agent
|
7
|
+
attr_reader :llm #: LLM::OpenAI
|
8
|
+
attr_reader :messages #: Array[ChatMessage]
|
9
|
+
attr_reader :tools #: Array[Tool]
|
10
|
+
|
11
|
+
# @rbs llm: LLM::OpenAI
|
12
|
+
# @rbs messages: Array[ChatMessage]
|
13
|
+
# @rbs tools: Array[Tool]
|
14
|
+
def initialize(
|
15
|
+
llm:,
|
16
|
+
messages: [],
|
17
|
+
tools: []
|
18
|
+
)
|
19
|
+
@llm = llm
|
20
|
+
@messages = messages
|
21
|
+
@tools = tools
|
22
|
+
end
|
23
|
+
|
24
|
+
def complete(&)
|
25
|
+
loop do
|
26
|
+
response = llm.complete(
|
27
|
+
messages:,
|
28
|
+
tools:
|
29
|
+
)
|
30
|
+
on_response(response, &)
|
31
|
+
on_new_message(response.message, &)
|
32
|
+
|
33
|
+
if response.tool
|
34
|
+
on_tool_call(tool: response.tool, tool_arguments: response.tool_arguments, &)
|
35
|
+
messages << response.message
|
36
|
+
|
37
|
+
tool_response = response.call_tool || 'no return value'
|
38
|
+
tool_response_message = ChatMessage.from_llm_response(
|
39
|
+
tool: response.tool,
|
40
|
+
tool_call_id: response.tool_call_id,
|
41
|
+
tool_arguments: response.tool_arguments,
|
42
|
+
tool_response:
|
43
|
+
)
|
44
|
+
on_tool_response(tool_response:, message: tool_response_message, &)
|
45
|
+
messages << tool_response_message
|
46
|
+
else
|
47
|
+
messages << response.message
|
48
|
+
|
49
|
+
return response
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def on_new_message(message, &callback)
|
55
|
+
callback&.call({ type: :new_message, message: })
|
56
|
+
end
|
57
|
+
|
58
|
+
def on_tool_call(tool:, tool_arguments:, &callback)
|
59
|
+
callback&.call({ type: :tool_call, tool:, tool_arguments: })
|
60
|
+
end
|
61
|
+
|
62
|
+
def on_tool_response(tool_response:, message:, &callback)
|
63
|
+
callback&.call({ type: :tool_response, tool_response:, message: })
|
64
|
+
end
|
65
|
+
|
66
|
+
def on_response(response, &callback)
|
67
|
+
callback&.call({ type: :response, response: })
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'time'
|
4
|
+
|
5
|
+
module Ruboty
|
6
|
+
module AiAgent
|
7
|
+
# Cache for MCP operations
|
8
|
+
# @rbs generic D < Object
|
9
|
+
class CachedValue
|
10
|
+
include Recordable
|
11
|
+
|
12
|
+
attr_reader :expires_at #: Time
|
13
|
+
attr_reader :data #: D
|
14
|
+
|
15
|
+
# @rbs data: D
|
16
|
+
# @rbs expires_at: Time
|
17
|
+
def initialize(data:, expires_at:)
|
18
|
+
@data = data
|
19
|
+
@expires_at = expires_at.is_a?(Time) ? expires_at.round : Time.parse(expires_at)
|
20
|
+
end
|
21
|
+
|
22
|
+
# @rbs return: Hash[Symbol, untyped]
|
23
|
+
def to_h
|
24
|
+
{
|
25
|
+
expires_at: expires_at.rfc2822,
|
26
|
+
data: data
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
# @rbs return: bool
|
31
|
+
def expired?
|
32
|
+
Time.now > expires_at
|
33
|
+
end
|
34
|
+
|
35
|
+
# @rbs return: bool
|
36
|
+
def valid?
|
37
|
+
!expired?
|
38
|
+
end
|
39
|
+
|
40
|
+
register_record_type :mcp_cache
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ruboty
|
4
|
+
module AiAgent
|
5
|
+
# Save MCP configuration details.
|
6
|
+
class ChatMessage
|
7
|
+
include Recordable
|
8
|
+
|
9
|
+
register_record_type :chat_message
|
10
|
+
|
11
|
+
attr_reader :role #: Symbol
|
12
|
+
attr_reader :content #: String
|
13
|
+
attr_reader :tool_call_id #: String?
|
14
|
+
attr_reader :tool_name #: String?
|
15
|
+
attr_reader :tool_arguments #: Hash[Symbol | String, untyped]?
|
16
|
+
attr_reader :token_usage #: TokenUsage?
|
17
|
+
|
18
|
+
# @rbs role: Symbol
|
19
|
+
# @rbs content: String
|
20
|
+
# @rbs ?tool_call_id: String?
|
21
|
+
# @rbs ?tool_name: String?
|
22
|
+
# @rbs ?tool_arguments: Hash[Symbol | String, untyped]?
|
23
|
+
# @rbs ?token_usage: TokenUsage?
|
24
|
+
def initialize(role:, content:, tool_call_id: nil, tool_name: nil, tool_arguments: nil, token_usage: nil)
|
25
|
+
@role = role
|
26
|
+
@content = content
|
27
|
+
@tool_call_id = tool_call_id
|
28
|
+
@tool_name = tool_name
|
29
|
+
@tool_arguments = tool_arguments
|
30
|
+
@token_usage = token_usage
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_h #: Hash[Symbol, untyped]
|
34
|
+
{
|
35
|
+
role:,
|
36
|
+
content:,
|
37
|
+
tool_call_id:,
|
38
|
+
tool_name:,
|
39
|
+
tool_arguments:,
|
40
|
+
token_usage:
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.from_llm_response(
|
45
|
+
tool:,
|
46
|
+
tool_call_id:,
|
47
|
+
tool_arguments:,
|
48
|
+
tool_response:
|
49
|
+
) #: ChatMessage
|
50
|
+
new(
|
51
|
+
role: :tool,
|
52
|
+
content: tool_response,
|
53
|
+
tool_name: tool.name,
|
54
|
+
tool_call_id:,
|
55
|
+
tool_arguments: tool_arguments
|
56
|
+
)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ruboty
|
4
|
+
module AiAgent
|
5
|
+
# Manage thread-specific data.
|
6
|
+
class ChatThread
|
7
|
+
attr_reader :database #: Ruboty::AiAgent::Database
|
8
|
+
attr_reader :id #: String
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def find_or_create(database:, id:) #: ChatThread
|
12
|
+
new(database: database, id: id)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(database:, id:)
|
17
|
+
@database = database
|
18
|
+
@id = id
|
19
|
+
end
|
20
|
+
|
21
|
+
# @rbs %a{memorized}
|
22
|
+
def messages #: ChatThreadMessages
|
23
|
+
@messages ||= ChatThreadMessages.new(database: database, chat_thread_id: id)
|
24
|
+
end
|
25
|
+
|
26
|
+
def clear #: void
|
27
|
+
messages.clear
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ruboty
|
4
|
+
module AiAgent
|
5
|
+
# A set of records for a specific thread.
|
6
|
+
# @rbs generic Record
|
7
|
+
class ChatThreadAssociations < RecordSet #[Record]
|
8
|
+
attr_reader :chat_thread_id #: String
|
9
|
+
|
10
|
+
def initialize(database:, chat_thread_id:)
|
11
|
+
super(database:)
|
12
|
+
|
13
|
+
@chat_thread_id = chat_thread_id
|
14
|
+
end
|
15
|
+
|
16
|
+
# @rbs!
|
17
|
+
# def self.association_key: () -> Symbol
|
18
|
+
# def self.association_key=: (Symbol) -> Symbol
|
19
|
+
|
20
|
+
# @rbs skip
|
21
|
+
class << self
|
22
|
+
attr_accessor :association_key
|
23
|
+
end
|
24
|
+
|
25
|
+
def association_key #: Symbol
|
26
|
+
self.class.association_key || raise(NotImplementedError, 'Subclasses must set the association_key method')
|
27
|
+
end
|
28
|
+
|
29
|
+
def namespace_keys
|
30
|
+
[:chat_threads, chat_thread_id, association_key]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ruboty
|
4
|
+
module AiAgent
|
5
|
+
# Manage messages for the chat thread.
|
6
|
+
class ChatThreadMessages < ChatThreadAssociations #[ChatMessage]
|
7
|
+
self.association_key = :messages
|
8
|
+
|
9
|
+
# @rbs message: ChatMessage
|
10
|
+
def add(message) #: void
|
11
|
+
store(message, key: (keys.last.to_s.to_i || -1) + 1)
|
12
|
+
end
|
13
|
+
|
14
|
+
alias << add
|
15
|
+
end
|
16
|
+
end
|
17
|
+
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 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
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ruboty
|
4
|
+
module AiAgent
|
5
|
+
module Commands
|
6
|
+
# Clear histories of the chat thread.
|
7
|
+
class Clear < Base
|
8
|
+
on(%r{/clear}, name: 'clear', description: 'Clear the chat history.')
|
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
|
+
chat_thread.clear
|
24
|
+
message.reply('Cleared the chat history.')
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ruboty
|
4
|
+
module AiAgent
|
5
|
+
module Commands
|
6
|
+
# Compact chat history by summarizing it
|
7
|
+
class Compact < Base
|
8
|
+
on(%r{/compact}, name: 'compact', description: 'Compact the chat history by summarizing it.')
|
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?
|
26
|
+
message.reply('No chat history to compact.')
|
27
|
+
return
|
28
|
+
end
|
29
|
+
|
30
|
+
summary = generate_summary(messages)
|
31
|
+
|
32
|
+
chat_thread.clear
|
33
|
+
chat_thread.messages.add(
|
34
|
+
ChatMessage.new(
|
35
|
+
role: :system,
|
36
|
+
content: "Previous conversation summary: #{summary}"
|
37
|
+
)
|
38
|
+
)
|
39
|
+
|
40
|
+
message.reply('Chat history has been compacted with a summary.')
|
41
|
+
rescue StandardError => e
|
42
|
+
if ENV['DEBUG']
|
43
|
+
message.reply("エラーが発生しました: #{e.full_message}")
|
44
|
+
else
|
45
|
+
message.reply("エラーが発生しました: #{e.message}")
|
46
|
+
end
|
47
|
+
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
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ruboty
|
4
|
+
module AiAgent
|
5
|
+
module Commands
|
6
|
+
# Show token usage information for the latest AI response
|
7
|
+
class Usage < Base
|
8
|
+
on(%r{/usage}, name: 'show_usage', description: 'Show token usage information for the latest AI response')
|
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
|
+
latest_message = chat_thread.messages.all_values.find(&:token_usage)
|
24
|
+
|
25
|
+
token_usage = latest_message&.token_usage
|
26
|
+
|
27
|
+
if token_usage
|
28
|
+
usage_text = "Token usage: #{format_number(token_usage.prompt_tokens)} (prompt) + #{format_number(token_usage.completion_tokens)} (completion) = #{format_number(token_usage.total_tokens)} (total)"
|
29
|
+
|
30
|
+
limit = token_usage.token_limit
|
31
|
+
if limit
|
32
|
+
usage_percentage = token_usage.usage_percentage
|
33
|
+
usage_text += " / #{format_number(limit)} (#{usage_percentage}%)"
|
34
|
+
end
|
35
|
+
|
36
|
+
message.reply(usage_text)
|
37
|
+
else
|
38
|
+
message.reply('No token usage information found.')
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
# @rbs number: Integer
|
45
|
+
# @rbs return: String
|
46
|
+
def format_number(number)
|
47
|
+
number.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\1,').reverse
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|