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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1782e855675403948a771dd5abe6fdba6a754cfc6e15bebad40f24bf3c7569f6
4
+ data.tar.gz: 2142af0fb45049c4e1b2397def8009248db8860e43a9fa2ae21f938a31e3af82
5
+ SHA512:
6
+ metadata.gz: 1a1b7fc8e8e41150055e87d2227e9b0ae8570c69482cf72249af38abef0978058419eed73f778a0f4d9c00343a55ce96be0cd62febdb4a20cd8b7976a6852415
7
+ data.tar.gz: 73aff06e4a6d7fb3601b9c95393b9c1496fd61eddaaabf0fe742e7701faad638a36c3919462e65847dab47d0450579c8047ba492beb4d51c81181b5b66307aef
data/.gitignore ADDED
@@ -0,0 +1,19 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ .env
11
+
12
+ # rspec failure tracking
13
+ .rspec_status
14
+
15
+ Gemfile.lock
16
+
17
+ # rbs collection
18
+ .gem_rbs_collection/
19
+ rbs_collection.lock.yaml
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,45 @@
1
+ # The behavior of RuboCop can be controlled via the .rubocop.yml
2
+ # configuration file. It makes it possible to enable/disable
3
+ # certain cops (checks) and to alter their behavior if they accept
4
+ # any parameters. The file can be placed either in your home
5
+ # directory or in some project directory.
6
+ #
7
+ # RuboCop will start looking for the configuration file in the directory
8
+ # where the inspected file is and continue its way up to the root directory.
9
+ #
10
+ # See https://docs.rubocop.org/rubocop/configuration
11
+ plugins:
12
+ - rubocop-rake
13
+ - rubocop-rbs_inline
14
+
15
+ AllCops:
16
+ TargetRubyVersion: 3.1
17
+
18
+ Naming/AccessorMethodName:
19
+ Exclude:
20
+ - lib/ruboty/handlers/ai_agent.rb # we name handler methods from its word pattern
21
+
22
+ Naming/FileName:
23
+ Exclude:
24
+ - Steepfile
25
+
26
+ Layout/LineLength:
27
+ Enabled: false
28
+
29
+ Layout/LeadingCommentSpace:
30
+ AllowRBSInlineAnnotation: true
31
+ AllowSteepAnnotation: true
32
+
33
+ Metrics:
34
+ Enabled: false
35
+
36
+ Style/CommentedKeyword:
37
+ Enabled: false # because of RBS annotations
38
+
39
+ Style/Documentation:
40
+ AllowedConstants:
41
+ - ClassMethods
42
+ - PrependFeatures
43
+
44
+ Style/RedundantBegin:
45
+ Enabled: false # because of Steep annotations
data/AGENTS.md ADDED
@@ -0,0 +1,22 @@
1
+ # Development Guide for AI
2
+
3
+ ## Dev instructions
4
+ - Use English for codes, commit messages and PR descriptions.
5
+
6
+ ## Dev environment tips
7
+ - Our RBS types in `sig/` are generated by rbs-inline and our scripts in `script/`.
8
+ - To update them, run `rake rbs`.
9
+
10
+ ## Testing instructions
11
+
12
+ - In this repository, we use the following tools:
13
+ - RSpec (for testing)
14
+ - RuboCop (for linting)
15
+ - Steep (for type checking)
16
+ - To run these all tools, use `bundle exec rake`.
17
+ - When you add or modify code, please follow TDD workflow.
18
+ - Fix any test or type errors until the whole suite is green.
19
+
20
+ ## PR instructions
21
+
22
+ - Always run `rake autocorrect` before committing.
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
data/CLAUDE.md ADDED
@@ -0,0 +1 @@
1
+ AGENTS.md
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at tomo.asleep@gmail.com. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6
+
7
+ # Specify your gem's dependencies in ruboty-ai_agent.gemspec
8
+ gemspec
9
+
10
+ gem 'rbs-inline', require: false
11
+ gem 'rubocop'
12
+ gem 'rubocop-rake'
13
+ gem 'rubocop-rbs_inline'
14
+ gem 'steep', require: false
data/README.md ADDED
@@ -0,0 +1,118 @@
1
+ # Ruboty::AiAgent
2
+
3
+ A Ruboty handler that uses LLMs (e.g., OpenAI) and MCP (Model Context Protocol) tools to generate AI-assisted replies. When an incoming message does not match any other handler, this plugin responds via an LLM and can call MCP tools when available.
4
+
5
+ ## Installation
6
+
7
+ Add to your Gemfile:
8
+
9
+ ```ruby
10
+ gem 'ruboty'
11
+ gem 'ruboty-ai_agent'
12
+ ```
13
+
14
+ Then install:
15
+
16
+ ```
17
+ bundle install
18
+ ```
19
+
20
+ ## Setup
21
+
22
+ ### OpenAI
23
+
24
+ Configure environment variables (e.g., via `.env`):
25
+
26
+ - `OPENAI_API_KEY`: Your LLM provider API key (required)
27
+ - `OPENAI_MODEL`: Model name (optional)
28
+
29
+
30
+
31
+ ## Usage
32
+
33
+ - Any message that does not match other handlers is sent to the AI and replied to.
34
+ - Conversations are tracked per thread, and tools exposed via MCP servers can be invoked through function calls when the model requests them.
35
+
36
+ ## Commands
37
+
38
+ Conversation management:
39
+ - `/clear` — Clear the current thread’s conversation history
40
+ - `/compact` — Summarize and compact the conversation history
41
+
42
+ System prompt (work in progress):
43
+ - `set system prompt "<PROMPT>"` — Set the system prompt (WIP)
44
+ - `show system prompt` — Show the current system prompt (WIP)
45
+
46
+ AI memory (lightweight profile):
47
+ - `add ai memory "<TEXT>"` — Add a memory entry
48
+ - `remove ai memory <INDEX>` — Remove a memory by index
49
+ - `list ai memories` — List all memory entries
50
+
51
+ AI custom commands (work in progress):
52
+ - `add ai command /<NAME> "<PROMPT>"` — Add a custom AI command (WIP)
53
+ - `remove ai command /<NAME>` — Remove a custom AI command (WIP)
54
+ - `list ai commands` — List available built-in commands
55
+
56
+ MCP (Model Context Protocol):
57
+ - `add mcp <NAME> <OPTIONS> <URL>` — Add an MCP server
58
+ - Example (HTTP transport with auth header):
59
+ - `add mcp search --transport http --header 'Authorization: Bearer xxx' https://example.com/mcp`
60
+ - Options:
61
+ - `--transport http|sse` (currently only `http` implemented; `sse` is not yet implemented)
62
+ - `--header 'Key: Value'` (repeatable)
63
+ - `remove mcp <NAME>` — Remove an MCP server
64
+ - `list mcp` / `list mcps` — List configured MCP servers
65
+
66
+ ## How It Works
67
+
68
+ - Unmatched messages are handled by `Ruboty::Handlers::AiAgent#chat` and sent to the configured LLM.
69
+ - If MCP servers are configured, their tool definitions are exposed to the model; when the model requests a function call, the corresponding MCP tool is invoked and its response is fed back into the conversation.
70
+ - During tool calls, the bot streams short status updates and returns a brief summary of the tool’s response.
71
+ - Conversation data is stored in Ruboty’s brain under the `:ai_agent` namespace.
72
+
73
+
74
+ ## Development
75
+
76
+ Install dependencies:
77
+
78
+ ```
79
+ bin/setup
80
+ ```
81
+
82
+ Local run with dotenv (for development):
83
+
84
+ ```
85
+ bundle exec ruboty --dotenv
86
+
87
+ # or using the included launcher
88
+ bin/ruboty --dotenv
89
+ ```
90
+
91
+ Run tests:
92
+
93
+ ```
94
+ rake spec
95
+ ```
96
+
97
+ Interactive console:
98
+
99
+ ```
100
+ bin/console
101
+ ```
102
+
103
+ Install locally:
104
+
105
+ ```
106
+ bundle exec rake install
107
+ ```
108
+
109
+ Release:
110
+ Update `version.rb` and run `bundle exec rake release` to tag, push, and publish to RubyGems.
111
+
112
+ ## Contributing
113
+
114
+ Bug reports and pull requests are welcome on GitHub at https://github.com/tomoasleep/ruboty-ai_agent.
115
+
116
+ ## Code of Conduct
117
+
118
+ Everyone interacting in the Ruboty::AiAgent project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/tomoasleep/ruboty-ai_agent/blob/main/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+ require 'rubocop/rake_task'
6
+ require 'steep/rake_task'
7
+
8
+ RSpec::Core::RakeTask.new(:spec)
9
+
10
+ RuboCop::RakeTask.new do |task|
11
+ task.plugins << 'rubocop-rake'
12
+ end
13
+
14
+ Steep::RakeTask.new do |t|
15
+ t.check.severity_level = :error
16
+ t.watch.verbose
17
+ end
18
+
19
+ task default: %i[rubocop steep spec]
20
+ task autocorrect: %i[rubocop:autocorrect rbs steep spec]
21
+
22
+ namespace :rbs do
23
+ desc 'Clean generated RBS files'
24
+ task :clean do
25
+ sh('rm -rf sig/generate')
26
+ sh('rm -rf sig/generated-by-scripts')
27
+ end
28
+
29
+ desc 'Install rbs collection'
30
+ task :collection do
31
+ sh('bundle exec rbs collection install')
32
+ end
33
+
34
+ desc 'Run rbs-inline to generate RBS files'
35
+ task :inline do
36
+ sh('bundle exec rbs-inline --opt-out --output lib')
37
+ end
38
+
39
+ desc 'Generate RBS definitions by script/generate-rbs.rb'
40
+ task :script do
41
+ sh('script/generate-data-rbs.rb')
42
+ sh('script/generate-concern-rbs.rb')
43
+ sh('script/generate-memorized-ivar-rbs.rb')
44
+ end
45
+ end
46
+
47
+ task rbs: %i[rbs:inline rbs:script]
data/Steepfile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ target :lib do
4
+ check 'lib'
5
+
6
+ signature 'sig'
7
+ signature 'sig-lib'
8
+
9
+ library 'json'
10
+ library 'net-http'
11
+ library 'optparse'
12
+ end
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'ruboty/ai_agent'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require 'irb'
15
+ IRB.start(__FILE__)
data/bin/ruboty ADDED
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Run Ruboty with this gem for testing.
5
+ #
6
+ # Usage:
7
+ # bin/ruboty [options]
8
+ #
9
+ # Example:
10
+ # bin/ruboty --dotenv
11
+
12
+ require 'bundler/inline'
13
+
14
+ gemfile do
15
+ source 'https://rubygems.org'
16
+
17
+ gem 'ruboty'
18
+ gem 'ruboty-ai_agent', path: '../'
19
+
20
+ # You can add other ruboty plugins here for testing.
21
+ # gem 'ruboty-echo'
22
+ # gem 'ruboty-alias'
23
+ # gem 'ruboty-slack_events'
24
+ end
25
+
26
+ module Ruboty
27
+ class Robot # rubocop:disable Style/Documentation
28
+ def bundle
29
+ # noop
30
+ end
31
+ end
32
+ end
33
+
34
+ Ruboty::CommandBuilder.new(ARGV).build.call
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruboty
4
+ module AiAgent
5
+ module Actions
6
+ # AddAiCommand action for Ruboty::AiAgent
7
+ class AddAiCommand < Base
8
+ def call
9
+ message.reply("TODO: Implement AddAiCommand action - name: #{name_param}, prompt: #{prompt_param}")
10
+ end
11
+
12
+ def name_param #: String
13
+ message[:name]
14
+ end
15
+
16
+ def prompt_param #: String
17
+ message[:prompt]
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruboty
4
+ module AiAgent
5
+ module Actions
6
+ # AddAiMemory action for Ruboty::AiAgent
7
+ class AddAiMemory < Base
8
+ def call
9
+ idx = user.ai_memories.add(prompt_param)
10
+
11
+ message.reply("Added memory #{idx}: #{prompt_param}")
12
+ end
13
+
14
+ def prompt_param #: String
15
+ message[:prompt]
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'optparse'
4
+
5
+ module Ruboty
6
+ module AiAgent
7
+ module Actions
8
+ # AddMcp action for Ruboty::AiAgent
9
+ class AddMcp < Base
10
+ def call
11
+ options = parse_config
12
+
13
+ case options[:transport]
14
+ when :http
15
+ url = options[:args].first #: String?
16
+ if url
17
+ new_mcp_configuration = McpConfiguration.new(
18
+ transport: :http,
19
+ name: name_param,
20
+ headers: options[:headers],
21
+ url: url
22
+ )
23
+ user.mcp_configurations.add(
24
+ new_mcp_configuration
25
+ )
26
+
27
+ message.reply("Added MCP configuration #{name_param}: #{new_mcp_configuration.to_h.except(:record_type).to_json}")
28
+ else
29
+ message.reply('Error: URL is required for HTTP transport. Please specify the URL as an argument.')
30
+ nil
31
+ end
32
+
33
+ when :sse
34
+ message.reply('Error: SSE transport is not yet implemented.')
35
+ else # steep:ignore UnreachableValueBranch
36
+ message.reply('Error: Invalid or missing transport type. Please specify --transport http or --transport sse.')
37
+ end
38
+ rescue OptionParser::InvalidOption, OptionParser::InvalidArgument => e
39
+ message.reply("Error parsing options: #{e.message}")
40
+ end
41
+
42
+ def name_param #: String
43
+ message[:name]
44
+ end
45
+
46
+ def config_param #: String
47
+ message[:config]
48
+ end
49
+
50
+ private
51
+
52
+ # @rbs! type config = { transport: :http | :sse, headers: Hash[String, String], args: Array[String] }
53
+
54
+ def parse_config #: config
55
+ options = {
56
+ transport: :http,
57
+ headers: {},
58
+ args: []
59
+ } #: config
60
+
61
+ args = config_param.split(/\s+(?=-)/)
62
+
63
+ parser = OptionParser.new do |opts|
64
+ opts.on('--transport TYPE', %w[http sse], 'Transport type (http or sse)') do |t|
65
+ options[:transport] = t
66
+ end
67
+
68
+ opts.on('--header VALUE', 'Add a header (can be specified multiple times)') do |h|
69
+ key, value = undump_string(h).split(':', 2).map!(&:strip)
70
+ unless key && value
71
+ message.reply("Warning: Invalid format for --header '#{h}'. Expected format is 'Key: Value'.")
72
+ next
73
+ end
74
+
75
+ options[:headers][key] = value
76
+ end
77
+ end
78
+
79
+ options[:args] = parser.parse(args)
80
+
81
+ options
82
+ end
83
+
84
+ # @rbs str: String
85
+ # @rbs return: String
86
+ def undump_string(str)
87
+ str.undump
88
+ rescue StandardError
89
+ str
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruboty
4
+ module AiAgent
5
+ module Actions
6
+ # Base class for actions.
7
+ # @abstract
8
+ class Base
9
+ attr_reader :message #: Ruboty::Message
10
+
11
+ # @rbs message: Ruboty::Message
12
+ # @rbs return: void
13
+ def self.call(message)
14
+ new(message).call
15
+ end
16
+
17
+ # @rbs return: void
18
+ def call
19
+ raise NotImplementedError
20
+ end
21
+
22
+ def initialize(message)
23
+ @message = message
24
+ end
25
+
26
+ # @rbs %a{memorized}
27
+ def database #: Ruboty::AiAgent::Database
28
+ @database ||= Ruboty::AiAgent::Database.new(message.robot.brain)
29
+ end
30
+
31
+ # @rbs %a{memorized}
32
+ def user #: Ruboty::AiAgent::User
33
+ @user ||= database.user(message.from_name)
34
+ end
35
+
36
+ # @rbs %a{memorized}
37
+ def chat_thread #: Ruboty::AiAgent::ChatThread
38
+ @chat_thread ||= database.chat_thread(message.from || 'default')
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruboty
4
+ module AiAgent
5
+ module Actions
6
+ # Chat action for Ruboty::AiAgent
7
+ class Chat < Base
8
+ CONVERSATION_KEY = :conversations
9
+
10
+ # @rbs override
11
+ def call
12
+ llm = LLM::OpenAI.new(
13
+ client: OpenAI::Client.new(
14
+ api_key: ENV.fetch('OPENAI_API_KEY', nil)
15
+ ),
16
+ model: ENV.fetch('OPENAI_MODEL', 'gpt-5-nano')
17
+ )
18
+
19
+ commands = Commands.builtins(message:, chat_thread:)
20
+ tools = McpClients.new(user.mcp_clients).available_tools
21
+
22
+ commands.each do |command|
23
+ return command.call if command.match?(body_param)
24
+ end
25
+
26
+ chat_thread.messages << ChatMessage.new(
27
+ role: :user,
28
+ content: body_param
29
+ )
30
+
31
+ agent = Agent.new(
32
+ llm:,
33
+ messages: chat_thread.messages.all_values,
34
+ tools:
35
+ )
36
+
37
+ agent.complete do |event|
38
+ case event[:type]
39
+ when :new_message
40
+ chat_thread.messages << event[:message]
41
+ message.reply(event[:message].content) if event[:message].content.length.positive?
42
+ when :tool_call
43
+ message.reply("Calling tool #{event[:tool].name} with arguments #{event[:tool_arguments]}",
44
+ streaming: true)
45
+ when :tool_response
46
+ chat_thread.messages << event[:message]
47
+ message.reply("Tool response: #{event[:tool_response].slice(0..100)}")
48
+ end
49
+ end
50
+ rescue StandardError => e
51
+ if ENV['DEBUG']
52
+ message.reply("エラーが発生しました: #{e.full_message}")
53
+ else
54
+ message.reply("エラーが発生しました: #{e.message}")
55
+ end
56
+ end
57
+
58
+ def body_param #: String
59
+ message[:body]
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end