activeagent 0.5.0rc2 → 0.5.0rc3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4d2af71e1d2c2bc28b9a7d2301efe45ec7252f7f749ec3e3e0385f813becff1f
4
- data.tar.gz: d5bc0672d10326144407d3f1c8554768c84fc61206541dde8f15e366cb21ae2d
3
+ metadata.gz: 0f3087fa32e11e33dd1f48e093a8d92b38e11264bf403faa65f4a57c8e3b2a4b
4
+ data.tar.gz: 57ce989b2279b23d56ab3b40eba65877ec336bc056cc375f869b1caf301aad2f
5
5
  SHA512:
6
- metadata.gz: a2f176f61abb5ab67321d70b56e858bd0ffb435636ebdad77c310c8656a43ae2086bc27a7d4b721b67061d5ac1530a7672ee23d7a3cfc104504c7a7666bbb998
7
- data.tar.gz: 1398927382c0e9c73c17eb0c275e1696e49ea835537877f927f9f6ccc31cc8a7e7fdfb93ef4478b617e02a99221419b6e4a03bda2973e2b0efdf1293c4ed119f
6
+ metadata.gz: 25c0437d878f5c2452276614c23c868dfdcec6667906e546b13176916fd12edd30ae4f6c21296ad3a724f5382e82a71a84594b4563165076aa5b50c52b98f7a2
7
+ data.tar.gz: 0cef9d5c876bf9d235c0090dfd4d873da3d0508c8c77820c70d408df2595eae7d3e167ffe65644ebcc7faa5b5e361be81c56446ad5c106877a2d651407a6d8a9
data/README.md CHANGED
@@ -1,5 +1,10 @@
1
- ![Active Agent Logo](https://framerusercontent.com/images/oEx786EYW2ZVL4Xf9hparOVLjHI.png)
2
- > *Build AI in Rails*
1
+ <picture>
2
+ <source media="(prefers-color-scheme: dark)" srcset="https://github.com/user-attachments/assets/2bad263a-c09f-40b6-94ba-fff8e346d65d">
3
+ <img alt="activeagents_banner" src="https://github.com/user-attachments/assets/0ebbaa2f-c6bf-4d40-bb77-931015a14be3">
4
+ </picture>
5
+ *Build AI in Rails*
6
+
7
+
3
8
  >
4
9
  > *Now Agents are Controllers*
5
10
  >
@@ -9,4 +14,175 @@
9
14
  Active Agent provides that missing AI layer in the Rails framework, offering a structured approach to building AI-powered applications through Agent Oriented Programming. **Now Agents are Controllers!** Designing applications using agents allows developers to create modular, reusable components that can be easily integrated into existing systems. This approach promotes code reusability, maintainability, and scalability, making it easier to build complex AI-driven applications with the Object Oriented Ruby code you already use today.
10
15
 
11
16
  ## Documentation
12
- [docs.activeagents.ai](https://docs.activeagents.ai) - The official documentation site for Active Agent.
17
+ [docs.activeagents.ai](https://docs.activeagents.ai) - The official documentation site for Active Agent.
18
+
19
+ ## Getting Started
20
+
21
+ ### Installation
22
+
23
+ Use bundler to add activeagent to your Gemfile and install:
24
+ ```bash
25
+ bundle add activeagent
26
+ ```
27
+
28
+ Add the generation provider gem you want to use:
29
+
30
+ ```bash
31
+ # OpenAI
32
+ bundle add ruby-openai
33
+
34
+ # Anthropic
35
+ bundle add ruby-anthropic
36
+
37
+ # Ollama (uses OpenAI-compatible API)
38
+ bundle add ruby-openai
39
+
40
+ # OpenRouter (uses OpenAI-compatible API)
41
+ bundle add ruby-openai
42
+ ```
43
+
44
+ ### Setup
45
+
46
+ Run the install generator to create the necessary configuration files:
47
+
48
+ ```bash
49
+ rails generate active_agent:install
50
+ ```
51
+
52
+ This creates:
53
+ - `config/active_agent.yml`: Configuration file for generation providers
54
+ - `app/agents`: Directory for your agent classes
55
+ - `app/views/agent_*`: Directory for agent prompt/view templates
56
+
57
+ ### Quick Example
58
+
59
+ Define an application agent:
60
+
61
+ ```ruby
62
+ class ApplicationAgent < ActiveAgent::Base
63
+ generate_with :openai,
64
+ instructions: "You are a helpful assistant.",
65
+ model: "gpt-4o-mini",
66
+ temperature: 0.7
67
+ end
68
+ ```
69
+
70
+ Use your agent:
71
+
72
+ ```ruby
73
+ message = "Test Application Agent"
74
+ prompt = ApplicationAgent.with(message: message).prompt_context
75
+ response = prompt.generate_now
76
+ ```
77
+
78
+ ### Your First Agent
79
+
80
+ Generate a new agent:
81
+
82
+ ```bash
83
+ rails generate active_agent:agent TravelAgent search book confirm
84
+ ```
85
+
86
+ This creates an agent with actions that can be called:
87
+
88
+ ```ruby
89
+ class TravelAgent < ApplicationAgent
90
+ def search
91
+ # Your search logic here
92
+ prompt
93
+ end
94
+
95
+ def book
96
+ # Your booking logic here
97
+ prompt
98
+ end
99
+
100
+ def confirm
101
+ # Your confirmation logic here
102
+ prompt
103
+ end
104
+ end
105
+ ```
106
+
107
+ ## Configuration
108
+
109
+ Configure generation providers in `config/active_agent.yml`:
110
+
111
+ ```yaml
112
+ development:
113
+ openai:
114
+ service: "OpenAI"
115
+ api_key: <%= Rails.application.credentials.dig(:openai, :api_key) %>
116
+ model: "gpt-4o-mini"
117
+ embeddings_model: "text-embedding-3-small"
118
+
119
+ anthropic:
120
+ service: "Anthropic"
121
+ api_key: <%= Rails.application.credentials.dig(:anthropic, :api_key) %>
122
+ model: "claude-3-5-sonnet"
123
+
124
+ ollama:
125
+ service: "Ollama"
126
+ model: "llama3.2"
127
+ embeddings_model: "nomic-embed-text"
128
+ host: "http://localhost:11434"
129
+ ```
130
+
131
+ ## Features
132
+
133
+ - **Agent-Oriented Programming**: Build AI applications using familiar Rails patterns
134
+ - **Multiple Provider Support**: Works with OpenAI, Anthropic, Ollama, and more
135
+ - **Action-Based Design**: Define agent capabilities through actions
136
+ - **View Templates**: Use ERB templates for prompts (text, JSON, HTML)
137
+ - **Streaming Support**: Real-time response streaming with ActionCable
138
+ - **Tool/Function Calling**: Agents can use tools to interact with external services
139
+ - **Context Management**: Maintain conversation history across interactions
140
+ - **Structured Output**: Define JSON schemas for predictable responses
141
+
142
+ ## Examples
143
+
144
+ ### Data Extraction
145
+ Extract structured data from images, PDFs, and text:
146
+
147
+ ```ruby
148
+ prompt = DataExtractionAgent.with(
149
+ output_schema: :chart_schema,
150
+ image_path: Rails.root.join("sales_chart.png")
151
+ ).parse_content
152
+ ```
153
+
154
+ ### Translation
155
+ Translate text between languages:
156
+
157
+ ```ruby
158
+ response = TranslationAgent.with(
159
+ message: "Hi, I'm Justin",
160
+ locale: "japanese"
161
+ ).translate.generate_now
162
+ ```
163
+
164
+ ### Tool Usage
165
+ Agents can use tools to perform actions:
166
+
167
+ ```ruby
168
+ # Agent with tool support
169
+ message = "Show me a cat"
170
+ prompt = SupportAgent.with(message: message).prompt_context
171
+ response = prompt.generate_now
172
+ # Response includes tool call results
173
+ ```
174
+
175
+ ## Learn More
176
+
177
+ - [Documentation](https://docs.activeagents.ai)
178
+ - [Getting Started Guide](https://docs.activeagents.ai/docs/getting-started)
179
+ - [API Reference](https://docs.activeagents.ai/docs/framework)
180
+ - [Examples](https://docs.activeagents.ai/docs/agents)
181
+
182
+ ## Contributing
183
+
184
+ We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
185
+
186
+ ## License
187
+
188
+ Active Agent is released under the [MIT License](LICENSE).
@@ -113,7 +113,6 @@ module ActiveAgent
113
113
  inherited_options = (self.options || {}).except(:instructions)
114
114
  self.options = inherited_options.merge(options)
115
115
  end
116
- self.options[:stream] = new.agent_stream if self.options[:stream]
117
116
  end
118
117
 
119
118
  def stream_with(&stream)
@@ -182,7 +181,9 @@ module ActiveAgent
182
181
  attr_internal :context
183
182
 
184
183
  def agent_stream
185
- proc do |message, delta, stop|
184
+ proc do |message, delta, stop, action_name|
185
+ @_action_name = action_name
186
+
186
187
  run_stream_callbacks(message, delta, stop) do |message, delta, stop|
187
188
  yield message, delta, stop if block_given?
188
189
  end
@@ -221,7 +222,6 @@ module ActiveAgent
221
222
  end
222
223
 
223
224
  def update_context(response)
224
- context.message = context.messages.last
225
225
  ActiveAgent::GenerationProvider::Response.new(prompt: context)
226
226
  end
227
227
 
@@ -233,16 +233,21 @@ module ActiveAgent
233
233
 
234
234
  def perform_action(action)
235
235
  current_context = context.clone
236
- process(action.name, *action.params)
237
- context.messages.last.role = :tool
238
- context.messages.last.action_id = action.id
239
- context.messages.last.action_name = action.name
240
- context.messages.last.generation_id = action.id
241
- current_context.messages << context.messages.last
236
+ # Set params from the action for controller access
237
+ if action.params.is_a?(Hash)
238
+ self.params = action.params
239
+ end
240
+ process(action.name)
241
+ context.message.role = :tool
242
+ context.message.action_id = action.id
243
+ context.message.action_name = action.name
244
+ context.message.generation_id = action.id
245
+ current_context.message = context.message
246
+ current_context.messages << context.message
242
247
  self.context = current_context
243
248
  end
244
249
 
245
- def initialize
250
+ def initialize # :nodoc:
246
251
  super
247
252
  @_prompt_was_called = false
248
253
  @_context = ActiveAgent::ActionPrompt::Prompt.new(options: self.class.options || {})
@@ -300,20 +305,21 @@ module ActiveAgent
300
305
  def prompt(headers = {}, &block)
301
306
  return context if @_prompt_was_called && headers.blank? && !block
302
307
 
303
- # Apply option hierarchy: prompt options > agent options > config options
308
+ # Apply option hierarchy: prompt options > agent > config options
304
309
  merged_options = merge_options(headers)
305
310
  raw_instructions = headers.has_key?(:instructions) ? headers[:instructions] : context.options[:instructions]
306
311
 
307
312
  context.instructions = prepare_instructions(raw_instructions)
308
313
 
309
314
  context.options.merge!(merged_options)
310
-
315
+ context.options[:stream] = agent_stream if context.options[:stream]
311
316
  content_type = headers[:content_type]
312
317
 
313
318
  headers = apply_defaults(headers)
314
319
  context.messages = headers[:messages] || []
315
320
  context.context_id = headers[:context_id]
316
321
  context.params = params
322
+ context.action_name = action_name
317
323
 
318
324
  context.output_schema = load_schema(headers[:output_schema], set_prefixes(headers[:output_schema], lookup_context.prefixes))
319
325
 
@@ -414,10 +420,7 @@ module ActiveAgent
414
420
  # Only merge runtime options that are actually present (not nil)
415
421
  runtime_options.each do |key, value|
416
422
  next if value.nil?
417
- # Special handling for stream option: preserve agent_stream proc if it exists
418
- if key == :stream && agent_options[:stream].is_a?(Proc) && !value.is_a?(Proc)
419
- next
420
- end
423
+
421
424
  merged[key] = value
422
425
  end
423
426
 
@@ -495,8 +498,8 @@ module ActiveAgent
495
498
  def collect_responses_from_templates(headers)
496
499
  templates_path = headers[:template_path] || self.class.agent_name
497
500
  templates_name = headers[:template_name] || action_name
498
-
499
501
  each_template(Array(templates_path), templates_name).map do |template|
502
+ next if template.format == :json && headers[:format] != :json
500
503
  format = template.format || formats.first
501
504
  {
502
505
  body: render(template: template, formats: [ format ]),
@@ -532,6 +535,7 @@ module ActiveAgent
532
535
  end
533
536
 
534
537
  def prepare_instructions(instructions)
538
+ binding.irb
535
539
  case instructions
536
540
  when Hash
537
541
  raise ArgumentError, "Expected `:template` key in instructions hash" unless instructions[:template]
@@ -4,7 +4,7 @@ module ActiveAgent
4
4
  module ActionPrompt
5
5
  class Prompt
6
6
  attr_reader :messages, :instructions
7
- attr_accessor :actions, :body, :content_type, :context_id, :message, :options, :mime_version, :charset, :context, :parts, :params, :action_choice, :agent_class, :output_schema
7
+ attr_accessor :actions, :body, :content_type, :context_id, :message, :options, :mime_version, :charset, :context, :parts, :params, :action_choice, :agent_class, :output_schema, :action_name
8
8
 
9
9
  def initialize(attributes = {})
10
10
  @options = attributes.fetch(:options, {})
@@ -26,6 +26,7 @@ module ActiveAgent
26
26
  @parts = attributes.fetch(:parts, [])
27
27
  @output_schema = attributes.fetch(:output_schema, nil)
28
28
  @messages = Message.from_messages(@messages)
29
+ @action_name = attributes.fetch(:action_name, nil)
29
30
  set_message if attributes[:message].is_a?(String) || @body.is_a?(String) && @message&.content
30
31
  set_messages if @instructions.present?
31
32
  end
@@ -56,10 +57,8 @@ module ActiveAgent
56
57
  end
57
58
 
58
59
  def add_part(message)
59
- if @content_type == message.content_type && message.content.present?
60
- @message = message
61
- set_message
62
- end
60
+ @message = message
61
+ set_message
63
62
 
64
63
  @parts << message
65
64
  end
@@ -11,26 +11,21 @@ module ActiveAgent
11
11
  end
12
12
 
13
13
  module ClassMethods
14
- # Defines a callback that will get called right before the
15
- # prompt is sent to the generation provider method.
16
- def before_generation(*filters, &blk)
17
- set_callback(:generation, :before, *filters, &blk)
18
- end
19
-
20
- # Defines a callback that will get called right after the
21
- # prompt's generation method is finished.
22
- def after_generation(*filters, &blk)
23
- set_callback(:generation, :after, *filters, &blk)
24
- end
25
-
26
- # Defines a callback that will get called around the prompt's generation method.
27
- def around_generation(*filters, &blk)
28
- set_callback(:generation, :around, *filters, &blk)
14
+ # # Defines a callback that will get called right before/after/around the
15
+ # # generation provider method.
16
+ [ :before, :after, :around ].each do |callback|
17
+ define_method "#{callback}_generation" do |*names, &blk|
18
+ _insert_callbacks(names, blk) do |name, options|
19
+ set_callback(:generation, callback, name, options)
20
+ end
21
+ end
29
22
  end
30
23
 
31
24
  # Defines a callback for handling streaming responses during generation
32
- def on_stream(*filters, &blk)
33
- set_callback(:stream, :before, *filters, &blk)
25
+ def on_stream(*names, &blk)
26
+ _insert_callbacks(names, blk) do |name, options|
27
+ set_callback(:stream, :before, name, options)
28
+ end
34
29
  end
35
30
  end
36
31
 
@@ -1,6 +1,12 @@
1
1
  # lib/active_agent/generation_provider/anthropic_provider.rb
2
2
 
3
- require "anthropic"
3
+ begin
4
+ gem "ruby-anthropic", "~> 0.4.2"
5
+ require "anthropic"
6
+ rescue LoadError
7
+ raise LoadError, "The 'ruby-anthropic' gem is required for AnthropicProvider. Please add it to your Gemfile and run `bundle install`."
8
+ end
9
+
4
10
  require "active_agent/action_prompt/action"
5
11
  require_relative "base"
6
12
  require_relative "response"
@@ -10,9 +16,8 @@ module ActiveAgent
10
16
  class AnthropicProvider < Base
11
17
  def initialize(config)
12
18
  super
13
- @api_key = config["api_key"]
14
- @model_name = config["model"] || "claude-3-5-sonnet-20240620"
15
- @client = Anthropic::Client.new(access_token: @api_key)
19
+ @access_token ||= config["api_key"] || config["access_token"] || Anthropic.configuration.access_token || ENV["ANTHROPIC_ACCESS_TOKEN"]
20
+ @client = Anthropic::Client.new(access_token: @access_token)
16
21
  end
17
22
 
18
23
  def generate(prompt)
@@ -40,7 +45,7 @@ module ActiveAgent
40
45
  proc do |chunk|
41
46
  if new_content = chunk.dig(:delta, :text)
42
47
  message.content += new_content
43
- agent_stream.call(message) if agent_stream.respond_to?(:call)
48
+ agent_stream.call(message, nil, false, prompt.action_name) if agent_stream.respond_to?(:call)
44
49
  end
45
50
  end
46
51
  end
@@ -4,7 +4,7 @@ module ActiveAgent
4
4
  module GenerationProvider
5
5
  class Base
6
6
  class GenerationProviderError < StandardError; end
7
- attr_reader :client, :config, :prompt, :response
7
+ attr_reader :client, :config, :prompt, :response, :access_token, :model_name
8
8
 
9
9
  def initialize(config)
10
10
  @config = config
@@ -6,10 +6,10 @@ module ActiveAgent
6
6
  class OllamaProvider < OpenAIProvider
7
7
  def initialize(config)
8
8
  @config = config
9
- @api_key = config["api_key"]
9
+ @access_token ||= config["api_key"] || config["access_token"] || ENV["OLLAMA_API_KEY"] || ENV["OLLAMA_ACCESS_TOKEN"]
10
10
  @model_name = config["model"]
11
11
  @host = config["host"] || "http://localhost:11434"
12
- @client = OpenAI::Client.new(uri_base: @host, access_token: @api_key, log_errors: true)
12
+ @client = OpenAI::Client.new(uri_base: @host, access_token: @access_token, log_errors: true)
13
13
  end
14
14
  end
15
15
  end
@@ -15,14 +15,13 @@ module ActiveAgent
15
15
  class OpenAIProvider < Base
16
16
  def initialize(config)
17
17
  super
18
- @api_key = config["api_key"]
19
- @model_name = config["model"] || "gpt-4o-mini"
18
+ @host = config["host"] || nil
19
+ @access_token ||= config["api_key"] || config["access_token"] || OpenAI.configuration.access_token || ENV["OPENAI_ACCESS_TOKEN"]
20
+ @organization_id = config["organization_id"] || OpenAI.configuration.organization_id || ENV["OPENAI_ORGANIZATION_ID"]
21
+ @admin_token = config["admin_token"] || OpenAI.configuration.admin_token || ENV["OPENAI_ADMIN_TOKEN"]
22
+ @client = OpenAI::Client.new(access_token: @access_token, uri_base: @host, organization_id: @organization_id)
20
23
 
21
- @client = if (@host = config["host"])
22
- OpenAI::Client.new(uri_base: @host, access_token: @api_key)
23
- else
24
- OpenAI::Client.new(access_token: @api_key)
25
- end
24
+ @model_name = config["model"] || "gpt-4o-mini"
26
25
  end
27
26
 
28
27
  def generate(prompt)
@@ -61,7 +60,7 @@ module ActiveAgent
61
60
  message.generation_id = chunk.dig("id")
62
61
  message.content += new_content
63
62
 
64
- agent_stream.call(message, new_content, false) do |message, new_content|
63
+ agent_stream.call(message, new_content, false, prompt.action_name) do |message, new_content|
65
64
  yield message, new_content if block_given?
66
65
  end
67
66
  elsif chunk.dig("choices", 0, "delta", "tool_calls") && chunk.dig("choices", 0, "delta", "role")
@@ -70,7 +69,7 @@ module ActiveAgent
70
69
  @response = ActiveAgent::GenerationProvider::Response.new(prompt:, message:)
71
70
  end
72
71
 
73
- agent_stream.call(message, nil, true) do |message|
72
+ agent_stream.call(message, nil, true, prompt.action_name) do |message|
74
73
  yield message, nil if block_given?
75
74
  end
76
75
  end
@@ -6,9 +6,9 @@ module ActiveAgent
6
6
  class OpenRouterProvider < OpenAIProvider
7
7
  def initialize(config)
8
8
  @config = config
9
- @api_key = config["api_key"]
9
+ @access_token ||= config["api_key"] || config["access_token"] || ENV["OPENROUTER_API_KEY"] || ENV["OPENROUTER_ACCESS_TOKEN"]
10
10
  @model_name = config["model"]
11
- @client = OpenAI::Client.new(uri_base: "https://openrouter.ai/api/v1", access_token: @api_key, log_errors: true)
11
+ @client = OpenAI::Client.new(uri_base: "https://openrouter.ai/api/v1", access_token: @access_token, log_errors: true)
12
12
  end
13
13
  end
14
14
  end
@@ -10,6 +10,34 @@ module ActiveAgent
10
10
  @message = message || prompt.message
11
11
  @raw_response = raw_response
12
12
  end
13
+
14
+ # Extract usage statistics from the raw response
15
+ def usage
16
+ return nil unless @raw_response
17
+
18
+ # OpenAI/OpenRouter format
19
+ if @raw_response.is_a?(Hash) && @raw_response["usage"]
20
+ @raw_response["usage"]
21
+ # Anthropic format
22
+ elsif @raw_response.is_a?(Hash) && @raw_response["usage"]
23
+ @raw_response["usage"]
24
+ else
25
+ nil
26
+ end
27
+ end
28
+
29
+ # Helper methods for common usage stats
30
+ def prompt_tokens
31
+ usage&.dig("prompt_tokens")
32
+ end
33
+
34
+ def completion_tokens
35
+ usage&.dig("completion_tokens")
36
+ end
37
+
38
+ def total_tokens
39
+ usage&.dig("total_tokens")
40
+ end
13
41
  end
14
42
  end
15
43
  end
@@ -12,17 +12,21 @@ module ActiveAgent
12
12
  end
13
13
 
14
14
  module ClassMethods
15
- def configuration(provider_name, **options)
16
- config = ActiveAgent.config[provider_name.to_s] || ActiveAgent.config.dig(ENV["RAILS_ENV"], provider_name.to_s)
15
+ def configuration(name_or_provider, **options)
16
+ config = ActiveAgent.config[name_or_provider.to_s] || ActiveAgent.config.dig(ENV["RAILS_ENV"], name_or_provider.to_s) || {}
17
17
 
18
- raise "Configuration not found for provider: #{provider_name}" unless config
18
+ config = { "service" => "OpenAI" } if config.empty? && name_or_provider == :openai
19
19
  config.merge!(options)
20
+ raise "Failed to load provider #{name_or_provider}: configuration not found for provider" if config["service"].nil?
20
21
  configure_provider(config)
22
+ rescue LoadError => e
23
+ raise RuntimeError, "Failed to load provider #{name_or_provider}: #{e.message}"
21
24
  end
22
25
 
23
26
  def configure_provider(config)
24
- require "active_agent/generation_provider/#{config["service"].underscore}_provider"
25
- ActiveAgent::GenerationProvider.const_get("#{config["service"].camelize}Provider").new(config)
27
+ service_name = config["service"]
28
+ require "active_agent/generation_provider/#{service_name.underscore}_provider"
29
+ ActiveAgent::GenerationProvider.const_get("#{service_name.camelize}Provider").new(config)
26
30
  end
27
31
 
28
32
  def generation_provider
@@ -40,6 +44,9 @@ module ActiveAgent
40
44
  when Symbol, String
41
45
  provider = configuration(name_or_provider)
42
46
  assign_provider(name_or_provider.to_s, provider)
47
+ when OpenAI::Client
48
+ name = :openai
49
+ assign_provider(name, name_or_provider)
43
50
  else
44
51
  raise ArgumentError
45
52
  end
@@ -1,3 +1,3 @@
1
1
  module ActiveAgent
2
- VERSION = "0.5.0rc2"
2
+ VERSION = "0.5.0rc3"
3
3
  end
data/lib/active_agent.rb CHANGED
@@ -56,6 +56,8 @@ module ActiveAgent
56
56
  config_file = YAML.load(ERB.new(File.read(file)).result, aliases: true)
57
57
  env = ENV["RAILS_ENV"] || ENV["ENV"] || "development"
58
58
  @config = config_file[env] || config_file
59
+ else
60
+ @config = {}
59
61
  end
60
62
  end
61
63
  end
data/lib/generators/USAGE CHANGED
@@ -3,23 +3,45 @@ Description:
3
3
  configuration files and application agent class.
4
4
 
5
5
  This generator creates:
6
- - Configuration file (config/active_agent.yml)
6
+ - Configuration file (config/active_agent.yml) unless --skip-config is used
7
7
  - Application agent base class (app/agents/application_agent.rb)
8
8
  - Layout templates for agent views (via template engine hooks)
9
9
 
10
+ Options:
11
+ --skip-config Skip the creation of config/active_agent.yml
12
+ --formats=FORMAT Specify formats to generate layouts for (default: text)
13
+ Can be one or more of: text, html, json
14
+ Example: --formats=text html json
15
+
10
16
  Examples:
11
17
  `bin/rails generate active_agent:install`
12
18
 
13
- creates the basic ActiveAgent setup:
19
+ creates the basic ActiveAgent setup with default text format:
14
20
  Config: config/active_agent.yml
15
21
  Base Agent: app/agents/application_agent.rb
16
- Layouts: app/views/layouts/agent.html.erb
17
- app/views/layouts/agent.text.erb
22
+ Layout: app/views/layouts/agent.text.erb
23
+
24
+ `bin/rails generate active_agent:install --formats=text html json`
25
+
26
+ creates ActiveAgent setup with all format layouts:
27
+ Config: config/active_agent.yml
28
+ Base Agent: app/agents/application_agent.rb
29
+ Layouts: app/views/layouts/agent.text.erb
30
+ app/views/layouts/agent.html.erb
18
31
  app/views/layouts/agent.json.erb
19
32
 
20
- `bin/rails generate active_agent:install --template-engine=haml`
33
+ `bin/rails generate active_agent:install --skip-config`
21
34
 
22
- creates ActiveAgent setup with Haml layouts instead of ERB.
35
+ creates ActiveAgent setup without configuration file:
36
+ Base Agent: app/agents/application_agent.rb
37
+ Layout: app/views/layouts/agent.text.erb
38
+
39
+ `bin/rails generate active_agent:install --skip-config --formats=html json`
40
+
41
+ creates ActiveAgent setup without config file but with HTML and JSON layouts:
42
+ Base Agent: app/agents/application_agent.rb
43
+ Layouts: app/views/layouts/agent.html.erb
44
+ app/views/layouts/agent.json.erb
23
45
 
24
46
  After running this generator, you can create individual agents with:
25
47
  `bin/rails generate active_agent:agent AGENT_NAME ACTION_NAME`
@@ -6,25 +6,51 @@ Description:
6
6
  engine and test framework generators. If no ApplicationAgent exists,
7
7
  it will be created automatically.
8
8
 
9
- Views are created for each action in three formats:
10
- - HTML (.html.erb) - for rich text responses
11
- - Text (.text.erb) - for plain text responses
12
- - JSON (.json.erb) - for structured function calling responses
9
+ By default, only text format views are created. You can specify which
10
+ formats to generate using the --formats option:
11
+ - text (.text.erb) - for plain text responses (default)
12
+ - html (.html.erb) - for rich text responses
13
+ - json (.json.erb) - for structured function calling responses
14
+
15
+ Options:
16
+ --formats=FORMAT Specify formats to generate views for (default: text)
17
+ Can be one or more of: text, html, json
18
+ Example: --formats=text html json
13
19
 
14
20
  Examples:
15
21
  `bin/rails generate active_agent:agent inventory search`
16
22
 
17
- creates an inventory agent class, views, and test:
23
+ creates an inventory agent class with text format view and test:
24
+ Agent: app/agents/inventory_agent.rb
25
+ View: app/views/inventory_agent/search.text.erb
26
+ Test: test/agents/inventory_agent_test.rb
27
+
28
+ `bin/rails generate active_agent:agent inventory search --formats=html json`
29
+
30
+ creates an inventory agent with html and json views:
18
31
  Agent: app/agents/inventory_agent.rb
19
32
  Views: app/views/inventory_agent/search.html.erb
20
- app/views/inventory_agent/search.text.erb
21
33
  app/views/inventory_agent/search.json.erb
22
34
  Test: test/agents/inventory_agent_test.rb
23
35
 
24
- `bin/rails generate active_agent:agent inventory search update report`
36
+ `bin/rails generate active_agent:agent inventory search update report --formats=text html json`
25
37
 
26
- creates an inventory agent with search, update, and report actions.
38
+ creates an inventory agent with search, update, and report actions in all formats:
39
+ Agent: app/agents/inventory_agent.rb
40
+ Views: app/views/inventory_agent/search.text.erb
41
+ app/views/inventory_agent/search.html.erb
42
+ app/views/inventory_agent/search.json.erb
43
+ app/views/inventory_agent/update.text.erb
44
+ app/views/inventory_agent/update.html.erb
45
+ app/views/inventory_agent/update.json.erb
46
+ app/views/inventory_agent/report.text.erb
47
+ app/views/inventory_agent/report.html.erb
48
+ app/views/inventory_agent/report.json.erb
49
+ Test: test/agents/inventory_agent_test.rb
27
50
 
28
- `bin/rails generate active_agent:agent admin/user create --template-engine=haml`
51
+ `bin/rails generate active_agent:agent admin/user create --formats=json`
29
52
 
30
- creates a namespaced admin/user agent with Haml templates.
53
+ creates a namespaced admin/user agent with only JSON view for API responses:
54
+ Agent: app/agents/admin/user_agent.rb
55
+ View: app/views/admin/user_agent/create.json.erb
56
+ Test: test/agents/admin/user_agent_test.rb
@@ -4,8 +4,8 @@ module ActiveAgent
4
4
  module Generators
5
5
  class AgentGenerator < ::Rails::Generators::NamedBase
6
6
  source_root File.expand_path("templates", __dir__)
7
-
8
7
  argument :actions, type: :array, default: [], banner: "method method"
8
+ class_option :formats, type: :array, default: [ "text" ], desc: "Specify formats to generate (text, html, json)"
9
9
 
10
10
  check_class_collision suffix: "Agent"
11
11
 
@@ -23,6 +23,10 @@ module ActiveAgent
23
23
 
24
24
  private
25
25
 
26
+ def formats
27
+ options[:formats].map(&:to_sym)
28
+ end
29
+
26
30
  def file_name # :doc:
27
31
  @_file_name ||= super.sub(/_agent\z/i, "")
28
32
  end
@@ -3,15 +3,16 @@
3
3
  module ActiveAgent
4
4
  module Generators
5
5
  class InstallGenerator < ::Rails::Generators::Base
6
+ class_option :skip_config, type: :boolean, default: false, desc: "Skip configuration file generation"
7
+ class_option :formats, type: :array, default: [ "text" ], desc: "Specify formats to generate (text, html, json)"
8
+
6
9
  def self.usage_path
7
10
  @usage_path ||= File.expand_path("../USAGE", __dir__)
8
11
  end
9
12
  source_root File.expand_path("templates", __dir__)
10
13
 
11
- hook_for :template_engine, :test_framework
12
-
13
14
  def create_configuration
14
- template "active_agent.yml", "config/active_agent.yml"
15
+ template "active_agent.yml", "config/active_agent.yml" unless options[:skip_config]
15
16
  end
16
17
 
17
18
  def create_application_agent
@@ -22,7 +23,12 @@ module ActiveAgent
22
23
  end
23
24
  end
24
25
 
26
+ hook_for :template_engine
27
+
25
28
  private
29
+ def formats
30
+ options[:formats].map(&:to_sym)
31
+ end
26
32
 
27
33
  def application_agent_file_name
28
34
  "app/agents/application_agent.rb"
@@ -1,7 +1,7 @@
1
1
  <% module_namespacing do -%>
2
2
  class ApplicationAgent < ActiveAgent::Base
3
- layout 'agent'
4
-
3
+ layout "agent"
4
+
5
5
  generate_with :openai, model: "gpt-4o-mini", instructions: "You are a helpful assistant."
6
6
  end
7
- <% end %>
7
+ <% end %>
@@ -7,6 +7,7 @@ module Erb # :nodoc:
7
7
  class AgentGenerator < Base # :nodoc:
8
8
  source_root File.expand_path("templates", __dir__)
9
9
  argument :actions, type: :array, default: [], banner: "method method"
10
+ class_option :formats, type: :array, default: [ "text" ], desc: "Specify formats to generate (text, html, json)"
10
11
 
11
12
  def copy_view_files
12
13
  view_base_path = File.join("app/views", class_path, file_name + "_agent")
@@ -30,9 +31,8 @@ module Erb # :nodoc:
30
31
  end
31
32
 
32
33
  private
33
-
34
34
  def formats
35
- [ :text, :html, :json ]
35
+ options[:formats].map(&:to_sym)
36
36
  end
37
37
 
38
38
  def file_name
@@ -1,16 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "rails/generators"
4
-
5
3
  module Erb # :nodoc:
6
4
  module Generators # :nodoc:
7
5
  class InstallGenerator < ::Rails::Generators::Base # :nodoc:
8
6
  source_root File.expand_path("templates", __dir__)
7
+ class_option :formats, type: :array, default: [ "text" ], desc: "Specify formats to generate (text, html, json)"
9
8
 
10
9
  def create_agent_layouts
11
- template "layout.html.erb.tt", "app/views/layouts/agent.html.erb"
12
- template "layout.text.erb.tt", "app/views/layouts/agent.text.erb"
13
- template "layout.json.erb.tt", "app/views/layouts/agent.json.erb"
10
+ if behavior == :invoke
11
+ formats.each do |format|
12
+ puts format
13
+ layout_path = File.join("app/views/layouts", filename_with_extensions("agent", format))
14
+ template filename_with_extensions(:layout, format), layout_path unless File.exist?(layout_path)
15
+ end
16
+ end
17
+ end
18
+
19
+ private
20
+ def formats
21
+ options[:formats].map(&:to_sym)
22
+ end
23
+
24
+ def file_name
25
+ @_file_name ||= super.sub(/_agent\z/i, "")
26
+ end
27
+
28
+ def filename_with_extensions(name, file_format = format)
29
+ [ name, file_format, handler ].compact.join(".")
30
+ end
31
+
32
+ def handler
33
+ :erb
14
34
  end
15
35
  end
16
36
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activeagent
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0rc2
4
+ version: 0.5.0rc3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Bowen
@@ -190,7 +190,6 @@ files:
190
190
  - lib/generators/active_agent/templates/application_agent.rb.tt
191
191
  - lib/generators/erb/agent_generator.rb
192
192
  - lib/generators/erb/install_generator.rb
193
- - lib/generators/erb/templates/application_agent.rb.tt
194
193
  - lib/generators/erb/templates/layout.html.erb.tt
195
194
  - lib/generators/erb/templates/layout.json.erb.tt
196
195
  - lib/generators/erb/templates/layout.text.erb.tt
@@ -1,7 +0,0 @@
1
- <% module_namespacing do -%>
2
- class ApplicationAgent < ActiveAgent::Base
3
- layout 'agent'
4
-
5
- generate_with :openai, model: "gpt-4o-mini", instructions: "You are a helpful assistant."
6
- end
7
- <% end %>