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.
Files changed (124) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +45 -0
  5. data/AGENTS.md +22 -0
  6. data/CHANGELOG.md +3 -0
  7. data/CLAUDE.md +1 -0
  8. data/CODE_OF_CONDUCT.md +74 -0
  9. data/Gemfile +14 -0
  10. data/README.md +118 -0
  11. data/Rakefile +47 -0
  12. data/Steepfile +12 -0
  13. data/bin/console +15 -0
  14. data/bin/ruboty +34 -0
  15. data/bin/setup +8 -0
  16. data/lib/ruboty/ai_agent/actions/add_ai_command.rb +22 -0
  17. data/lib/ruboty/ai_agent/actions/add_ai_memory.rb +20 -0
  18. data/lib/ruboty/ai_agent/actions/add_mcp.rb +94 -0
  19. data/lib/ruboty/ai_agent/actions/base.rb +43 -0
  20. data/lib/ruboty/ai_agent/actions/chat.rb +64 -0
  21. data/lib/ruboty/ai_agent/actions/list_ai_commands.rb +19 -0
  22. data/lib/ruboty/ai_agent/actions/list_ai_memories.rb +18 -0
  23. data/lib/ruboty/ai_agent/actions/list_mcp.rb +18 -0
  24. data/lib/ruboty/ai_agent/actions/remove_ai_command.rb +18 -0
  25. data/lib/ruboty/ai_agent/actions/remove_ai_memory.rb +25 -0
  26. data/lib/ruboty/ai_agent/actions/remove_mcp.rb +24 -0
  27. data/lib/ruboty/ai_agent/actions/set_system_prompt.rb +31 -0
  28. data/lib/ruboty/ai_agent/actions/show_system_prompt.rb +30 -0
  29. data/lib/ruboty/ai_agent/actions.rb +22 -0
  30. data/lib/ruboty/ai_agent/agent.rb +71 -0
  31. data/lib/ruboty/ai_agent/cached_value.rb +43 -0
  32. data/lib/ruboty/ai_agent/chat_message.rb +60 -0
  33. data/lib/ruboty/ai_agent/chat_thread.rb +31 -0
  34. data/lib/ruboty/ai_agent/chat_thread_associations.rb +34 -0
  35. data/lib/ruboty/ai_agent/chat_thread_messages.rb +17 -0
  36. data/lib/ruboty/ai_agent/commands/base.rb +39 -0
  37. data/lib/ruboty/ai_agent/commands/clear.rb +29 -0
  38. data/lib/ruboty/ai_agent/commands/compact.rb +80 -0
  39. data/lib/ruboty/ai_agent/commands/usage.rb +52 -0
  40. data/lib/ruboty/ai_agent/commands.rb +33 -0
  41. data/lib/ruboty/ai_agent/database/query_methods.rb +84 -0
  42. data/lib/ruboty/ai_agent/database.rb +40 -0
  43. data/lib/ruboty/ai_agent/global_settings.rb +33 -0
  44. data/lib/ruboty/ai_agent/http_mcp_client.rb +215 -0
  45. data/lib/ruboty/ai_agent/llm/openai/model.rb +29 -0
  46. data/lib/ruboty/ai_agent/llm/openai.rb +181 -0
  47. data/lib/ruboty/ai_agent/llm/response.rb +21 -0
  48. data/lib/ruboty/ai_agent/llm.rb +11 -0
  49. data/lib/ruboty/ai_agent/mcp_clients.rb +48 -0
  50. data/lib/ruboty/ai_agent/mcp_configuration.rb +31 -0
  51. data/lib/ruboty/ai_agent/record_set.rb +71 -0
  52. data/lib/ruboty/ai_agent/recordable.rb +116 -0
  53. data/lib/ruboty/ai_agent/token_usage.rb +45 -0
  54. data/lib/ruboty/ai_agent/tool.rb +29 -0
  55. data/lib/ruboty/ai_agent/user.rb +52 -0
  56. data/lib/ruboty/ai_agent/user_ai_memories.rb +17 -0
  57. data/lib/ruboty/ai_agent/user_associations.rb +34 -0
  58. data/lib/ruboty/ai_agent/user_mcp_caches.rb +90 -0
  59. data/lib/ruboty/ai_agent/user_mcp_client.rb +93 -0
  60. data/lib/ruboty/ai_agent/user_mcp_configurations.rb +15 -0
  61. data/lib/ruboty/ai_agent/user_mcp_tools_caches.rb +14 -0
  62. data/lib/ruboty/ai_agent/version.rb +7 -0
  63. data/lib/ruboty/ai_agent.rb +40 -0
  64. data/lib/ruboty/handlers/ai_agent.rb +84 -0
  65. data/rbs_collection.yaml +23 -0
  66. data/ruboty-ai_agent.gemspec +49 -0
  67. data/script/generate-concern-rbs.rb +351 -0
  68. data/script/generate-data-rbs.rb +250 -0
  69. data/script/generate-memorized-ivar-rbs.rb +292 -0
  70. data/sig/generated/ruboty/ai_agent/actions/add_ai_command.rbs +16 -0
  71. data/sig/generated/ruboty/ai_agent/actions/add_ai_memory.rbs +14 -0
  72. data/sig/generated/ruboty/ai_agent/actions/add_mcp.rbs +26 -0
  73. data/sig/generated/ruboty/ai_agent/actions/base.rbs +34 -0
  74. data/sig/generated/ruboty/ai_agent/actions/chat.rbs +17 -0
  75. data/sig/generated/ruboty/ai_agent/actions/list_ai_commands.rbs +13 -0
  76. data/sig/generated/ruboty/ai_agent/actions/list_ai_memories.rbs +12 -0
  77. data/sig/generated/ruboty/ai_agent/actions/list_mcp.rbs +12 -0
  78. data/sig/generated/ruboty/ai_agent/actions/remove_ai_command.rbs +14 -0
  79. data/sig/generated/ruboty/ai_agent/actions/remove_ai_memory.rbs +14 -0
  80. data/sig/generated/ruboty/ai_agent/actions/remove_mcp.rbs +14 -0
  81. data/sig/generated/ruboty/ai_agent/actions/set_system_prompt.rbs +16 -0
  82. data/sig/generated/ruboty/ai_agent/actions/show_system_prompt.rbs +12 -0
  83. data/sig/generated/ruboty/ai_agent/actions.rbs +9 -0
  84. data/sig/generated/ruboty/ai_agent/agent.rbs +29 -0
  85. data/sig/generated/ruboty/ai_agent/cached_value.rbs +28 -0
  86. data/sig/generated/ruboty/ai_agent/chat_message.rbs +34 -0
  87. data/sig/generated/ruboty/ai_agent/chat_thread.rbs +22 -0
  88. data/sig/generated/ruboty/ai_agent/chat_thread_associations.rbs +21 -0
  89. data/sig/generated/ruboty/ai_agent/chat_thread_messages.rbs +13 -0
  90. data/sig/generated/ruboty/ai_agent/commands/base.rbs +40 -0
  91. data/sig/generated/ruboty/ai_agent/commands/clear.rbs +20 -0
  92. data/sig/generated/ruboty/ai_agent/commands/compact.rbs +30 -0
  93. data/sig/generated/ruboty/ai_agent/commands/usage.rbs +26 -0
  94. data/sig/generated/ruboty/ai_agent/commands.rbs +13 -0
  95. data/sig/generated/ruboty/ai_agent/database/query_methods.rbs +39 -0
  96. data/sig/generated/ruboty/ai_agent/database.rbs +27 -0
  97. data/sig/generated/ruboty/ai_agent/global_settings.rbs +23 -0
  98. data/sig/generated/ruboty/ai_agent/http_mcp_client.rbs +62 -0
  99. data/sig/generated/ruboty/ai_agent/llm/openai/model.rbs +21 -0
  100. data/sig/generated/ruboty/ai_agent/llm/openai.rbs +54 -0
  101. data/sig/generated/ruboty/ai_agent/llm/response.rbs +29 -0
  102. data/sig/generated/ruboty/ai_agent/llm.rbs +9 -0
  103. data/sig/generated/ruboty/ai_agent/mcp_clients.rbs +24 -0
  104. data/sig/generated/ruboty/ai_agent/mcp_configuration.rbs +35 -0
  105. data/sig/generated/ruboty/ai_agent/record_set.rbs +42 -0
  106. data/sig/generated/ruboty/ai_agent/recordable.rbs +56 -0
  107. data/sig/generated/ruboty/ai_agent/token_usage.rbs +30 -0
  108. data/sig/generated/ruboty/ai_agent/tool.rbs +27 -0
  109. data/sig/generated/ruboty/ai_agent/user.rbs +35 -0
  110. data/sig/generated/ruboty/ai_agent/user_ai_memories.rbs +11 -0
  111. data/sig/generated/ruboty/ai_agent/user_associations.rbs +21 -0
  112. data/sig/generated/ruboty/ai_agent/user_mcp_caches.rbs +44 -0
  113. data/sig/generated/ruboty/ai_agent/user_mcp_client.rbs +58 -0
  114. data/sig/generated/ruboty/ai_agent/user_mcp_configurations.rbs +11 -0
  115. data/sig/generated/ruboty/ai_agent/user_mcp_tools_caches.rbs +11 -0
  116. data/sig/generated/ruboty/ai_agent/version.rbs +7 -0
  117. data/sig/generated/ruboty/ai_agent.rbs +9 -0
  118. data/sig/generated/ruboty/handlers/ai_agent.rbs +32 -0
  119. data/sig/generated-by-scripts/concerns.rbs +27 -0
  120. data/sig/generated-by-scripts/memorized_ivars.rbs +42 -0
  121. data/sig-lib/event_stream_parser/event_stream_parser.rbs +21 -0
  122. data/sig-lib/mem/mem.rbs +19 -0
  123. data/sig-lib/ruboty/ruboty.rbs +421 -0
  124. 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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruboty
4
+ module AiAgent
5
+ VERSION = '0.1.0'
6
+ end
7
+ 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