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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +367 -0
- data/CHANGELOG.md +41 -0
- data/CLAUDE.md +243 -0
- data/DESIGN.md +957 -0
- data/LICENSE +21 -0
- data/README.md +420 -0
- data/REQUIREMENT.md +308 -0
- data/Rakefile +12 -0
- data/SPEC.md +420 -0
- data/app/soka/agents/application_agent.rb +5 -0
- data/app/soka/tools/application_tool.rb +5 -0
- data/lib/generators/soka/agent/agent_generator.rb +60 -0
- data/lib/generators/soka/agent/templates/agent.rb.tt +25 -0
- data/lib/generators/soka/agent/templates/agent_spec.rb.tt +31 -0
- data/lib/generators/soka/install/install_generator.rb +39 -0
- data/lib/generators/soka/install/templates/application_agent.rb +5 -0
- data/lib/generators/soka/install/templates/application_tool.rb +5 -0
- data/lib/generators/soka/install/templates/soka.rb +31 -0
- data/lib/generators/soka/tool/templates/tool.rb.tt +26 -0
- data/lib/generators/soka/tool/templates/tool_spec.rb.tt +48 -0
- data/lib/generators/soka/tool/tool_generator.rb +68 -0
- data/lib/soka/rails/agent_extensions.rb +42 -0
- data/lib/soka/rails/configuration.rb +65 -0
- data/lib/soka/rails/errors.rb +73 -0
- data/lib/soka/rails/railtie.rb +15 -0
- data/lib/soka/rails/rspec.rb +29 -0
- data/lib/soka/rails/test_helpers.rb +117 -0
- data/lib/soka/rails/version.rb +7 -0
- data/lib/soka/rails.rb +24 -0
- data/lib/soka_rails.rb +11 -0
- metadata +124 -0
data/DESIGN.md
ADDED
@@ -0,0 +1,957 @@
|
|
1
|
+
# Soka Rails Design Document
|
2
|
+
|
3
|
+
## 1. System Architecture Design
|
4
|
+
|
5
|
+
### 1.1 Overall Architecture Diagram
|
6
|
+
|
7
|
+
```
|
8
|
+
┌─────────────────────────────────────────────────────────────┐
|
9
|
+
│ Rails Application │
|
10
|
+
├─────────────────────────────────────────────────────────────┤
|
11
|
+
│ Soka Rails Layer │
|
12
|
+
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────────┐ │
|
13
|
+
│ │ Railtie │ │ Generators │ │ Test Helpers │ │
|
14
|
+
│ └─────────────┘ └──────────────┘ └──────────────────┘ │
|
15
|
+
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────────┐ │
|
16
|
+
│ │Configuration│ │ Agent Bridge │ │ Tool Bridge │ │
|
17
|
+
│ └─────────────┘ └──────────────┘ └──────────────────┘ │
|
18
|
+
├─────────────────────────────────────────────────────────────┤
|
19
|
+
│ Soka Core Framework │
|
20
|
+
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────────┐ │
|
21
|
+
│ │ Agent │ │ Tool │ │ ReAct Engine │ │
|
22
|
+
│ └─────────────┘ └──────────────┘ └──────────────────┘ │
|
23
|
+
└─────────────────────────────────────────────────────────────┘
|
24
|
+
```
|
25
|
+
|
26
|
+
### 1.2 Module Design
|
27
|
+
|
28
|
+
#### 1.2.1 Core Module Structure
|
29
|
+
```ruby
|
30
|
+
module Soka
|
31
|
+
module Rails
|
32
|
+
# Version definition
|
33
|
+
VERSION = "0.0.1.beta1"
|
34
|
+
|
35
|
+
# Main components
|
36
|
+
class Railtie < ::Rails::Railtie; end
|
37
|
+
class Configuration; end
|
38
|
+
class Engine < ::Rails::Engine; end
|
39
|
+
|
40
|
+
# Bridge layer
|
41
|
+
module AgentBridge; end
|
42
|
+
module ToolBridge; end
|
43
|
+
|
44
|
+
# Test support
|
45
|
+
module TestHelpers; end
|
46
|
+
|
47
|
+
# Generator namespace
|
48
|
+
module Generators; end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
```
|
52
|
+
|
53
|
+
## 2. Core Component Design
|
54
|
+
|
55
|
+
### 2.1 Railtie Design (FR-001, FR-002)
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
# lib/soka/rails/railtie.rb
|
59
|
+
module Soka
|
60
|
+
module Rails
|
61
|
+
class Railtie < ::Rails::Railtie
|
62
|
+
# Autoload path configuration
|
63
|
+
initializer "soka.autoload_paths" do |app|
|
64
|
+
app.config.autoload_paths << app.root.join("app/soka")
|
65
|
+
app.config.eager_load_paths << app.root.join("app/soka")
|
66
|
+
end
|
67
|
+
|
68
|
+
# Load configuration
|
69
|
+
initializer "soka.load_configuration" do
|
70
|
+
config_file = ::Rails.root.join("config/initializers/soka.rb")
|
71
|
+
require config_file if config_file.exist?
|
72
|
+
end
|
73
|
+
|
74
|
+
# Setup Zeitwerk
|
75
|
+
initializer "soka.setup_zeitwerk" do
|
76
|
+
::Rails.autoloaders.main.push_dir(
|
77
|
+
::Rails.root.join("app/soka"),
|
78
|
+
namespace: Object
|
79
|
+
)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Integrate Rails logger
|
83
|
+
initializer "soka.setup_logger" do
|
84
|
+
Soka.configure do |config|
|
85
|
+
config.logger = ::Rails.logger
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Development environment hot reload
|
90
|
+
if ::Rails.env.development?
|
91
|
+
config.to_prepare do
|
92
|
+
# Reload Soka related classes
|
93
|
+
Dir[::Rails.root.join("app/soka/**/*.rb")].each { |f| load f }
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
```
|
100
|
+
|
101
|
+
### 2.2 Configuration Design (FR-004, FR-010)
|
102
|
+
|
103
|
+
```ruby
|
104
|
+
# lib/soka/rails/configuration.rb
|
105
|
+
module Soka
|
106
|
+
module Rails
|
107
|
+
class Configuration
|
108
|
+
attr_accessor :ai_provider, :ai_model, :ai_api_key
|
109
|
+
attr_accessor :max_iterations, :timeout
|
110
|
+
|
111
|
+
def initialize
|
112
|
+
# Use ENV.fetch to set default values
|
113
|
+
@ai_provider = ENV.fetch('SOKA_PROVIDER', :gemini).to_sym
|
114
|
+
@ai_model = ENV.fetch('SOKA_MODEL', 'gemini-2.0-flash-exp')
|
115
|
+
@ai_api_key = ENV.fetch('SOKA_API_KEY', nil)
|
116
|
+
|
117
|
+
# Performance settings
|
118
|
+
@max_iterations = ::Rails.env.production? ? 10 : 5
|
119
|
+
@timeout = 30.seconds
|
120
|
+
end
|
121
|
+
|
122
|
+
# DSL configuration methods
|
123
|
+
def ai
|
124
|
+
yield(AIConfig.new(self)) if block_given?
|
125
|
+
end
|
126
|
+
|
127
|
+
def performance
|
128
|
+
yield(PerformanceConfig.new(self)) if block_given?
|
129
|
+
end
|
130
|
+
|
131
|
+
# Internal configuration classes
|
132
|
+
class AIConfig
|
133
|
+
def initialize(config)
|
134
|
+
@config = config
|
135
|
+
end
|
136
|
+
|
137
|
+
def provider=(value)
|
138
|
+
@config.ai_provider = value
|
139
|
+
end
|
140
|
+
|
141
|
+
def model=(value)
|
142
|
+
@config.ai_model = value
|
143
|
+
end
|
144
|
+
|
145
|
+
def api_key=(value)
|
146
|
+
@config.ai_api_key = value
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
class PerformanceConfig
|
151
|
+
def initialize(config)
|
152
|
+
@config = config
|
153
|
+
end
|
154
|
+
|
155
|
+
def max_iterations=(value)
|
156
|
+
@config.max_iterations = value
|
157
|
+
end
|
158
|
+
|
159
|
+
def timeout=(value)
|
160
|
+
@config.timeout = value
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# Global configuration methods
|
166
|
+
class << self
|
167
|
+
attr_writer :configuration
|
168
|
+
|
169
|
+
def configuration
|
170
|
+
@configuration ||= Configuration.new
|
171
|
+
end
|
172
|
+
|
173
|
+
def configure
|
174
|
+
yield(configuration)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
```
|
180
|
+
|
181
|
+
### 2.3 ApplicationAgent Design (FR-006, FR-007)
|
182
|
+
|
183
|
+
```ruby
|
184
|
+
# app/soka/agents/application_agent.rb
|
185
|
+
class ApplicationAgent < Soka::Agent
|
186
|
+
# Rails environment default configuration
|
187
|
+
if defined?(::Rails)
|
188
|
+
max_iterations ::Rails.env.production? ? 10 : 5
|
189
|
+
timeout 30.seconds
|
190
|
+
end
|
191
|
+
|
192
|
+
# Default tools
|
193
|
+
tool RailsInfoTool if defined?(RailsInfoTool)
|
194
|
+
|
195
|
+
# Rails integration hooks
|
196
|
+
before_action :log_agent_start
|
197
|
+
after_action :log_agent_complete
|
198
|
+
on_error :handle_agent_error
|
199
|
+
|
200
|
+
private
|
201
|
+
|
202
|
+
# Log Agent start execution
|
203
|
+
def log_agent_start(input)
|
204
|
+
::Rails.logger.info "[Soka] #{self.class.name} started: #{input.truncate(100)}"
|
205
|
+
tag_request
|
206
|
+
end
|
207
|
+
|
208
|
+
# Log Agent completion
|
209
|
+
def log_agent_complete(result)
|
210
|
+
::Rails.logger.info "[Soka] #{self.class.name} completed: #{result.status}"
|
211
|
+
cleanup_request_tags
|
212
|
+
end
|
213
|
+
|
214
|
+
# Error handling and tracking
|
215
|
+
def handle_agent_error(error, context)
|
216
|
+
::Rails.logger.error "[Soka] #{self.class.name} error: #{error.message}"
|
217
|
+
::Rails.logger.error error.backtrace.join("\n")
|
218
|
+
|
219
|
+
# Integrate Rails error tracking
|
220
|
+
if defined?(::Rails.error)
|
221
|
+
::Rails.error.report(error,
|
222
|
+
context: {
|
223
|
+
agent: self.class.name,
|
224
|
+
input: context[:input],
|
225
|
+
iteration: context[:iteration]
|
226
|
+
}
|
227
|
+
)
|
228
|
+
end
|
229
|
+
|
230
|
+
:continue # Allow continuing execution
|
231
|
+
end
|
232
|
+
|
233
|
+
# Tag request for tracking
|
234
|
+
def tag_request
|
235
|
+
return unless defined?(::Rails.application.config.log_tags)
|
236
|
+
|
237
|
+
request_id = SecureRandom.uuid
|
238
|
+
Thread.current[:soka_request_id] = request_id
|
239
|
+
end
|
240
|
+
|
241
|
+
def cleanup_request_tags
|
242
|
+
Thread.current[:soka_request_id] = nil
|
243
|
+
end
|
244
|
+
end
|
245
|
+
```
|
246
|
+
|
247
|
+
### 2.4 ApplicationTool Design (FR-008)
|
248
|
+
|
249
|
+
```ruby
|
250
|
+
# app/soka/tools/application_tool.rb
|
251
|
+
class ApplicationTool < Soka::AgentTool
|
252
|
+
# Rails specific helper methods
|
253
|
+
|
254
|
+
protected
|
255
|
+
|
256
|
+
# Safe parameter filtering
|
257
|
+
private
|
258
|
+
def safe_params(params)
|
259
|
+
ActionController::Parameters.new(params).permit!
|
260
|
+
end
|
261
|
+
end
|
262
|
+
```
|
263
|
+
|
264
|
+
### 2.5 RailsInfoTool Design (FR-009)
|
265
|
+
|
266
|
+
```ruby
|
267
|
+
# app/soka/tools/rails_info_tool.rb
|
268
|
+
class RailsInfoTool < ApplicationTool
|
269
|
+
desc "Get Rails application information"
|
270
|
+
|
271
|
+
params do
|
272
|
+
requires :info_type, String,
|
273
|
+
desc: "Type of information to retrieve",
|
274
|
+
validates: {
|
275
|
+
inclusion: {
|
276
|
+
in: %w[routes version environment config]
|
277
|
+
}
|
278
|
+
}
|
279
|
+
end
|
280
|
+
|
281
|
+
def call(info_type:)
|
282
|
+
case info_type
|
283
|
+
when 'routes'
|
284
|
+
fetch_routes_info
|
285
|
+
when 'version'
|
286
|
+
fetch_version_info
|
287
|
+
when 'environment'
|
288
|
+
fetch_environment_info
|
289
|
+
when 'config'
|
290
|
+
fetch_safe_config_info
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
private
|
295
|
+
|
296
|
+
def fetch_routes_info
|
297
|
+
routes = ::Rails.application.routes.routes.map do |route|
|
298
|
+
next unless route.name.present?
|
299
|
+
|
300
|
+
{
|
301
|
+
name: route.name,
|
302
|
+
verb: route.verb,
|
303
|
+
path: route.path.spec.to_s,
|
304
|
+
controller: route.defaults[:controller],
|
305
|
+
action: route.defaults[:action]
|
306
|
+
}
|
307
|
+
end.compact
|
308
|
+
|
309
|
+
{ routes: routes }
|
310
|
+
end
|
311
|
+
|
312
|
+
def fetch_version_info
|
313
|
+
{
|
314
|
+
rails_version: ::Rails.version,
|
315
|
+
ruby_version: RUBY_VERSION,
|
316
|
+
app_name: ::Rails.application.class.module_parent_name,
|
317
|
+
environment: ::Rails.env
|
318
|
+
}
|
319
|
+
end
|
320
|
+
|
321
|
+
def fetch_environment_info
|
322
|
+
{
|
323
|
+
rails_env: ::Rails.env,
|
324
|
+
time_zone: Time.zone.name,
|
325
|
+
host: ::Rails.application.config.hosts.first.to_s,
|
326
|
+
database: ActiveRecord::Base.connection.adapter_name
|
327
|
+
}
|
328
|
+
end
|
329
|
+
|
330
|
+
def fetch_safe_config_info
|
331
|
+
# Only return safe configuration values
|
332
|
+
safe_configs = {
|
333
|
+
time_zone: ::Rails.application.config.time_zone,
|
334
|
+
locale: I18n.locale.to_s,
|
335
|
+
default_locale: I18n.default_locale.to_s,
|
336
|
+
available_locales: I18n.available_locales.map(&:to_s),
|
337
|
+
eager_load: ::Rails.application.config.eager_load,
|
338
|
+
consider_all_requests_local: ::Rails.application.config.consider_all_requests_local,
|
339
|
+
force_ssl: ::Rails.application.config.force_ssl,
|
340
|
+
public_file_server_enabled: ::Rails.application.config.public_file_server.enabled
|
341
|
+
}
|
342
|
+
|
343
|
+
safe_configs
|
344
|
+
end
|
345
|
+
end
|
346
|
+
```
|
347
|
+
|
348
|
+
## 3. Generator Design (FR-003)
|
349
|
+
|
350
|
+
### 3.1 Install Generator
|
351
|
+
|
352
|
+
```ruby
|
353
|
+
# lib/generators/soka/install/install_generator.rb
|
354
|
+
module Soka
|
355
|
+
module Generators
|
356
|
+
class InstallGenerator < ::Rails::Generators::Base
|
357
|
+
source_root File.expand_path('templates', __dir__)
|
358
|
+
|
359
|
+
def create_initializer
|
360
|
+
template 'soka.rb', 'config/initializers/soka.rb'
|
361
|
+
end
|
362
|
+
|
363
|
+
def create_application_agent
|
364
|
+
template 'application_agent.rb', 'app/soka/agents/application_agent.rb'
|
365
|
+
end
|
366
|
+
|
367
|
+
def create_application_tool
|
368
|
+
template 'application_tool.rb', 'app/soka/tools/application_tool.rb'
|
369
|
+
end
|
370
|
+
|
371
|
+
def create_rails_info_tool
|
372
|
+
template 'rails_info_tool.rb', 'app/soka/tools/rails_info_tool.rb'
|
373
|
+
end
|
374
|
+
|
375
|
+
def add_soka_directory
|
376
|
+
empty_directory 'app/soka'
|
377
|
+
empty_directory 'app/soka/agents'
|
378
|
+
empty_directory 'app/soka/tools'
|
379
|
+
end
|
380
|
+
|
381
|
+
def display_post_install_message
|
382
|
+
say "\nSoka Rails has been successfully installed!", :green
|
383
|
+
say "\nNext steps:"
|
384
|
+
say " 1. Set your AI provider API key: SOKA_API_KEY=your_key"
|
385
|
+
say " 2. Create your first agent: rails generate soka:agent MyAgent"
|
386
|
+
say " 3. Create your first tool: rails generate soka:tool MyTool"
|
387
|
+
end
|
388
|
+
end
|
389
|
+
end
|
390
|
+
end
|
391
|
+
```
|
392
|
+
|
393
|
+
### 3.2 Agent Generator
|
394
|
+
|
395
|
+
```ruby
|
396
|
+
# lib/generators/soka/agent/agent_generator.rb
|
397
|
+
module Soka
|
398
|
+
module Generators
|
399
|
+
class AgentGenerator < ::Rails::Generators::NamedBase
|
400
|
+
source_root File.expand_path('templates', __dir__)
|
401
|
+
|
402
|
+
argument :tools, type: :array, default: [], banner: "tool1 tool2"
|
403
|
+
|
404
|
+
def create_agent_file
|
405
|
+
@agent_class_name = class_name
|
406
|
+
@tools_list = tools
|
407
|
+
|
408
|
+
template 'agent.rb.tt',
|
409
|
+
File.join('app/soka/agents', class_path, "#{file_name}_agent.rb")
|
410
|
+
end
|
411
|
+
|
412
|
+
def create_test_file
|
413
|
+
@agent_class_name = class_name
|
414
|
+
|
415
|
+
template 'agent_spec.rb.tt',
|
416
|
+
File.join('spec/soka/agents', class_path, "#{file_name}_agent_spec.rb")
|
417
|
+
end
|
418
|
+
end
|
419
|
+
end
|
420
|
+
end
|
421
|
+
```
|
422
|
+
|
423
|
+
### 3.3 Tool Generator
|
424
|
+
|
425
|
+
```ruby
|
426
|
+
# lib/generators/soka/tool/tool_generator.rb
|
427
|
+
module Soka
|
428
|
+
module Generators
|
429
|
+
class ToolGenerator < ::Rails::Generators::NamedBase
|
430
|
+
source_root File.expand_path('templates', __dir__)
|
431
|
+
|
432
|
+
argument :params, type: :array, default: [], banner: 'param1:type param2:type'
|
433
|
+
|
434
|
+
def create_tool_file
|
435
|
+
@tool_class_name = class_name
|
436
|
+
@params_list = parse_params
|
437
|
+
|
438
|
+
template 'tool.rb.tt',
|
439
|
+
File.join('app/soka/tools', class_path, "#{file_name}_tool.rb")
|
440
|
+
end
|
441
|
+
|
442
|
+
def create_test_file
|
443
|
+
@tool_class_name = class_name
|
444
|
+
|
445
|
+
template 'tool_spec.rb.tt',
|
446
|
+
File.join('spec/soka/tools', class_path, "#{file_name}_tool_spec.rb")
|
447
|
+
end
|
448
|
+
|
449
|
+
private
|
450
|
+
|
451
|
+
def parse_params
|
452
|
+
params.map do |param|
|
453
|
+
name, type = param.split(':')
|
454
|
+
{ name: name, type: type || 'String' }
|
455
|
+
end
|
456
|
+
end
|
457
|
+
end
|
458
|
+
end
|
459
|
+
end
|
460
|
+
```
|
461
|
+
|
462
|
+
## 4. Test Support Design (FR-005)
|
463
|
+
|
464
|
+
### 4.1 RSpec Test Helpers
|
465
|
+
|
466
|
+
```ruby
|
467
|
+
# lib/soka/rails/test_helpers.rb
|
468
|
+
module Soka
|
469
|
+
module Rails
|
470
|
+
module TestHelpers
|
471
|
+
extend ActiveSupport::Concern
|
472
|
+
|
473
|
+
included do
|
474
|
+
let(:mock_llm) { instance_double(Soka::LLM) }
|
475
|
+
end
|
476
|
+
|
477
|
+
# Mock AI response
|
478
|
+
def mock_ai_response(response_attrs = {})
|
479
|
+
default_response = {
|
480
|
+
final_answer: "Mocked answer",
|
481
|
+
confidence_score: 0.95,
|
482
|
+
status: :completed,
|
483
|
+
iterations: 1,
|
484
|
+
thought_process: []
|
485
|
+
}
|
486
|
+
|
487
|
+
response = default_response.merge(response_attrs)
|
488
|
+
|
489
|
+
allow(Soka::LLM).to receive(:new).and_return(mock_llm)
|
490
|
+
allow(mock_llm).to receive(:chat).and_return(
|
491
|
+
OpenStruct.new(content: build_react_response(response))
|
492
|
+
)
|
493
|
+
end
|
494
|
+
|
495
|
+
# Mock tool execution
|
496
|
+
def mock_tool_execution(tool_class, result)
|
497
|
+
allow_any_instance_of(tool_class).to receive(:call).and_return(result)
|
498
|
+
end
|
499
|
+
|
500
|
+
# Build ReAct format response
|
501
|
+
def build_react_response(attrs)
|
502
|
+
thoughts = attrs[:thought_process].presence ||
|
503
|
+
["Analyzing the request"]
|
504
|
+
|
505
|
+
response = thoughts.map { |t| "<Thought>#{t}</Thought>" }.join("\n")
|
506
|
+
response += "\n<Final_Answer>#{attrs[:final_answer]}</Final_Answer>"
|
507
|
+
response
|
508
|
+
end
|
509
|
+
|
510
|
+
# Agent test helper methods
|
511
|
+
def run_agent(agent, input, &block)
|
512
|
+
result = nil
|
513
|
+
|
514
|
+
if block_given?
|
515
|
+
result = agent.run(input, &block)
|
516
|
+
else
|
517
|
+
result = agent.run(input)
|
518
|
+
end
|
519
|
+
|
520
|
+
expect(result).to be_a(Struct)
|
521
|
+
result
|
522
|
+
end
|
523
|
+
|
524
|
+
# Event collector
|
525
|
+
def collect_agent_events(agent, input)
|
526
|
+
events = []
|
527
|
+
|
528
|
+
agent.run(input) do |event|
|
529
|
+
events << event
|
530
|
+
end
|
531
|
+
|
532
|
+
events
|
533
|
+
end
|
534
|
+
|
535
|
+
# Test configuration
|
536
|
+
def with_test_configuration
|
537
|
+
original_config = Soka::Rails.configuration.dup
|
538
|
+
|
539
|
+
Soka::Rails.configure do |config|
|
540
|
+
config.ai_provider = :mock
|
541
|
+
config.max_iterations = 3
|
542
|
+
config.timeout = 5.seconds
|
543
|
+
yield config if block_given?
|
544
|
+
end
|
545
|
+
|
546
|
+
yield
|
547
|
+
ensure
|
548
|
+
Soka::Rails.configuration = original_config
|
549
|
+
end
|
550
|
+
end
|
551
|
+
end
|
552
|
+
end
|
553
|
+
```
|
554
|
+
|
555
|
+
### 4.2 RSpec Configuration
|
556
|
+
|
557
|
+
```ruby
|
558
|
+
# lib/soka/rails/rspec.rb
|
559
|
+
require 'soka/rails/test_helpers'
|
560
|
+
|
561
|
+
RSpec.configure do |config|
|
562
|
+
config.include Soka::Rails::TestHelpers, type: :agent
|
563
|
+
config.include Soka::Rails::TestHelpers, type: :tool
|
564
|
+
|
565
|
+
config.before(:each, type: :agent) do
|
566
|
+
# Reset Soka configuration
|
567
|
+
Soka.configuration = Soka::Configuration.new
|
568
|
+
end
|
569
|
+
|
570
|
+
config.before(:each, type: :tool) do
|
571
|
+
# Ensure tools are available
|
572
|
+
allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('test'))
|
573
|
+
end
|
574
|
+
end
|
575
|
+
```
|
576
|
+
|
577
|
+
## 5. Integration Points Design
|
578
|
+
|
579
|
+
### 5.1 Controller Integration
|
580
|
+
|
581
|
+
```ruby
|
582
|
+
# Example: Using in Controller
|
583
|
+
class ConversationsController < ApplicationController
|
584
|
+
def create
|
585
|
+
agent = CustomerSupportAgent.new(
|
586
|
+
memory: session_memory,
|
587
|
+
context: current_user_context
|
588
|
+
)
|
589
|
+
|
590
|
+
result = agent.run(conversation_params[:message])
|
591
|
+
|
592
|
+
session[:conversation_memory] = agent.memory.to_a
|
593
|
+
|
594
|
+
respond_to do |format|
|
595
|
+
format.json { render json: build_response(result) }
|
596
|
+
format.html { redirect_to conversation_path, notice: result.final_answer }
|
597
|
+
end
|
598
|
+
rescue Soka::Error => e
|
599
|
+
handle_soka_error(e)
|
600
|
+
end
|
601
|
+
|
602
|
+
private
|
603
|
+
|
604
|
+
def session_memory
|
605
|
+
Soka::Memory.new(session[:conversation_memory] || [])
|
606
|
+
end
|
607
|
+
|
608
|
+
def current_user_context
|
609
|
+
{
|
610
|
+
user_id: current_user.id,
|
611
|
+
user_name: current_user.name,
|
612
|
+
user_role: current_user.role
|
613
|
+
}
|
614
|
+
end
|
615
|
+
|
616
|
+
def build_response(result)
|
617
|
+
{
|
618
|
+
answer: result.final_answer,
|
619
|
+
confidence: result.confidence_score,
|
620
|
+
status: result.status,
|
621
|
+
metadata: {
|
622
|
+
iterations: result.iterations,
|
623
|
+
timestamp: Time.current
|
624
|
+
}
|
625
|
+
}
|
626
|
+
end
|
627
|
+
|
628
|
+
def handle_soka_error(error)
|
629
|
+
Rails.logger.error "[Soka Error] #{error.message}"
|
630
|
+
render json: { error: "AI processing error occurred" }, status: :internal_server_error
|
631
|
+
end
|
632
|
+
end
|
633
|
+
```
|
634
|
+
|
635
|
+
### 5.2 ActiveJob Integration
|
636
|
+
|
637
|
+
```ruby
|
638
|
+
# Example: Background job integration
|
639
|
+
class ProcessConversationJob < ApplicationJob
|
640
|
+
queue_as :default
|
641
|
+
|
642
|
+
def perform(user_id, message)
|
643
|
+
user = User.find(user_id)
|
644
|
+
agent = CustomerSupportAgent.new(
|
645
|
+
memory: user.conversation_memory,
|
646
|
+
context: { user_id: user.id }
|
647
|
+
)
|
648
|
+
|
649
|
+
result = agent.run(message)
|
650
|
+
|
651
|
+
# Save result
|
652
|
+
user.conversations.create!(
|
653
|
+
message: message,
|
654
|
+
response: result.final_answer,
|
655
|
+
confidence: result.confidence_score,
|
656
|
+
metadata: {
|
657
|
+
iterations: result.iterations,
|
658
|
+
thought_process: result.thought_process
|
659
|
+
}
|
660
|
+
)
|
661
|
+
|
662
|
+
# Send notification
|
663
|
+
UserMailer.conversation_processed(user, result).deliver_later
|
664
|
+
end
|
665
|
+
end
|
666
|
+
```
|
667
|
+
|
668
|
+
### 5.3 ActionCable Integration
|
669
|
+
|
670
|
+
```ruby
|
671
|
+
# Example: Real-time communication integration
|
672
|
+
class ConversationChannel < ApplicationCable::Channel
|
673
|
+
def subscribed
|
674
|
+
stream_from "conversation_#{params[:conversation_id]}"
|
675
|
+
end
|
676
|
+
|
677
|
+
def receive(data)
|
678
|
+
agent = CustomerSupportAgent.new
|
679
|
+
|
680
|
+
agent.run(data['message']) do |event|
|
681
|
+
ActionCable.server.broadcast(
|
682
|
+
"conversation_#{params[:conversation_id]}",
|
683
|
+
{
|
684
|
+
type: event.type,
|
685
|
+
content: event.content,
|
686
|
+
timestamp: Time.current
|
687
|
+
}
|
688
|
+
)
|
689
|
+
end
|
690
|
+
end
|
691
|
+
end
|
692
|
+
```
|
693
|
+
|
694
|
+
## 6. Performance Optimization Design
|
695
|
+
|
696
|
+
### 6.1 Connection Pool Design
|
697
|
+
|
698
|
+
```ruby
|
699
|
+
module Soka
|
700
|
+
module Rails
|
701
|
+
class ConnectionPool
|
702
|
+
include Singleton
|
703
|
+
|
704
|
+
def initialize
|
705
|
+
@pools = {}
|
706
|
+
@mutex = Mutex.new
|
707
|
+
end
|
708
|
+
|
709
|
+
def with_connection(provider, &block)
|
710
|
+
pool = get_pool(provider)
|
711
|
+
pool.with(&block)
|
712
|
+
end
|
713
|
+
|
714
|
+
private
|
715
|
+
|
716
|
+
def get_pool(provider)
|
717
|
+
@mutex.synchronize do
|
718
|
+
@pools[provider] ||= ConnectionPool.new(
|
719
|
+
size: pool_size,
|
720
|
+
timeout: pool_timeout
|
721
|
+
) do
|
722
|
+
create_connection(provider)
|
723
|
+
end
|
724
|
+
end
|
725
|
+
end
|
726
|
+
|
727
|
+
def pool_size
|
728
|
+
::Rails.env.production? ? 10 : 2
|
729
|
+
end
|
730
|
+
|
731
|
+
def pool_timeout
|
732
|
+
5.seconds
|
733
|
+
end
|
734
|
+
|
735
|
+
def create_connection(provider)
|
736
|
+
Soka::LLM.new(provider: provider)
|
737
|
+
end
|
738
|
+
end
|
739
|
+
end
|
740
|
+
end
|
741
|
+
```
|
742
|
+
|
743
|
+
## 7. Error Handling Design
|
744
|
+
|
745
|
+
### 7.1 Error Class Hierarchy
|
746
|
+
|
747
|
+
```ruby
|
748
|
+
module Soka
|
749
|
+
module Rails
|
750
|
+
class Error < StandardError; end
|
751
|
+
|
752
|
+
class ConfigurationError < Error; end
|
753
|
+
class AgentError < Error; end
|
754
|
+
class ToolError < Error; end
|
755
|
+
class GeneratorError < Error; end
|
756
|
+
|
757
|
+
# Specific errors
|
758
|
+
class MissingApiKeyError < ConfigurationError
|
759
|
+
def initialize(provider)
|
760
|
+
super("Missing API key for provider: #{provider}")
|
761
|
+
end
|
762
|
+
end
|
763
|
+
|
764
|
+
class InvalidProviderError < ConfigurationError
|
765
|
+
def initialize(provider)
|
766
|
+
super("Invalid AI provider: #{provider}")
|
767
|
+
end
|
768
|
+
end
|
769
|
+
|
770
|
+
class AgentTimeoutError < AgentError
|
771
|
+
def initialize(timeout)
|
772
|
+
super("Agent execution timeout after #{timeout} seconds")
|
773
|
+
end
|
774
|
+
end
|
775
|
+
end
|
776
|
+
end
|
777
|
+
```
|
778
|
+
|
779
|
+
## 8. Security Design (NFR-003)
|
780
|
+
|
781
|
+
### 8.1 API Key Management
|
782
|
+
|
783
|
+
```ruby
|
784
|
+
module Soka
|
785
|
+
module Rails
|
786
|
+
module Security
|
787
|
+
class ApiKeyManager
|
788
|
+
def self.fetch_api_key(provider)
|
789
|
+
key = ENV.fetch("SOKA_#{provider.upcase}_API_KEY") do
|
790
|
+
ENV.fetch('SOKA_API_KEY', nil)
|
791
|
+
end
|
792
|
+
|
793
|
+
raise MissingApiKeyError.new(provider) if key.blank?
|
794
|
+
|
795
|
+
key
|
796
|
+
end
|
797
|
+
|
798
|
+
def self.validate_api_key(key)
|
799
|
+
return false if key.blank?
|
800
|
+
return false if key.length < 20
|
801
|
+
return false if key.include?(' ')
|
802
|
+
|
803
|
+
true
|
804
|
+
end
|
805
|
+
end
|
806
|
+
end
|
807
|
+
end
|
808
|
+
end
|
809
|
+
```
|
810
|
+
|
811
|
+
### 8.2 Parameter Filtering
|
812
|
+
|
813
|
+
```ruby
|
814
|
+
module Soka
|
815
|
+
module Rails
|
816
|
+
module Security
|
817
|
+
module ParameterFiltering
|
818
|
+
extend ActiveSupport::Concern
|
819
|
+
|
820
|
+
FILTERED_PARAMS = %w[
|
821
|
+
api_key
|
822
|
+
password
|
823
|
+
secret
|
824
|
+
token
|
825
|
+
auth
|
826
|
+
].freeze
|
827
|
+
|
828
|
+
def filter_sensitive_params(params)
|
829
|
+
filtered = params.deep_dup
|
830
|
+
|
831
|
+
FILTERED_PARAMS.each do |param|
|
832
|
+
filtered.deep_transform_keys! do |key|
|
833
|
+
if key.to_s.downcase.include?(param)
|
834
|
+
filtered[key] = '[FILTERED]'
|
835
|
+
end
|
836
|
+
end
|
837
|
+
end
|
838
|
+
|
839
|
+
filtered
|
840
|
+
end
|
841
|
+
end
|
842
|
+
end
|
843
|
+
end
|
844
|
+
end
|
845
|
+
```
|
846
|
+
|
847
|
+
## 9. Deployment Considerations
|
848
|
+
|
849
|
+
### 9.1 Gem Structure
|
850
|
+
|
851
|
+
```
|
852
|
+
soka-rails/
|
853
|
+
├── lib/
|
854
|
+
│ ├── soka-rails.rb
|
855
|
+
│ ├── soka/
|
856
|
+
│ │ └── rails/
|
857
|
+
│ │ ├── version.rb
|
858
|
+
│ │ ├── railtie.rb
|
859
|
+
│ │ ├── configuration.rb
|
860
|
+
│ │ ├── test_helpers.rb
|
861
|
+
│ │ └── rspec.rb
|
862
|
+
│ └── generators/
|
863
|
+
│ └── soka/
|
864
|
+
│ ├── install/
|
865
|
+
│ ├── agent/
|
866
|
+
│ └── tool/
|
867
|
+
├── app/
|
868
|
+
│ └── soka/
|
869
|
+
│ ├── agents/
|
870
|
+
│ │ └── application_agent.rb
|
871
|
+
│ └── tools/
|
872
|
+
│ ├── application_tool.rb
|
873
|
+
│ └── rails_info_tool.rb
|
874
|
+
├── spec/
|
875
|
+
├── Gemfile
|
876
|
+
├── soka-rails.gemspec
|
877
|
+
├── README.md
|
878
|
+
└── LICENSE
|
879
|
+
```
|
880
|
+
|
881
|
+
### 9.2 Gemspec Configuration
|
882
|
+
|
883
|
+
```ruby
|
884
|
+
# frozen_string_literal: true
|
885
|
+
|
886
|
+
require_relative 'lib/soka/rails/version'
|
887
|
+
|
888
|
+
Gem::Specification.new do |spec|
|
889
|
+
spec.name = 'soka-rails'
|
890
|
+
spec.version = Soka::Rails::VERSION
|
891
|
+
spec.authors = ['jiunjiun']
|
892
|
+
spec.email = ['imjiunjiun@gmail.com']
|
893
|
+
|
894
|
+
spec.summary = 'Rails integration for Soka AI Agent Framework'
|
895
|
+
spec.description = 'Soka Rails provides seamless integration between the Soka AI Agent Framework ' \
|
896
|
+
'and Ruby on Rails applications, following Rails conventions for easy adoption.'
|
897
|
+
spec.homepage = 'https://github.com/jiunjiun/soka-rails'
|
898
|
+
spec.license = 'MIT'
|
899
|
+
spec.required_ruby_version = '>= 3.4'
|
900
|
+
|
901
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
902
|
+
spec.metadata['source_code_uri'] = 'https://github.com/jiunjiun/soka-rails'
|
903
|
+
spec.metadata['changelog_uri'] = 'https://github.com/jiunjiun/soka-rails/blob/main/CHANGELOG.md'
|
904
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
905
|
+
|
906
|
+
# Specify which files should be added to the gem when it is released.
|
907
|
+
gemspec = File.basename(__FILE__)
|
908
|
+
spec.files = IO.popen(%w[git ls-files -z], chdir: __dir__, err: IO::NULL) do |ls|
|
909
|
+
ls.readlines("\x0", chomp: true).reject do |f|
|
910
|
+
(f == gemspec) ||
|
911
|
+
f.start_with?(*%w[bin/ test/ spec/ features/ .git .github/ appveyor Gemfile])
|
912
|
+
end
|
913
|
+
end
|
914
|
+
|
915
|
+
spec.require_paths = ['lib']
|
916
|
+
|
917
|
+
# Runtime dependencies
|
918
|
+
spec.add_dependency 'rails', '>= 7.0', '< 9.0'
|
919
|
+
spec.add_dependency 'soka', '~> 0.0.1'
|
920
|
+
spec.add_dependency 'zeitwerk', '~> 2.6'
|
921
|
+
|
922
|
+
# Development dependencies
|
923
|
+
spec.add_development_dependency 'rspec-rails', '~> 6.1'
|
924
|
+
spec.add_development_dependency 'rubocop', '~> 1.60'
|
925
|
+
spec.add_development_dependency 'rubocop-rails', '~> 2.23'
|
926
|
+
spec.add_development_dependency 'rubocop-rspec', '~> 2.25'
|
927
|
+
spec.add_development_dependency 'pry-rails', '~> 0.3'
|
928
|
+
spec.add_development_dependency 'yard', '~> 0.9'
|
929
|
+
end
|
930
|
+
```
|
931
|
+
|
932
|
+
## 10. Implementation Priority
|
933
|
+
|
934
|
+
### Phase 1: Core Foundation (Week 1-2)
|
935
|
+
1. Railtie implementation
|
936
|
+
2. Configuration system
|
937
|
+
3. Base Agent/Tool classes
|
938
|
+
|
939
|
+
### Phase 2: Generator (Week 3-4)
|
940
|
+
1. Install Generator
|
941
|
+
2. Agent Generator
|
942
|
+
3. Tool Generator
|
943
|
+
|
944
|
+
### Phase 3: Test Support (Week 5-6)
|
945
|
+
1. RSpec Test Helpers
|
946
|
+
2. Mock system
|
947
|
+
3. Test examples
|
948
|
+
|
949
|
+
### Phase 4: Advanced Features (Week 7-8)
|
950
|
+
1. Connection pool optimization
|
951
|
+
2. Performance optimization
|
952
|
+
3. Error handling improvements
|
953
|
+
|
954
|
+
### Phase 5: Documentation & Release (Week 9-10)
|
955
|
+
1. API documentation
|
956
|
+
2. Usage guide
|
957
|
+
3. Gem release preparation
|