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,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ruboty
|
4
|
+
module AiAgent
|
5
|
+
# @rbs!
|
6
|
+
# type transports = :http | :websocket
|
7
|
+
|
8
|
+
McpConfiguration = Data.define(
|
9
|
+
:name, #: String
|
10
|
+
:transport, #: transports
|
11
|
+
:headers, #: Hash[String, String]
|
12
|
+
:url #: String
|
13
|
+
)
|
14
|
+
|
15
|
+
# Save MCP configuration details.
|
16
|
+
class McpConfiguration
|
17
|
+
include Recordable
|
18
|
+
|
19
|
+
# @rbs name: String
|
20
|
+
# @rbs transport: transports
|
21
|
+
# @rbs url: String
|
22
|
+
# @rbs headers: Hash[String, String]?
|
23
|
+
def initialize(name:, transport:, url:, headers: {})
|
24
|
+
# No superclass method `initialize` in RBS.
|
25
|
+
super(name:, transport:, headers:, url:) # steep:ignore UnexpectedKeywordArgument
|
26
|
+
end
|
27
|
+
|
28
|
+
register_record_type :mcp_configuration
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ruboty
|
4
|
+
module AiAgent
|
5
|
+
# A set of records.
|
6
|
+
# @rbs generic Record
|
7
|
+
class RecordSet
|
8
|
+
attr_reader :database #: Ruboty::AiAgent::Database
|
9
|
+
|
10
|
+
def initialize(database:)
|
11
|
+
@database = database
|
12
|
+
end
|
13
|
+
|
14
|
+
def namespace_keys #: Array[Database::keynable]
|
15
|
+
raise NotImplementedError, 'Subclasses must implement the namespace_keys method'
|
16
|
+
end
|
17
|
+
|
18
|
+
def length #: Integer
|
19
|
+
database.len(*namespace_keys)
|
20
|
+
end
|
21
|
+
|
22
|
+
def all #: untyped
|
23
|
+
database.fetch(*namespace_keys)
|
24
|
+
end
|
25
|
+
|
26
|
+
def all_values #: Array[Record]
|
27
|
+
case (kv = all)
|
28
|
+
when Hash
|
29
|
+
kv.values
|
30
|
+
when Array
|
31
|
+
kv
|
32
|
+
else
|
33
|
+
[]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def keys #: Array[Database::keynable]
|
38
|
+
database.keys(*namespace_keys)
|
39
|
+
end
|
40
|
+
|
41
|
+
# @rbs key: Database::keynable
|
42
|
+
# @rbs return: Record | nil
|
43
|
+
def fetch(key)
|
44
|
+
database.fetch(*namespace_keys, key)
|
45
|
+
end
|
46
|
+
|
47
|
+
# @rbs key: Database::keynable
|
48
|
+
# @rbs record: Record
|
49
|
+
# @rbs return: void
|
50
|
+
def store(record, key:)
|
51
|
+
database.store(record, at: [*namespace_keys, key])
|
52
|
+
end
|
53
|
+
|
54
|
+
# @rbs key: Database::keynable
|
55
|
+
# @rbs return: void
|
56
|
+
def remove(key)
|
57
|
+
database.delete(*namespace_keys, key)
|
58
|
+
end
|
59
|
+
|
60
|
+
# @rbs key: Database::keynable
|
61
|
+
# @rbs return: boolish
|
62
|
+
def key?(key)
|
63
|
+
database.key?(*namespace_keys, key)
|
64
|
+
end
|
65
|
+
|
66
|
+
def clear #: void
|
67
|
+
database.delete(*namespace_keys)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ruboty
|
4
|
+
module AiAgent
|
5
|
+
# @rbs!
|
6
|
+
# interface _WithToH
|
7
|
+
# def to_h: () -> Hash[Database::keynable, untyped]
|
8
|
+
# end
|
9
|
+
|
10
|
+
# Convertable between Hash and Recordable bidirectionally.
|
11
|
+
# @rbs module-self _WithToH
|
12
|
+
module Recordable
|
13
|
+
class << self
|
14
|
+
def included(base)
|
15
|
+
base.extend(ClassMethods)
|
16
|
+
base.prepend(PrependMethods)
|
17
|
+
end
|
18
|
+
|
19
|
+
# @rbs @record_types: Hash[Symbol, Class]
|
20
|
+
|
21
|
+
def record_types #: Hash[Symbol, Class]
|
22
|
+
@record_types ||= {}
|
23
|
+
end
|
24
|
+
|
25
|
+
# @rbs hash: Hash[Symbol, untyped]?
|
26
|
+
# @rbs return: bool
|
27
|
+
def convertable?(hash)
|
28
|
+
return false unless hash.is_a?(Hash)
|
29
|
+
|
30
|
+
type = hash[:record_type]
|
31
|
+
type && record_types.include?(type)
|
32
|
+
end
|
33
|
+
|
34
|
+
# @rbs value: untyped
|
35
|
+
# @rbs return: untyped
|
36
|
+
def instantiate_recursively(value)
|
37
|
+
case value
|
38
|
+
when Hash
|
39
|
+
transformed = value.transform_values { |v| instantiate_recursively(v) }
|
40
|
+
if convertable?(transformed)
|
41
|
+
record_from_hash(transformed)
|
42
|
+
else
|
43
|
+
transformed
|
44
|
+
end
|
45
|
+
when Array
|
46
|
+
value.map { |v| instantiate_recursively(v) }
|
47
|
+
else
|
48
|
+
value
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# @rbs value: untyped
|
53
|
+
# @rbs return: untyped
|
54
|
+
def hashify_recursively(value)
|
55
|
+
case value
|
56
|
+
when Recordable
|
57
|
+
hashify_recursively(value.to_h)
|
58
|
+
when Hash
|
59
|
+
value.transform_values { |v| hashify_recursively(v) }
|
60
|
+
when Array
|
61
|
+
value.map { |v| hashify_recursively(v) }
|
62
|
+
else
|
63
|
+
value
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# @rbs record: Recordable
|
68
|
+
# @rbs return: Hash[Database::keynable, untyped]
|
69
|
+
def record_to_hash(record)
|
70
|
+
record.to_h
|
71
|
+
end
|
72
|
+
|
73
|
+
# @rbs hash: Hash[Symbol, untyped]
|
74
|
+
# @rbs return: Recordable
|
75
|
+
def record_from_hash(hash)
|
76
|
+
type = hash[:record_type]
|
77
|
+
klass = record_types[type]
|
78
|
+
raise "Unknown record type: #{type}" unless klass
|
79
|
+
|
80
|
+
klass.new(**hash.except(:record_type))
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# @rbs module-self Class
|
85
|
+
module ClassMethods
|
86
|
+
attr_accessor :record_type #: Symbol
|
87
|
+
|
88
|
+
# @rbs name: Symbol
|
89
|
+
def register_record_type(name)
|
90
|
+
name = name.to_sym
|
91
|
+
self.record_type = name
|
92
|
+
|
93
|
+
Recordable.record_types.merge!({ name => self }) do
|
94
|
+
raise "Duplicate record type: #{name}"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# @rbs module-self Recordable::ClassMethods.instance
|
100
|
+
module PrependMethods
|
101
|
+
def to_h #: Hash[Database::keynable, untyped]
|
102
|
+
{
|
103
|
+
record_type: record_type,
|
104
|
+
**super
|
105
|
+
}
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# @rbs %a{pure}
|
110
|
+
def record_type #: Symbol
|
111
|
+
self.class #: singleton(::Object) & ClassMethods
|
112
|
+
.record_type
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ruboty
|
4
|
+
module AiAgent
|
5
|
+
# Token usage information from LLM responses
|
6
|
+
class TokenUsage
|
7
|
+
include Recordable
|
8
|
+
|
9
|
+
register_record_type :token_usage
|
10
|
+
|
11
|
+
attr_reader :prompt_tokens #: Integer
|
12
|
+
attr_reader :completion_tokens #: Integer
|
13
|
+
attr_reader :total_tokens #: Integer
|
14
|
+
attr_reader :token_limit #: Integer?
|
15
|
+
|
16
|
+
# @rbs prompt_tokens: Integer
|
17
|
+
# @rbs completion_tokens: Integer
|
18
|
+
# @rbs total_tokens: Integer
|
19
|
+
# @rbs ?token_limit: Integer?
|
20
|
+
def initialize(prompt_tokens:, completion_tokens:, total_tokens:, token_limit: nil)
|
21
|
+
@prompt_tokens = prompt_tokens
|
22
|
+
@completion_tokens = completion_tokens
|
23
|
+
@total_tokens = total_tokens
|
24
|
+
@token_limit = token_limit
|
25
|
+
end
|
26
|
+
|
27
|
+
# Calculate usage percentage if token limit is available
|
28
|
+
# @rbs return: (Float | nil)
|
29
|
+
def usage_percentage
|
30
|
+
return nil unless token_limit
|
31
|
+
|
32
|
+
(total_tokens.to_f / token_limit * 100).round(2).to_f
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_h #: Hash[Symbol, untyped]
|
36
|
+
{
|
37
|
+
prompt_tokens:,
|
38
|
+
completion_tokens:,
|
39
|
+
total_tokens:,
|
40
|
+
token_limit:
|
41
|
+
}
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ruboty
|
4
|
+
module AiAgent
|
5
|
+
# Define a tool that the AI agent can use.
|
6
|
+
class Tool
|
7
|
+
attr_reader :name, :title, :description #: String
|
8
|
+
attr_reader :input_schema #: Hash[untyped, untyped]?
|
9
|
+
attr_reader :on_call #: (^(Hash[String, untyped]) -> String )?
|
10
|
+
|
11
|
+
# @rbs name: String
|
12
|
+
# @rbs title: String
|
13
|
+
# @rbs description: String
|
14
|
+
# @rbs input_schema: Hash[untyped, untyped]?
|
15
|
+
# @rbs &on_call: ? (Hash[String, untyped]) -> String
|
16
|
+
def initialize(name:, title:, description:, input_schema:, &on_call) #: void
|
17
|
+
@name = name
|
18
|
+
@title = title
|
19
|
+
@description = description
|
20
|
+
@input_schema = input_schema
|
21
|
+
@on_call = on_call
|
22
|
+
end
|
23
|
+
|
24
|
+
def call(params) #: String?
|
25
|
+
on_call&.call(params)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ruboty
|
4
|
+
module AiAgent
|
5
|
+
# User class to manage user-specific data.
|
6
|
+
class User
|
7
|
+
attr_reader :database #: Ruboty::AiAgent::Database
|
8
|
+
attr_reader :id #: String
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def find_or_create(database:, id:) #: User
|
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 mcp_configurations #: UserMcpConfigurations
|
23
|
+
@mcp_configurations ||= UserMcpConfigurations.new(database: database, user_id: id)
|
24
|
+
end
|
25
|
+
|
26
|
+
def mcp_clients #: Array[UserMcpClient]
|
27
|
+
mcp_configurations.all_values.map do |config|
|
28
|
+
UserMcpClient.new(user: self, mcp_name: config.name)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# @rbs %a{memorized}
|
33
|
+
def ai_memories #: UserAiMemories
|
34
|
+
@ai_memories ||= UserAiMemories.new(database: database, user_id: id)
|
35
|
+
end
|
36
|
+
|
37
|
+
def system_prompt #: String?
|
38
|
+
database.fetch(:users, id, :system_prompt)
|
39
|
+
end
|
40
|
+
|
41
|
+
# @rbs prompt: String?
|
42
|
+
def system_prompt=(prompt)
|
43
|
+
database.store(prompt, at: [:users, id, :system_prompt])
|
44
|
+
end
|
45
|
+
|
46
|
+
# @rbs %a{memorized}
|
47
|
+
def mcp_tools_caches #: UserMcpToolsCaches
|
48
|
+
@mcp_tools_caches ||= UserMcpToolsCaches.new(database: database, user_id: id)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ruboty
|
4
|
+
module AiAgent
|
5
|
+
# Manage Ai Memories for a specific user.
|
6
|
+
class UserAiMemories < UserAssociations #[String]
|
7
|
+
self.association_key = :ai_memories
|
8
|
+
|
9
|
+
# @rbs memory: String
|
10
|
+
def add(memory) #: void
|
11
|
+
next_id = ((keys.map { |key| Integer(key) }.max || 0) + 1).to_s # steep:ignore
|
12
|
+
store(memory, key: next_id)
|
13
|
+
next_id
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
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 user.
|
6
|
+
# @rbs generic Record
|
7
|
+
class UserAssociations < RecordSet #[Record]
|
8
|
+
attr_reader :user_id #: String
|
9
|
+
|
10
|
+
def initialize(database:, user_id:)
|
11
|
+
super(database:)
|
12
|
+
|
13
|
+
@user_id = user_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
|
+
[:users, user_id, association_key]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ruboty
|
4
|
+
module AiAgent
|
5
|
+
# Manage MCP caches for a specific user.
|
6
|
+
# @abstract
|
7
|
+
# @rbs generic D < Object
|
8
|
+
class UserMcpCaches < UserAssociations #[CachedValue[D]]
|
9
|
+
# @rbs!
|
10
|
+
# def self.cache_type_key: () -> Symbol?
|
11
|
+
# def self.cache_type_key=: (Symbol?) -> Symbol?
|
12
|
+
|
13
|
+
# @rbs!
|
14
|
+
# def self.cache_duration: () -> Integer?
|
15
|
+
# def self.cache_duration=: (Integer?) -> Integer?
|
16
|
+
|
17
|
+
class << self
|
18
|
+
# @rbs skip
|
19
|
+
attr_accessor :cache_type_key
|
20
|
+
# @rbs skip
|
21
|
+
attr_accessor :cache_duration
|
22
|
+
|
23
|
+
def association_key #: Symbol
|
24
|
+
raise 'Subclasses must set the cache_type_key method' unless cache_type_key
|
25
|
+
|
26
|
+
:"mcp_caches:#{cache_type_key}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def cache_type_key #: Symbol
|
31
|
+
self.class.cache_type_key || raise(NotImplementedError, 'Subclasses must set the cache_type_key method')
|
32
|
+
end
|
33
|
+
|
34
|
+
def cache_duration #: Integer
|
35
|
+
self.class.cache_duration || raise(NotImplementedError, 'Subclasses must set the cache_duration method')
|
36
|
+
end
|
37
|
+
|
38
|
+
# @rbs key: String)
|
39
|
+
# @rbs &block: () -> (D | CachedValue[D])
|
40
|
+
# @rbs return: D
|
41
|
+
def fetch_or_store_data(key, &block)
|
42
|
+
if (fetched = fetch_data(key))
|
43
|
+
fetched
|
44
|
+
else
|
45
|
+
data = block.call
|
46
|
+
cache = data.is_a?(CachedValue) ? data : CachedValue.new(data:, expires_at: Time.now + cache_duration)
|
47
|
+
store(cache, key:)
|
48
|
+
cache.data
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# @rbs key: String
|
53
|
+
# @rbs return: D?
|
54
|
+
def fetch_data(key)
|
55
|
+
fetch(key)&.data
|
56
|
+
end
|
57
|
+
|
58
|
+
# @rbs override
|
59
|
+
def all
|
60
|
+
super().reject { |(_key, cache)| cache.expired? }
|
61
|
+
end
|
62
|
+
|
63
|
+
# @rbs override
|
64
|
+
def fetch(key)
|
65
|
+
cache = super(key)
|
66
|
+
|
67
|
+
if cache&.expired?
|
68
|
+
remove(key)
|
69
|
+
nil
|
70
|
+
end
|
71
|
+
|
72
|
+
cache
|
73
|
+
end
|
74
|
+
|
75
|
+
# @rbs key: String
|
76
|
+
# @rbs data: D
|
77
|
+
# @rbs return: void
|
78
|
+
def store_data(data, key:)
|
79
|
+
expires_at = Time.now + cache_duration
|
80
|
+
|
81
|
+
cache = CachedValue.new(
|
82
|
+
data:,
|
83
|
+
expires_at:
|
84
|
+
) #: CachedValue[D]
|
85
|
+
|
86
|
+
store(cache, key:)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ruboty
|
4
|
+
module AiAgent
|
5
|
+
# Wrapper for MCP client with caching support for a specific user
|
6
|
+
class UserMcpClient
|
7
|
+
attr_reader :user #: User
|
8
|
+
attr_reader :mcp_name #: String
|
9
|
+
|
10
|
+
# @rbs user: User
|
11
|
+
# @rbs mcp_name: String
|
12
|
+
# @rbs return: void
|
13
|
+
def initialize(user:, mcp_name:)
|
14
|
+
@user = user
|
15
|
+
@mcp_name = mcp_name
|
16
|
+
end
|
17
|
+
|
18
|
+
# @rbs return: Array[tool_def]
|
19
|
+
def list_tools
|
20
|
+
user.mcp_tools_caches.fetch_or_store_data(mcp_name) do
|
21
|
+
mcp_client.list_tools
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# @rbs name: String
|
26
|
+
# @rbs arguments: Hash[String, untyped]
|
27
|
+
# @rbs &block: ? (Hash[String, untyped]) -> void
|
28
|
+
# @rbs return: untyped
|
29
|
+
def call_tool(name, arguments = {}, &block)
|
30
|
+
mcp_client.call_tool(name, arguments, &block)
|
31
|
+
end
|
32
|
+
|
33
|
+
# @rbs return: untyped
|
34
|
+
def list_prompts
|
35
|
+
mcp_client.list_prompts
|
36
|
+
end
|
37
|
+
|
38
|
+
# @rbs name: String
|
39
|
+
# @rbs arguments: Hash[String, untyped]
|
40
|
+
# @rbs return: untyped
|
41
|
+
def get_prompt(name, arguments = {})
|
42
|
+
mcp_client.get_prompt(name, arguments)
|
43
|
+
end
|
44
|
+
|
45
|
+
# @rbs return: untyped
|
46
|
+
def list_resources
|
47
|
+
mcp_client.list_resources
|
48
|
+
end
|
49
|
+
|
50
|
+
# @rbs uri: String
|
51
|
+
# @rbs return: untyped
|
52
|
+
def read_resource(uri)
|
53
|
+
mcp_client.read_resource(uri)
|
54
|
+
end
|
55
|
+
|
56
|
+
# @rbs return: untyped
|
57
|
+
def ping
|
58
|
+
mcp_client.ping
|
59
|
+
end
|
60
|
+
|
61
|
+
# @rbs return: untyped
|
62
|
+
def initialize_session
|
63
|
+
mcp_client.initialize_session
|
64
|
+
end
|
65
|
+
|
66
|
+
# @rbs return: untyped
|
67
|
+
def cleanup_session
|
68
|
+
mcp_client.cleanup_session
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
# @rbs return: McpConfiguration
|
74
|
+
def configuration
|
75
|
+
user.mcp_configurations.all_values.find { |config| config.name == mcp_name } ||
|
76
|
+
raise("MCP configuration not found: #{mcp_name}")
|
77
|
+
end
|
78
|
+
|
79
|
+
# @rbs return: HttpMcpClient
|
80
|
+
def mcp_client
|
81
|
+
@mcp_client ||= case configuration.transport
|
82
|
+
when :http
|
83
|
+
HttpMcpClient.new(
|
84
|
+
url: configuration.url,
|
85
|
+
headers: configuration.headers || {}
|
86
|
+
)
|
87
|
+
else
|
88
|
+
raise "Unknown MCP server type: #{configuration.transport}"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ruboty
|
4
|
+
module AiAgent
|
5
|
+
# Manage MCP configurations for a specific user.
|
6
|
+
class UserMcpConfigurations < UserAssociations #[McpConfiguration]
|
7
|
+
self.association_key = :mcp_configurations
|
8
|
+
|
9
|
+
# @rbs mcp_configuration: McpConfiguration:
|
10
|
+
def add(mcp_configuration) #: void
|
11
|
+
store(mcp_configuration, key: mcp_configuration.name)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ruboty
|
4
|
+
module AiAgent
|
5
|
+
# @rbs!
|
6
|
+
# type tool_def = Hash[Symbol | String, untyped]
|
7
|
+
|
8
|
+
# Manage tools cache for a specific user and server.
|
9
|
+
class UserMcpToolsCaches < UserMcpCaches #[Array[tool_def]]
|
10
|
+
self.cache_type_key = :tools
|
11
|
+
self.cache_duration = ENV.fetch('MCP_CACHE_DURATION', 600).to_i
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
require 'ruboty/ai_agent/version'
|
6
|
+
require 'ruboty/handlers/ai_agent'
|
7
|
+
|
8
|
+
module Ruboty
|
9
|
+
# Add AI Agent feature to Ruboty.
|
10
|
+
module AiAgent
|
11
|
+
class Error < StandardError; end
|
12
|
+
|
13
|
+
autoload :Actions, 'ruboty/ai_agent/actions'
|
14
|
+
autoload :Agent, 'ruboty/ai_agent/agent'
|
15
|
+
autoload :ChatMessage, 'ruboty/ai_agent/chat_message'
|
16
|
+
autoload :ChatThread, 'ruboty/ai_agent/chat_thread'
|
17
|
+
autoload :ChatThreadAssociations, 'ruboty/ai_agent/chat_thread_associations'
|
18
|
+
autoload :ChatThreadMessages, 'ruboty/ai_agent/chat_thread_messages'
|
19
|
+
autoload :Commands, 'ruboty/ai_agent/commands'
|
20
|
+
autoload :Database, 'ruboty/ai_agent/database'
|
21
|
+
autoload :GlobalSettings, 'ruboty/ai_agent/global_settings'
|
22
|
+
autoload :HttpMcpClient, 'ruboty/ai_agent/http_mcp_client'
|
23
|
+
autoload :LLM, 'ruboty/ai_agent/llm'
|
24
|
+
autoload :CachedValue, 'ruboty/ai_agent/cached_value'
|
25
|
+
autoload :McpClient, 'ruboty/ai_agent/mcp_client'
|
26
|
+
autoload :McpClients, 'ruboty/ai_agent/mcp_clients'
|
27
|
+
autoload :McpConfiguration, 'ruboty/ai_agent/mcp_configuration'
|
28
|
+
autoload :Recordable, 'ruboty/ai_agent/recordable'
|
29
|
+
autoload :RecordSet, 'ruboty/ai_agent/record_set'
|
30
|
+
autoload :TokenUsage, 'ruboty/ai_agent/token_usage'
|
31
|
+
autoload :Tool, 'ruboty/ai_agent/tool'
|
32
|
+
autoload :User, 'ruboty/ai_agent/user'
|
33
|
+
autoload :UserAiMemories, 'ruboty/ai_agent/user_ai_memories'
|
34
|
+
autoload :UserAssociations, 'ruboty/ai_agent/user_associations'
|
35
|
+
autoload :UserMcpCaches, 'ruboty/ai_agent/user_mcp_caches'
|
36
|
+
autoload :UserMcpClient, 'ruboty/ai_agent/user_mcp_client'
|
37
|
+
autoload :UserMcpConfigurations, 'ruboty/ai_agent/user_mcp_configurations'
|
38
|
+
autoload :UserMcpToolsCaches, 'ruboty/ai_agent/user_mcp_tools_caches'
|
39
|
+
end
|
40
|
+
end
|