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,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ruboty'
|
4
|
+
|
5
|
+
module Ruboty
|
6
|
+
module Handlers
|
7
|
+
# Handler for AI agent commands
|
8
|
+
class AiAgent < Base
|
9
|
+
env :OPENAI_API_KEY, 'Pass your OpenAI API Key'
|
10
|
+
env :OPENAI_MODEL, 'OpenAI model to use', optional: true
|
11
|
+
|
12
|
+
on(
|
13
|
+
/(?<body>.+)/m,
|
14
|
+
description: 'AI responds to your message if given message did not match any other handlers',
|
15
|
+
missing: true,
|
16
|
+
name: 'chat'
|
17
|
+
)
|
18
|
+
|
19
|
+
on(/add mcp (?<name>\S+)\s+(?<config>.+)\z/, name: 'add_mcp', description: 'Add a new MCP server')
|
20
|
+
on(/remove mcp (?<name>\S+)/, name: 'remove_mcp', description: 'Remove the specified MCP server')
|
21
|
+
on(/list mcps?/, name: 'list_mcp', description: 'List configured MCP servers')
|
22
|
+
|
23
|
+
on(/set system prompt "(?<prompt>.+?)"(?: in (?<scope>user|global) scope)?/, name: 'set_system_prompt', description: 'Set system prompt')
|
24
|
+
on(/show system prompt/, name: 'show_system_prompt', description: 'Show system prompt')
|
25
|
+
|
26
|
+
on(/add ai memory "(?<prompt>.+)"/, name: 'add_ai_memory', description: 'Add a new AI memory')
|
27
|
+
on(/remove ai memory (?<index>\d+)/, name: 'remove_ai_memory', description: 'Remove the specified AI memory')
|
28
|
+
on(/list ai memor(?:y|ies)/, name: 'list_mcp', description: "List AI's memories")
|
29
|
+
|
30
|
+
on(%r{add ai command /(?<name>\S+)\s+"(?<prompt>.+)"}, name: 'add_ai_command',
|
31
|
+
description: 'Add a new AI command')
|
32
|
+
on(%r{remove ai command /(?<name>\S+)}, name: 'remove_ai_command', description: 'Remove the specified AI command')
|
33
|
+
on(/list ai commands?/, name: 'list_ai_commands', description: 'List AI commands')
|
34
|
+
|
35
|
+
def chat(message)
|
36
|
+
Ruboty::AiAgent::Actions::Chat.call(message)
|
37
|
+
end
|
38
|
+
|
39
|
+
def add_mcp(message)
|
40
|
+
Ruboty::AiAgent::Actions::AddMcp.call(message)
|
41
|
+
end
|
42
|
+
|
43
|
+
def remove_mcp(message)
|
44
|
+
Ruboty::AiAgent::Actions::RemoveMcp.call(message)
|
45
|
+
end
|
46
|
+
|
47
|
+
def list_mcp(message)
|
48
|
+
Ruboty::AiAgent::Actions::ListMcp.call(message)
|
49
|
+
end
|
50
|
+
|
51
|
+
def set_system_prompt(message)
|
52
|
+
Ruboty::AiAgent::Actions::SetSystemPrompt.call(message)
|
53
|
+
end
|
54
|
+
|
55
|
+
def show_system_prompt(message)
|
56
|
+
Ruboty::AiAgent::Actions::ShowSystemPrompt.call(message)
|
57
|
+
end
|
58
|
+
|
59
|
+
def add_ai_memory(message)
|
60
|
+
Ruboty::AiAgent::Actions::AddAiMemory.call(message)
|
61
|
+
end
|
62
|
+
|
63
|
+
def remove_ai_memory(message)
|
64
|
+
Ruboty::AiAgent::Actions::RemoveAiMemory.call(message)
|
65
|
+
end
|
66
|
+
|
67
|
+
def list_ai_memories(message)
|
68
|
+
Ruboty::AiAgent::Actions::ListAiMemories.call(message)
|
69
|
+
end
|
70
|
+
|
71
|
+
def add_ai_command(message)
|
72
|
+
Ruboty::AiAgent::Actions::AddAiCommand.call(message)
|
73
|
+
end
|
74
|
+
|
75
|
+
def remove_ai_command(message)
|
76
|
+
Ruboty::AiAgent::Actions::RemoveAiCommand.call(message)
|
77
|
+
end
|
78
|
+
|
79
|
+
def list_ai_commands(message)
|
80
|
+
Ruboty::AiAgent::Actions::ListAiCommands.call(message)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
data/rbs_collection.yaml
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# Download sources
|
2
|
+
sources:
|
3
|
+
- type: git
|
4
|
+
name: ruby/gem_rbs_collection
|
5
|
+
remote: https://github.com/ruby/gem_rbs_collection.git
|
6
|
+
revision: main
|
7
|
+
repo_dir: gems
|
8
|
+
|
9
|
+
# You can specify local directories as sources also.
|
10
|
+
# - type: local
|
11
|
+
# path: path/to/your/local/repository
|
12
|
+
|
13
|
+
# A directory to install the downloaded RBSs
|
14
|
+
path: .gem_rbs_collection
|
15
|
+
|
16
|
+
gems:
|
17
|
+
# stdlibs
|
18
|
+
- name: net-http
|
19
|
+
- name: json
|
20
|
+
- name: optparse
|
21
|
+
# # If you want to avoid installing rbs files for gems, you can specify them here.
|
22
|
+
# - name: GEM_NAME
|
23
|
+
# ignore: true
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'ruboty/ai_agent/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = 'ruboty-ai_agent'
|
9
|
+
spec.version = Ruboty::AiAgent::VERSION
|
10
|
+
spec.authors = ['Tomoya Chiba']
|
11
|
+
spec.email = ['tomo.asleep@gmail.com']
|
12
|
+
|
13
|
+
spec.summary = 'A Ruboty plugin that uses AI to generate responses.'
|
14
|
+
spec.description = 'A Ruboty plugin that uses AI to generate responses.'
|
15
|
+
spec.homepage = 'https://github.com/tomoasleep/ruboty-ai_agent'
|
16
|
+
|
17
|
+
spec.required_ruby_version = '>= 3.1.0'
|
18
|
+
|
19
|
+
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
20
|
+
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
21
|
+
if spec.respond_to?(:metadata)
|
22
|
+
spec.metadata['allowed_push_host'] = 'https://rubygems.org'
|
23
|
+
|
24
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
25
|
+
spec.metadata['source_code_uri'] = 'https://github.com/tomoasleep/ruboty-ai_agent'
|
26
|
+
spec.metadata['changelog_uri'] = 'https://github.com/tomoasleep/ruboty-ai_agent/blob/main/CHANGELOG.md'
|
27
|
+
else
|
28
|
+
raise 'RubyGems 2.0 or newer is required to protect against ' \
|
29
|
+
'public gem pushes.'
|
30
|
+
end
|
31
|
+
|
32
|
+
# Specify which files should be added to the gem when it is released.
|
33
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
34
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
35
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
36
|
+
end
|
37
|
+
spec.bindir = 'exe'
|
38
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
39
|
+
spec.require_paths = ['lib']
|
40
|
+
|
41
|
+
spec.add_development_dependency 'bundler', '~> 2'
|
42
|
+
spec.add_development_dependency 'rake', '~> 13'
|
43
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
44
|
+
spec.add_development_dependency 'webmock', '~> 3.0'
|
45
|
+
|
46
|
+
spec.add_dependency 'event_stream_parser', '~> 1.0'
|
47
|
+
spec.add_dependency 'openai', '~> 0.22.0'
|
48
|
+
spec.add_dependency 'ruboty', '~> 1.3'
|
49
|
+
end
|
@@ -0,0 +1,351 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# Generate RBS definitions for classes/modules that include concern modules
|
5
|
+
#
|
6
|
+
# This script detects concern modules (modules with ClassMethods or PrependMethods)
|
7
|
+
# and generates extend/prepend declarations for classes that include them.
|
8
|
+
#
|
9
|
+
# Example Ruby code:
|
10
|
+
#
|
11
|
+
# module Trackable
|
12
|
+
# def self.included(base)
|
13
|
+
# base.extend(ClassMethods)
|
14
|
+
# base.prepend(PrependMethods)
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# module ClassMethods
|
18
|
+
# def track_method(name)
|
19
|
+
# # ...
|
20
|
+
# end
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# module PrependMethods
|
24
|
+
# def initialize(*)
|
25
|
+
# super
|
26
|
+
# # tracking logic
|
27
|
+
# end
|
28
|
+
# end
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# class User
|
32
|
+
# include Trackable
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# Existing RBS:
|
36
|
+
#
|
37
|
+
# module Trackable
|
38
|
+
# module ClassMethods
|
39
|
+
# def track_method: (Symbol) -> void
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
# module PrependMethods
|
43
|
+
# def initialize: (*untyped) -> void
|
44
|
+
# end
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# class User
|
48
|
+
# include Trackable
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
# This script will generate:
|
52
|
+
#
|
53
|
+
# class User
|
54
|
+
# extend Trackable::ClassMethods
|
55
|
+
# prepend Trackable::PrependMethods
|
56
|
+
# end
|
57
|
+
#
|
58
|
+
# The script:
|
59
|
+
# 1. Loads existing RBS definitions from sig/ directory
|
60
|
+
# 2. Finds modules that have ClassMethods or PrependMethods submodules
|
61
|
+
# 3. Finds classes/modules that include these concern modules
|
62
|
+
# 4. Generates appropriate extend/prepend declarations
|
63
|
+
|
64
|
+
require 'rbs'
|
65
|
+
require 'rbs/environment'
|
66
|
+
require 'pathname'
|
67
|
+
require 'fileutils'
|
68
|
+
|
69
|
+
# Process Ruby source files and RBS definitions to generate concern RBS
|
70
|
+
class ConcernRbsGenerator
|
71
|
+
def initialize(lib_path: 'lib', sig_path: 'sig', output_path: 'sig/generated-by-scripts', namespace_filter: nil)
|
72
|
+
@lib_path = Pathname(lib_path)
|
73
|
+
@sig_path = Pathname(sig_path)
|
74
|
+
@output_path = Pathname(output_path)
|
75
|
+
@namespace_filter = namespace_filter
|
76
|
+
@env = RBS::Environment.new
|
77
|
+
end
|
78
|
+
|
79
|
+
def process
|
80
|
+
load_rbs_environment
|
81
|
+
generate_concern_rbs
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def load_rbs_environment
|
87
|
+
puts 'Loading RBS environment...'
|
88
|
+
|
89
|
+
# Load standard library RBS
|
90
|
+
loader = RBS::EnvironmentLoader.new
|
91
|
+
|
92
|
+
# Add sig subdirectories, excluding generated-by-scripts
|
93
|
+
Pathname('sig').children.select(&:directory?).each do |dir|
|
94
|
+
next if dir.basename.to_s == 'generated-by-scripts'
|
95
|
+
|
96
|
+
loader.add(path: dir)
|
97
|
+
end
|
98
|
+
|
99
|
+
loader.add(path: Pathname('.gem_rbs_collection'))
|
100
|
+
loader.load(env: @env)
|
101
|
+
|
102
|
+
puts "Loaded #{@env.class_decls.size} class declarations"
|
103
|
+
puts "Loaded #{@env.interface_decls.size} interface declarations"
|
104
|
+
end
|
105
|
+
|
106
|
+
def generate_concern_rbs
|
107
|
+
puts "\nGenerating concern RBS..."
|
108
|
+
|
109
|
+
concern_modules = find_concern_modules
|
110
|
+
includers = find_concern_includers(concern_modules)
|
111
|
+
|
112
|
+
if includers.empty?
|
113
|
+
puts 'No classes/modules found that include concern modules'
|
114
|
+
return
|
115
|
+
end
|
116
|
+
|
117
|
+
puts "\nFound #{includers.size} classes/modules that include concerns:"
|
118
|
+
includers.each do |includer|
|
119
|
+
puts " - #{includer[:name]} includes #{includer[:concern][:name]}"
|
120
|
+
end
|
121
|
+
|
122
|
+
rbs_content = generate_rbs_content(includers)
|
123
|
+
|
124
|
+
return unless rbs_content
|
125
|
+
|
126
|
+
FileUtils.mkdir_p(@output_path)
|
127
|
+
output_file = @output_path / 'concerns.rbs'
|
128
|
+
File.write(output_file, rbs_content)
|
129
|
+
puts "\nGenerated #{output_file}"
|
130
|
+
end
|
131
|
+
|
132
|
+
def find_concern_modules
|
133
|
+
concern_modules = []
|
134
|
+
|
135
|
+
@env.class_decls.each do |type_name, decl|
|
136
|
+
# Skip if not a module
|
137
|
+
next unless decl.primary.decl.is_a?(RBS::AST::Declarations::Module)
|
138
|
+
|
139
|
+
module_name = type_name.to_s
|
140
|
+
|
141
|
+
# Filter by namespace if specified
|
142
|
+
next if @namespace_filter && !module_name.start_with?("::#{@namespace_filter}")
|
143
|
+
|
144
|
+
# Check if module has a ClassMethods submodule
|
145
|
+
class_methods_name = RBS::TypeName.new(
|
146
|
+
namespace: RBS::Namespace.new(path: type_name.namespace.path + [type_name.name], absolute: type_name.namespace.absolute?),
|
147
|
+
name: :ClassMethods
|
148
|
+
)
|
149
|
+
|
150
|
+
# Check if module has a PrependMethods submodule
|
151
|
+
prepend_methods_name = RBS::TypeName.new(
|
152
|
+
namespace: RBS::Namespace.new(path: type_name.namespace.path + [type_name.name], absolute: type_name.namespace.absolute?),
|
153
|
+
name: :PrependMethods
|
154
|
+
)
|
155
|
+
|
156
|
+
has_class_methods = @env.class_decls.key?(class_methods_name)
|
157
|
+
has_prepend_methods = @env.class_decls.key?(prepend_methods_name)
|
158
|
+
|
159
|
+
next unless has_class_methods || has_prepend_methods
|
160
|
+
|
161
|
+
concern_modules << {
|
162
|
+
name: module_name,
|
163
|
+
type_name: type_name,
|
164
|
+
has_class_methods: has_class_methods,
|
165
|
+
has_prepend_methods: has_prepend_methods
|
166
|
+
}
|
167
|
+
end
|
168
|
+
|
169
|
+
concern_modules
|
170
|
+
end
|
171
|
+
|
172
|
+
def find_concern_includers(concern_modules)
|
173
|
+
includers = []
|
174
|
+
|
175
|
+
@env.class_decls.each do |type_name, decl|
|
176
|
+
# Filter by namespace if specified
|
177
|
+
next if @namespace_filter && !type_name.to_s.start_with?("::#{@namespace_filter}")
|
178
|
+
|
179
|
+
# Check all declarations (primary and others) for includes
|
180
|
+
all_decls = [decl.primary] + decl.decls
|
181
|
+
|
182
|
+
includes = []
|
183
|
+
is_class = false
|
184
|
+
|
185
|
+
all_decls.each do |d|
|
186
|
+
case d.decl
|
187
|
+
when RBS::AST::Declarations::Class
|
188
|
+
is_class = true
|
189
|
+
d.decl.members.each do |member|
|
190
|
+
case member
|
191
|
+
when RBS::AST::Members::Include
|
192
|
+
# Resolve the included module name to full TypeName
|
193
|
+
included_type_name = resolve_type_name(member.name, type_name.namespace)
|
194
|
+
includes << included_type_name if included_type_name
|
195
|
+
end
|
196
|
+
end
|
197
|
+
when RBS::AST::Declarations::Module
|
198
|
+
d.decl.members.each do |member|
|
199
|
+
case member
|
200
|
+
when RBS::AST::Members::Include
|
201
|
+
# Resolve the included module name to full TypeName
|
202
|
+
included_type_name = resolve_type_name(member.name, type_name.namespace)
|
203
|
+
includes << included_type_name if included_type_name
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
# Check if any of the includes are concern modules
|
210
|
+
includes.uniq.each do |included_name|
|
211
|
+
concern = concern_modules.find do |c|
|
212
|
+
c[:type_name] == included_name
|
213
|
+
end
|
214
|
+
|
215
|
+
next unless concern
|
216
|
+
|
217
|
+
# Get type parameters from Ruby source file
|
218
|
+
type_params = extract_type_parameters_from_source(type_name)
|
219
|
+
|
220
|
+
includers << {
|
221
|
+
name: type_name.to_s,
|
222
|
+
type_name: type_name,
|
223
|
+
is_class: is_class,
|
224
|
+
concern: concern,
|
225
|
+
type_params: type_params
|
226
|
+
}
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
includers
|
231
|
+
end
|
232
|
+
|
233
|
+
def extract_type_parameters_from_source(type_name)
|
234
|
+
# Convert type name to file path
|
235
|
+
# e.g., ::Ruboty::AiAgent::CachedValue -> lib/ruboty/ai_agent/cached_value.rb
|
236
|
+
path_parts = type_name.to_s.sub(/^::/, '').split('::').map do |part|
|
237
|
+
part.gsub(/([A-Z]+)([A-Z][a-z])|([a-z\d])([A-Z])/) do
|
238
|
+
"#{Regexp.last_match(1) || Regexp.last_match(3)}_#{Regexp.last_match(2) || Regexp.last_match(4)}"
|
239
|
+
end.downcase
|
240
|
+
end
|
241
|
+
|
242
|
+
file_path = @lib_path / "#{path_parts.join('/')}.rb"
|
243
|
+
return nil unless file_path.exist?
|
244
|
+
|
245
|
+
content = file_path.read
|
246
|
+
lines = content.lines
|
247
|
+
|
248
|
+
# Find the class/module definition line
|
249
|
+
class_or_module_name = type_name.to_s.split('::').last
|
250
|
+
|
251
|
+
lines.each_with_index do |line, index|
|
252
|
+
# Look for @rbs generic comment before class/module definition
|
253
|
+
next unless line =~ /^\s*#\s*@rbs\s+generic\s+(.+)$/
|
254
|
+
|
255
|
+
type_params_str = Regexp.last_match(1).strip
|
256
|
+
# Check if next non-comment line is the class/module definition
|
257
|
+
next_line_index = index + 1
|
258
|
+
while next_line_index < lines.length
|
259
|
+
next_line = lines[next_line_index]
|
260
|
+
# Skip comment lines and empty lines
|
261
|
+
break unless next_line =~ /^\s*(#|$)/
|
262
|
+
|
263
|
+
next_line_index += 1
|
264
|
+
end
|
265
|
+
|
266
|
+
next unless next_line_index < lines.length
|
267
|
+
|
268
|
+
next_line = lines[next_line_index]
|
269
|
+
if next_line =~ /^\s*(class|module)\s+#{Regexp.escape(class_or_module_name)}\b/
|
270
|
+
# Parse type parameters (e.g., "D" or "K, V")
|
271
|
+
return type_params_str.split(',').map(&:strip)
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
nil
|
276
|
+
end
|
277
|
+
|
278
|
+
def resolve_type_name(name, current_namespace)
|
279
|
+
# If the name is already absolute, use it as is
|
280
|
+
return name if name.absolute?
|
281
|
+
|
282
|
+
# Otherwise, resolve relative to current namespace
|
283
|
+
RBS::TypeName.new(
|
284
|
+
namespace: current_namespace,
|
285
|
+
name: name.name
|
286
|
+
)
|
287
|
+
end
|
288
|
+
|
289
|
+
def generate_rbs_content(includers)
|
290
|
+
content = []
|
291
|
+
content << '# Generated RBS definitions for concern modules'
|
292
|
+
content << '# This file is automatically generated by script/generate-concern-rbs.rb'
|
293
|
+
content << ''
|
294
|
+
|
295
|
+
# Group by namespace for cleaner output
|
296
|
+
grouped = includers.group_by do |includer|
|
297
|
+
parts = includer[:name].sub(/^::/, '').split('::')
|
298
|
+
parts[0...-1]
|
299
|
+
end
|
300
|
+
|
301
|
+
grouped.each do |namespace_parts, group_includers|
|
302
|
+
indent_level = 0
|
303
|
+
|
304
|
+
# Open namespaces
|
305
|
+
namespace_parts.each do |part|
|
306
|
+
content << (' ' * indent_level) + "module #{part}"
|
307
|
+
indent_level += 1
|
308
|
+
end
|
309
|
+
|
310
|
+
# Add extend/prepend for each includer in this namespace
|
311
|
+
group_includers.each do |includer|
|
312
|
+
simple_name = includer[:name].sub(/^::/, '').split('::').last
|
313
|
+
keyword = includer[:is_class] ? 'class' : 'module'
|
314
|
+
|
315
|
+
# Add type parameters if present
|
316
|
+
if includer[:type_params] && !includer[:type_params].empty?
|
317
|
+
type_params_str = "[#{includer[:type_params].join(', ')}]"
|
318
|
+
content << (' ' * indent_level) + "#{keyword} #{simple_name}#{type_params_str}"
|
319
|
+
else
|
320
|
+
content << (' ' * indent_level) + "#{keyword} #{simple_name}"
|
321
|
+
end
|
322
|
+
|
323
|
+
concern = includer[:concern]
|
324
|
+
concern_simple_name = concern[:name].sub(/^::/, '').split('::').last
|
325
|
+
|
326
|
+
content << (' ' * (indent_level + 1)) + "extend #{concern_simple_name}::ClassMethods" if concern[:has_class_methods]
|
327
|
+
|
328
|
+
content << (' ' * (indent_level + 1)) + "prepend #{concern_simple_name}::PrependMethods" if concern[:has_prepend_methods]
|
329
|
+
|
330
|
+
content << "#{' ' * indent_level}end"
|
331
|
+
content << ''
|
332
|
+
end
|
333
|
+
|
334
|
+
# Close namespaces
|
335
|
+
namespace_parts.size.times do
|
336
|
+
indent_level -= 1
|
337
|
+
content << "#{' ' * indent_level}end"
|
338
|
+
end
|
339
|
+
|
340
|
+
content << ''
|
341
|
+
end
|
342
|
+
|
343
|
+
content.join("\n").strip
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
# Run if executed directly
|
348
|
+
if __FILE__ == $PROGRAM_NAME
|
349
|
+
generator = ConcernRbsGenerator.new(namespace_filter: 'Ruboty')
|
350
|
+
generator.process
|
351
|
+
end
|