soka-rails 0.0.1.beta4

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.
data/SPEC.md ADDED
@@ -0,0 +1,420 @@
1
+ # Soka Rails - Rails Integration for Soka AI Agent Framework
2
+
3
+ ## Overview
4
+
5
+ Soka Rails is the Rails integration package for the Soka AI Agent Framework, providing seamless integration with the Rails ecosystem, following Rails conventions, allowing developers to easily use AI Agent in Rails applications.
6
+
7
+ ## Core Features
8
+
9
+ * **Native Rails Integration**: Following Rails conventions and best practices
10
+ * **Autoloading Support**: Automatically loads app/soka directory
11
+ * **Generator Support**: Quickly generate Agent and Tool templates
12
+ * **Rails Configuration Integration**: Uses Rails configuration system
13
+ * **Rails Testing Integration**: Seamless RSpec integration
14
+
15
+ ## Directory Structure
16
+
17
+ ```
18
+ rails-app/
19
+ ├── app/
20
+ │ └── soka/
21
+ │ ├── agents/ # Agent definitions
22
+ │ │ ├── application_agent.rb # Base Agent class
23
+ │ │ ├── weather_agent.rb
24
+ │ │ └── support_agent.rb
25
+ │ └── tools/ # Tool definitions
26
+ │ ├── application_tool.rb # Base Tool class
27
+ │ └── rails_info_tool.rb # Rails environment info tool
28
+ └── config/
29
+ └── initializers/
30
+ └── soka.rb # Soka global configuration
31
+ ```
32
+
33
+ ## Installation and Setup
34
+
35
+ ### 1. Install Gem
36
+
37
+ ```ruby
38
+ # Gemfile
39
+ gem 'soka-rails'
40
+ ```
41
+
42
+ ### 2. Run Installation Generator
43
+
44
+ ```bash
45
+ rails generate soka:install
46
+ ```
47
+
48
+ This will generate:
49
+ - `config/initializers/soka.rb` - Main configuration file
50
+ - `app/soka/agents/application_agent.rb` - Base Agent class
51
+ - `app/soka/tools/application_tool.rb` - Base Tool class
52
+
53
+ ## Configuration System
54
+
55
+ ### Main Configuration File (config/initializers/soka.rb)
56
+
57
+ ```ruby
58
+ # config/initializers/soka.rb
59
+ Soka::Rails.configure do |config|
60
+ # Use environment variables to manage API keys
61
+ config.ai do |ai|
62
+ ai.provider = ENV.fetch('SOKA_PROVIDER', :gemini)
63
+ ai.model = ENV.fetch('SOKA_MODEL', 'gemini-2.5-flash-lite')
64
+ ai.api_key = ENV['SOKA_API_KEY']
65
+ end
66
+
67
+ # Performance configuration
68
+ config.performance do |perf|
69
+ perf.max_iterations = Rails.env.production? ? 10 : 5
70
+ perf.timeout = 30.seconds
71
+ end
72
+ end
73
+ ```
74
+
75
+ ## Agent System
76
+
77
+ ### ApplicationAgent Base Class
78
+
79
+ ```ruby
80
+ # app/soka/agents/application_agent.rb
81
+ class ApplicationAgent < Soka::Agent
82
+ # Default configuration for Rails environment
83
+ if Rails.env.development?
84
+ max_iterations 5
85
+ timeout 15.seconds
86
+ end
87
+
88
+ # Auto-register Rails related tools
89
+ tool RailsInfoTool
90
+ tool ApplicationTool
91
+
92
+ # Rails integration lifecycle hooks
93
+ before_action :log_to_rails
94
+ on_error :notify_error_tracking
95
+
96
+ private
97
+
98
+ def log_to_rails(input)
99
+ Rails.logger.info "[Soka] Starting agent execution: #{input}"
100
+ end
101
+
102
+ def notify_error_tracking(error, context)
103
+ # Integrate Rails error tracking services
104
+ Rails.error.report(error, context: { agent: self.class.name, input: context })
105
+ :continue
106
+ end
107
+ end
108
+ ```
109
+
110
+ ### Custom Agent Example
111
+
112
+ ```ruby
113
+ # app/soka/agents/customer_support_agent.rb
114
+ class CustomerSupportAgent < ApplicationAgent
115
+ # Register business-related tools
116
+ tool OrderLookupTool
117
+ tool UserInfoTool
118
+ tool RefundTool, if: -> { Current.user&.admin? }
119
+
120
+ # Integrate Rails authentication
121
+ before_action :authenticate_user!
122
+
123
+ private
124
+
125
+ def authenticate_user!
126
+ raise UnauthorizedError unless Current.user.present?
127
+ end
128
+ end
129
+ ```
130
+
131
+ ## Tool System
132
+
133
+ ### Rails-specific Tools
134
+
135
+ ```ruby
136
+ # app/soka/tools/rails_info_tool.rb
137
+ class RailsInfoTool < ApplicationTool
138
+ desc "Get Rails application information"
139
+
140
+ params do
141
+ requires :info_type, String, desc: "Type of information",
142
+ validates: { inclusion: { in: %w[routes version environment config] } }
143
+ end
144
+
145
+ def call(info_type:)
146
+ case info_type
147
+ when 'routes'
148
+ Rails.application.routes.routes.map { |r| format_route(r) }.compact
149
+ when 'version'
150
+ { rails: Rails.version, ruby: RUBY_VERSION, app: Rails.application.class.name }
151
+ when 'environment'
152
+ { env: Rails.env, host: Rails.application.config.hosts.first }
153
+ when 'config'
154
+ safe_config_values
155
+ end
156
+ end
157
+
158
+ private
159
+
160
+ def format_route(route)
161
+ return unless route.name
162
+
163
+ {
164
+ name: route.name,
165
+ verb: route.verb,
166
+ path: route.path.spec.to_s,
167
+ controller: route.defaults[:controller],
168
+ action: route.defaults[:action]
169
+ }
170
+ end
171
+
172
+ def safe_config_values
173
+ # Only return safe configuration values
174
+ {
175
+ time_zone: Rails.application.config.time_zone,
176
+ locale: I18n.locale
177
+ }
178
+ end
179
+ end
180
+ ```
181
+
182
+ ### Custom Tool Example
183
+
184
+ ```ruby
185
+ # app/soka/tools/order_lookup_tool.rb
186
+ class OrderLookupTool < ApplicationTool
187
+ desc "Look up order information"
188
+
189
+ params do
190
+ requires :order_id, String, desc: "Order ID"
191
+ optional :include_items, :boolean, desc: "Include order items", default: false
192
+ end
193
+
194
+ def call(order_id:, include_items: false)
195
+ # Mock order lookup logic
196
+ order = {
197
+ id: order_id,
198
+ status: "delivered",
199
+ total: "$99.99",
200
+ date: "2025-01-15"
201
+ }
202
+
203
+ if include_items
204
+ order[:items] = [
205
+ { name: "Product A", quantity: 2, price: "$49.99" }
206
+ ]
207
+ end
208
+
209
+ order
210
+ rescue => e
211
+ { error: e.message }
212
+ end
213
+ end
214
+ ```
215
+
216
+ ## Generator Support
217
+
218
+ ### Installation Generator
219
+
220
+ ```bash
221
+ rails generate soka:install
222
+ ```
223
+
224
+ Generated content:
225
+ - Configuration file (`config/initializers/soka.rb`)
226
+ - Base classes (`ApplicationAgent`, `ApplicationTool`)
227
+ - Example Agent and Tool
228
+
229
+ ### Agent Generator
230
+
231
+ ```bash
232
+ rails generate soka:agent weather
233
+ ```
234
+
235
+ Generates `app/soka/agents/weather_agent.rb`:
236
+
237
+ ```ruby
238
+ class WeatherAgent < ApplicationAgent
239
+ # Tool registration
240
+ # tool YourTool
241
+
242
+ # Configuration
243
+ # max_iterations 10
244
+ # timeout 30.seconds
245
+
246
+ # Hooks
247
+ # before_action :your_method
248
+ # after_action :your_method
249
+ # on_error :your_method
250
+
251
+ private
252
+
253
+ # Implement your private methods
254
+ end
255
+ ```
256
+
257
+ ### Tool Generator
258
+
259
+ ```bash
260
+ rails generate soka:tool weather_api
261
+ ```
262
+
263
+ Generates `app/soka/tools/weather_api_tool.rb`:
264
+
265
+ ```ruby
266
+ class WeatherApiTool < ApplicationTool
267
+ desc "Description of your tool"
268
+
269
+ params do
270
+ # requires :param_name, String, desc: "Parameter description"
271
+ # optional :param_name, String, desc: "Parameter description"
272
+ end
273
+
274
+ def call(**params)
275
+ # Implement your tool logic
276
+ "Tool result"
277
+ end
278
+ end
279
+ ```
280
+
281
+ ## Usage Examples
282
+
283
+ ### Basic Usage
284
+
285
+ ```ruby
286
+ # Using in Controller
287
+ class ConversationsController < ApplicationController
288
+ def create
289
+ agent = CustomerSupportAgent.new
290
+ result = agent.run(params[:message])
291
+
292
+ render json: {
293
+ answer: result.final_answer,
294
+ confidence: result.confidence_score,
295
+ status: result.status
296
+ }
297
+ end
298
+ end
299
+ ```
300
+
301
+ ### Using Memory
302
+
303
+ ```ruby
304
+ class ConversationsController < ApplicationController
305
+ def create
306
+ # Load memory from session
307
+ memory = session[:conversation_memory] || []
308
+
309
+ agent = CustomerSupportAgent.new(memory: memory)
310
+ result = agent.run(params[:message])
311
+
312
+ # Update memory
313
+ session[:conversation_memory] = agent.memory.to_a
314
+
315
+ render json: { answer: result.final_answer }
316
+ end
317
+ end
318
+ ```
319
+
320
+ ### Event Handling
321
+
322
+ ```ruby
323
+ class ConversationsController < ApplicationController
324
+ include ActionController::Live
325
+
326
+ def stream
327
+ response.headers['Content-Type'] = 'text/event-stream'
328
+ agent = CustomerSupportAgent.new
329
+
330
+ agent.run(params[:message]) do |event|
331
+ response.stream.write("event: #{event.type}\n")
332
+ response.stream.write("data: #{event.content.to_json}\n\n")
333
+ end
334
+ ensure
335
+ response.stream.close
336
+ end
337
+ end
338
+ ```
339
+
340
+ ## Testing Support
341
+
342
+ ### RSpec Integration
343
+
344
+ ```ruby
345
+ # spec/rails_helper.rb
346
+ require 'soka/rails/rspec'
347
+
348
+ RSpec.configure do |config|
349
+ config.include Soka::Rails::TestHelpers, type: :agent
350
+ config.include Soka::Rails::TestHelpers, type: :tool
351
+ end
352
+
353
+ # spec/soka/agents/weather_agent_spec.rb
354
+ require 'rails_helper'
355
+
356
+ RSpec.describe WeatherAgent, type: :agent do
357
+ let(:agent) { described_class.new }
358
+
359
+ before do
360
+ # Mock AI response
361
+ mock_ai_response(
362
+ final_answer: "Today in Taipei is sunny, temperature 28°C"
363
+ )
364
+ end
365
+
366
+ it "responds to weather queries" do
367
+ result = agent.run("What's the weather in Taipei today?")
368
+
369
+ expect(result).to be_successful
370
+ expect(result.final_answer).to include("28°C")
371
+ end
372
+
373
+ it "handles multiple queries" do
374
+ # First query
375
+ result1 = agent.run("What's the weather today?")
376
+ expect(result1).to be_successful
377
+
378
+ # Second query
379
+ result2 = agent.run("What's the weather tomorrow?")
380
+ expect(result2).to be_successful
381
+ end
382
+ end
383
+
384
+ # spec/soka/tools/rails_info_tool_spec.rb
385
+ RSpec.describe RailsInfoTool, type: :tool do
386
+ let(:tool) { described_class.new }
387
+
388
+ it "returns Rails version info" do
389
+ result = tool.call(info_type: "version")
390
+
391
+ expect(result).to include(:rails, :ruby, :app)
392
+ expect(result[:rails]).to eq(Rails.version)
393
+ end
394
+
395
+ it "returns safe config values only" do
396
+ result = tool.call(info_type: "config")
397
+
398
+ expect(result).to include(:time_zone, :locale)
399
+ expect(result).not_to include(:secret_key_base)
400
+ end
401
+ end
402
+ ```
403
+
404
+ ## Version Compatibility
405
+
406
+ - Ruby: >= 3.0
407
+ - Rails: >= 7.0
408
+ - Soka: >= 1.0
409
+
410
+ ## Contributing
411
+
412
+ 1. Fork the project
413
+ 2. Create a feature branch (`git checkout -b feature/amazing-feature`)
414
+ 3. Commit your changes (`git commit -m 'Add amazing feature'`)
415
+ 4. Push to the branch (`git push origin feature/amazing-feature`)
416
+ 5. Open a Pull Request
417
+
418
+ ## License
419
+
420
+ MIT License
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Base class for all Soka agents in the application
4
+ class ApplicationAgent < Soka::Agent
5
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Base class for all Soka tools in the application
4
+ class ApplicationTool < Soka::AgentTool
5
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators/named_base'
4
+
5
+ module Soka
6
+ module Generators
7
+ # Generator for creating new Soka agent classes with their associated tests
8
+ class AgentGenerator < ::Rails::Generators::NamedBase
9
+ source_root File.expand_path('templates', __dir__)
10
+
11
+ argument :tools, type: :array, default: [], banner: 'tool1 tool2'
12
+
13
+ def create_agent_file
14
+ @agent_class_name = agent_class_name
15
+ @tools_list = tools
16
+
17
+ template 'agent.rb.tt',
18
+ File.join('app/soka/agents', class_path, "#{agent_file_name}.rb")
19
+ end
20
+
21
+ def create_test_file
22
+ return unless rspec_installed?
23
+
24
+ @agent_class_name = agent_class_name
25
+
26
+ template 'agent_spec.rb.tt',
27
+ File.join('spec/soka/agents', class_path, "#{agent_file_name}_spec.rb")
28
+ end
29
+
30
+ private
31
+
32
+ # Normalize the agent file name to always end with _agent
33
+ def agent_file_name
34
+ base_name = file_name.to_s
35
+
36
+ # Remove existing _agent suffix if present to avoid duplication
37
+ base_name = base_name.sub(/_agent\z/, '')
38
+
39
+ # Add _agent suffix
40
+ "#{base_name}_agent"
41
+ end
42
+
43
+ # Normalize the agent class name to always end with Agent
44
+ def agent_class_name
45
+ base_class = class_name.to_s
46
+
47
+ # Remove existing Agent suffix if present to avoid duplication
48
+ base_class = base_class.sub(/Agent\z/, '')
49
+
50
+ # Add Agent suffix
51
+ "#{base_class}Agent"
52
+ end
53
+
54
+ def rspec_installed?
55
+ File.exist?(::Rails.root.join('spec/spec_helper.rb')) ||
56
+ File.exist?(::Rails.root.join('spec/rails_helper.rb'))
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= @agent_class_name %> < ApplicationAgent
4
+ # Tool registration
5
+ <% if @tools_list.any? -%>
6
+ <% @tools_list.each do |tool| -%>
7
+ tool <%= tool.camelize %>Tool
8
+ <% end -%>
9
+ <% else -%>
10
+ # tool YourTool
11
+ <% end -%>
12
+
13
+ # Configuration
14
+ # max_iterations 10
15
+ # timeout 30
16
+
17
+ # Hooks
18
+ # before_action :your_method
19
+ # after_action :your_method
20
+ # on_error :your_method
21
+
22
+ private
23
+
24
+ # Implement your private methods here
25
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails_helper'
4
+
5
+ RSpec.describe <%= @agent_class_name %>Agent, type: :agent do
6
+ let(:agent) { described_class.new }
7
+
8
+ describe '#run' do
9
+ before do
10
+ # Mock AI response
11
+ mock_ai_response(
12
+ final_answer: 'Test response'
13
+ )
14
+ end
15
+
16
+ it 'responds to queries' do
17
+ result = agent.run('Test query')
18
+
19
+ expect(result).to be_successful
20
+ expect(result.final_answer).to eq('Test response')
21
+ end
22
+
23
+ it 'handles errors gracefully' do
24
+ allow(agent).to receive(:execute).and_raise(StandardError, 'Test error')
25
+
26
+ expect {
27
+ agent.run('Test query')
28
+ }.not_to raise_error
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators/base'
4
+
5
+ module Soka
6
+ module Generators
7
+ # Generator for installing Soka Rails configuration
8
+ class InstallGenerator < ::Rails::Generators::Base
9
+ source_root File.expand_path('templates', __dir__)
10
+
11
+ def create_initializer
12
+ template 'soka.rb', 'config/initializers/soka.rb'
13
+ end
14
+
15
+ def create_application_agent
16
+ template 'application_agent.rb', 'app/soka/agents/application_agent.rb'
17
+ end
18
+
19
+ def create_application_tool
20
+ template 'application_tool.rb', 'app/soka/tools/application_tool.rb'
21
+ end
22
+
23
+ def add_soka_directory
24
+ empty_directory 'app/soka'
25
+ empty_directory 'app/soka/agents'
26
+ empty_directory 'app/soka/tools'
27
+ end
28
+
29
+ def display_post_install_message
30
+ say "\nSoka Rails has been successfully installed!", :green
31
+ say "\nNext steps:"
32
+ say ' 1. Set your AI provider API key: GEMINI_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY'
33
+ say ' 2. Create your first agent: rails generate soka:agent MyAgent'
34
+ say ' 3. Create your first tool: rails generate soka:tool MyTool'
35
+ say "\nFor more information, visit: https://github.com/jiunjiun/soka-rails"
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Base class for all Soka agents in the application
4
+ class ApplicationAgent < Soka::Agent
5
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Base class for all Soka tools in the application
4
+ class ApplicationTool < Soka::AgentTool
5
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Soka Rails configuration
4
+ Soka::Rails.configure do |config|
5
+ # AI Provider Configuration
6
+ config.ai do |ai|
7
+ # Setup Gemini AI Studio
8
+ ai.provider = :gemini
9
+ ai.model = 'gemini-2.5-flash-lite'
10
+ ai.api_key = ENV.fetch('GEMINI_API_KEY', nil)
11
+
12
+ # Setup OpenAI
13
+ # ai.provider = :openai
14
+ # ai.model = 'gpt-4.1-mini'
15
+ # ai.api_key = ENV.fetch('OPENAI_API_KEY', nil)
16
+
17
+ # Setup Anthropic
18
+ # ai.provider = :anthropic
19
+ # ai.model = 'claude-sonnet-4-0'
20
+ # ai.api_key = ENV.fetch('ANTHROPIC_API_KEY', nil)
21
+ end
22
+
23
+ # Performance Configuration
24
+ config.performance do |perf|
25
+ # Maximum iterations for ReAct loop
26
+ perf.max_iterations = Rails.env.production? ? 10 : 5
27
+
28
+ # Timeout for agent execution (in seconds)
29
+ perf.timeout = 30
30
+ end
31
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= @tool_class_name %> < ApplicationTool
4
+ desc 'Description of your tool'
5
+
6
+ params do
7
+ <% if @params_list.any? -%>
8
+ <% @params_list.each do |param| -%>
9
+ requires :<%= param[:name] %>, <%= param[:type] %>, desc: '<%= param[:name].humanize %>'
10
+ <% end -%>
11
+ <% else -%>
12
+ # requires :param_name, String, desc: 'Parameter description'
13
+ # optional :param_name, String, desc: 'Parameter description'
14
+ <% end -%>
15
+ end
16
+
17
+ def call(<%= @params_list.map { |param| param[:name] }.join(', ') %>)
18
+ # Implement your tool logic here
19
+
20
+ # Return a hash with your results
21
+ {
22
+ result: 'Tool execution result',
23
+ processed_at: Time.current
24
+ }
25
+ end
26
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails_helper'
4
+
5
+ RSpec.describe <%= @tool_class_name %>Tool, type: :tool do
6
+ let(:tool) { described_class.new }
7
+
8
+ describe '#call' do
9
+ <% if @params_list.any? -%>
10
+ let(:params) do
11
+ {
12
+ <% @params_list.each do |param| -%>
13
+ <%= param[:name] %>: <%= param[:type] == 'String' ? "'test_value'" : "test_value" %>,
14
+ <% end -%>
15
+ }
16
+ end
17
+
18
+ it 'executes successfully with valid params' do
19
+ result = tool.call(**params)
20
+
21
+ expect(result).to be_a(Hash)
22
+ expect(result[:result]).to be_present
23
+ end
24
+ <% else -%>
25
+ it 'executes successfully' do
26
+ result = tool.call
27
+
28
+ expect(result).to be_a(Hash)
29
+ expect(result[:result]).to be_present
30
+ end
31
+ <% end -%>
32
+
33
+ it 'includes timestamp in response' do
34
+ result = tool.call<%= @params_list.any? ? '(**params)' : '' %>
35
+
36
+ expect(result[:processed_at]).to be_present
37
+ expect(result[:processed_at]).to be_within(1.second).of(Time.current)
38
+ end
39
+
40
+ it 'handles errors gracefully' do
41
+ allow(tool).to receive(:call).and_raise(StandardError, 'Test error')
42
+
43
+ expect {
44
+ tool.call<%= @params_list.any? ? '(**params)' : '' %>
45
+ }.to raise_error(StandardError)
46
+ end
47
+ end
48
+ end