activeagent 0.0.1.alpha → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +153 -0
  3. data/Rakefile +3 -0
  4. data/lib/active_agent/README.md +21 -0
  5. data/lib/active_agent/action_prompt/README.md +44 -0
  6. data/lib/active_agent/action_prompt/base.rb +0 -0
  7. data/lib/active_agent/action_prompt/collector.rb +34 -0
  8. data/lib/active_agent/action_prompt/message.rb +44 -0
  9. data/lib/active_agent/action_prompt/prompt.rb +79 -0
  10. data/lib/active_agent/action_prompt.rb +127 -0
  11. data/lib/active_agent/base.rb +439 -0
  12. data/lib/active_agent/callbacks.rb +31 -0
  13. data/lib/active_agent/deprecator.rb +7 -0
  14. data/lib/active_agent/engine.rb +14 -0
  15. data/lib/active_agent/generation.rb +78 -0
  16. data/lib/active_agent/generation_job.rb +47 -0
  17. data/lib/active_agent/generation_methods.rb +60 -0
  18. data/lib/active_agent/generation_provider/README.md +17 -0
  19. data/lib/active_agent/generation_provider/base.rb +36 -0
  20. data/lib/active_agent/generation_provider/open_ai_provider.rb +68 -0
  21. data/lib/active_agent/generation_provider/response.rb +15 -0
  22. data/lib/active_agent/generation_provider.rb +63 -0
  23. data/lib/active_agent/inline_preview_interceptor.rb +60 -0
  24. data/lib/active_agent/log_subscriber.rb +44 -0
  25. data/lib/active_agent/operation.rb +13 -0
  26. data/lib/active_agent/parameterized.rb +66 -0
  27. data/lib/active_agent/preview.rb +133 -0
  28. data/lib/active_agent/prompt_helper.rb +19 -0
  29. data/lib/active_agent/queued_generation.rb +12 -0
  30. data/lib/active_agent/railtie.rb +89 -0
  31. data/lib/active_agent/rescuable.rb +34 -0
  32. data/lib/active_agent/service.rb +25 -0
  33. data/lib/active_agent/test_case.rb +125 -0
  34. data/lib/active_agent/version.rb +3 -0
  35. data/lib/active_agent.rb +27 -5
  36. data/lib/generators/active_agent/USAGE +18 -0
  37. data/lib/generators/active_agent/agent_generator.rb +63 -0
  38. data/lib/generators/active_agent/templates/action.html.erb.tt +0 -0
  39. data/lib/generators/active_agent/templates/action.json.jbuilder.tt +33 -0
  40. data/lib/generators/active_agent/templates/agent.rb.tt +14 -0
  41. data/lib/generators/active_agent/templates/agent_spec.rb.tt +0 -0
  42. data/lib/generators/active_agent/templates/agent_test.rb.tt +0 -0
  43. data/lib/generators/active_agent/templates/application_agent.rb.tt +4 -0
  44. metadata +128 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: de5df915a84f0835479481fe92aeca21119898593b7b598817c0be2ff70a78ea
4
- data.tar.gz: '085f9480e8f0bca465b47a5c7751b629dd256d413145b8673a734286d9db7714'
3
+ metadata.gz: 9fd9454a4d2c7177bcdd55b33f306074e50ce042466f9c91f0c2a6915205c4e2
4
+ data.tar.gz: 1d44cf106407cb1dd0ac83281393bf5d80e06c5af6ba86d696dec66223c298ca
5
5
  SHA512:
6
- metadata.gz: 6dc168a515156565906653929fedce429712addf8f454ed5732cc05225c30ad9bae3db7c7c4315440b29447f22b814ad25c49c7f1914c63d2e8f165099fac6f0
7
- data.tar.gz: 4115f1c0e0496708cecfecade88975cfff72bdefc2b939e310dcb0c34f6ba46917cff46134c67e1157f545828a08e658341e5665f6c3f58b6785a27b63b8a38f
6
+ metadata.gz: fca8164f2bd4766107ac5a550c122a0672ba542fe909e63b1b34cbfed28d77fe850ec9adcc2fd0100d74ef59e33d6297740fab29d6e5fee75265bbeaf587be5b
7
+ data.tar.gz: 59de3e4889ed5cb59920fe5e1a6fba20160e881ab64c711b2837f2bbdabc9ef9168b204887d31f058eaa8e4920c059397149efad65c6b9dda57b1bf74ff2dbb0
data/README.md ADDED
@@ -0,0 +1,153 @@
1
+ # ActiveAgent
2
+
3
+ ActiveAgent is a Rails framework for creating and managing AI agents. It provides a structured way to interact with AI services through agents that can generate text, images, speech-to-text, and text-to-speech. It includes modules for defining prompts, actions, and rendering generative UI, as well as scaling with asynchronous jobs and streaming.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'active_agent'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ ```sh
16
+ bundle install
17
+ ```
18
+ ## Getting Started
19
+
20
+ ## Usage
21
+
22
+ ### Generating an Agent
23
+ ```
24
+ rails generate agent inventory search
25
+ ```
26
+
27
+ This will generate the following files:
28
+ ```
29
+ app/agents/application_agent.rb
30
+ app/agents/inventory_agent.rb
31
+ app/views/inventory_agent/search.text.erb
32
+ app/views/inventory_agent/search.json.jbuilder
33
+ ```
34
+
35
+ ### Define Agents
36
+
37
+ Agents are the core of ActiveAgent. An agent takes prompts and can perform actions to generate content. Agents are defined by a simple Ruby class that inherits from `ActiveAgent::Base` and are located in the `app/agents` directory.
38
+
39
+ ```ruby
40
+ #
41
+
42
+ inventory_agent.rb
43
+
44
+
45
+ class InventoryAgent < ActiveAgent::Base
46
+ generate_with :openai, model: 'gpt-4o-mini', temperature: 0.5, instructions: :inventory_operations
47
+
48
+ def search
49
+ @items = Item.search(params[:query])
50
+ end
51
+
52
+ def inventory_operations
53
+ @organization = Organization.find(params[:account_id])
54
+ prompt
55
+ end
56
+ end
57
+ ```
58
+
59
+ ### Interact with AI Services
60
+
61
+ ActiveAgent allows you to interact with various AI services to generate text, images, speech-to-text, and text-to-speech.
62
+
63
+ ```ruby
64
+ class SupportAgent < ActiveAgent::Base
65
+ generate_with :openai, model: 'gpt-4o-mini', instructions: :instructions
66
+
67
+ def perform(content, context)
68
+ @content = content
69
+ @context = context
70
+ end
71
+
72
+ def generate_message
73
+ provider_instance.generate(self)
74
+ end
75
+
76
+ private
77
+
78
+ def after_generate
79
+ broadcast_message
80
+ end
81
+
82
+ def broadcast_message
83
+ broadcast_append_later_to(
84
+ broadcast_stream,
85
+ target: broadcast_target,
86
+ partial: 'support_agent/message',
87
+ locals: { message: @message }
88
+ )
89
+ end
90
+
91
+ def broadcast_stream
92
+ "#{dom_id(@chat)}_messages"
93
+ end
94
+ end
95
+ ```
96
+
97
+ ### Render Generative UI
98
+
99
+ ActiveAgent uses Action Prompt both for rendering `instructions` prompt views as well as rendering action views. Prompts are Action Views that provide instructions for the agent to generate content.
100
+
101
+ ```erb
102
+ <!--
103
+
104
+ instructions.text.erb
105
+
106
+ -->
107
+ INSTRUCTIONS: You are an inventory manager for <%= @organization.name %>. You can search for inventory or reconcile inventory using <%= assigned_actions %>
108
+ ```
109
+
110
+ ### Scale with Asynchronous Jobs and Streaming
111
+
112
+ ActiveAgent supports asynchronous job processing and streaming for scalable AI interactions.
113
+
114
+ #### Asynchronous Jobs
115
+
116
+ Use the `generate_later` method to enqueue a job for later processing.
117
+
118
+ ```ruby
119
+ InventoryAgent.with(query: query).search.generate_later
120
+ ```
121
+
122
+ #### Streaming
123
+
124
+ Use the `stream_with` method to handle streaming responses.
125
+
126
+ ```ruby
127
+ class InventoryAgent < ActiveAgent::Base
128
+ generate_with :openai, model: 'gpt-4o-mini', stream: :broadcast_results
129
+
130
+ private
131
+
132
+ def broadcast_results
133
+ proc do |chunk, _bytesize|
134
+ @message.content = @message.content + chunk
135
+ broadcast_append_to(
136
+ "#{dom_id(chat)}_messages",
137
+ partial: "messages/message",
138
+ locals: { message: @message, scroll_to: true },
139
+ target: "#{dom_id(chat)}_messages"
140
+ )
141
+ end
142
+ end
143
+ end
144
+ ```
145
+
146
+ ## Contributing
147
+
148
+ Bug reports and pull requests are welcome on GitHub at https://github.com/yourusername/active_agent.
149
+
150
+ ## License
151
+
152
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
153
+ ```
data/Rakefile ADDED
@@ -0,0 +1,3 @@
1
+ require "bundler/setup"
2
+
3
+ require "bundler/gem_tasks"
@@ -0,0 +1,21 @@
1
+ # Active Agent
2
+ Active Agent is a Rails framework for creating and managing AI agents. It provides a structured way to interact with generation providers through agents with context including prompts, tools, and messages. It includes two core modules, Generation Provider and Action Prompt, along with several support classes to handle different aspects of agent creation and management.
3
+
4
+ ## Core Modules
5
+
6
+ - Generation Provider - module for configuring and interacting with generation providers through Prompts and Responses.
7
+ - Action Prompt - module for defining prompts, tools, and messages. The Base class implements an AbstractController to render prompts and actions. Prompts are Action Views that provide instructions for the agent to generate content, formatted messages for the agent and users including **streaming generative UI**.
8
+
9
+ ## Main Components
10
+
11
+ - Base class - for creating and configuring agents.
12
+ - Queued Generation - module for managing queued generation requests and responses. Using the Generation Job class to perform asynchronous generation requests, it provides a way to **stream generation** requests back to the Job, Agent, or User.
13
+
14
+ ### ActiveAgent::Base
15
+
16
+ The Base class is used to create agents that interact with generation providers through prompts and messages. It includes methods for configuring and interacting with generation providers using Prompts and Responses. The Base class also provides a structured way to render prompts and actions.
17
+
18
+ #### Core Methods
19
+
20
+ - `generate_with(provider, options = {})` - Configures the agent to generate content using the specified generation provider and options.
21
+ - `streams_with(provider, options = {})` - Configures the agent to stream content using the specified generation provider's stream option.
@@ -0,0 +1,44 @@
1
+ # Active Agent: Action Prompt
2
+
3
+ ActionPrompt provides a structured way to create and manage prompts and tools for AI interactions. It includes several support classes to handle different aspects of prompt creation and management. The Base class implements an AbstractController to perform actions that render prompts..
4
+
5
+ ## Main Components
6
+
7
+ Module - for including in your agent classes to provide prompt functionality.
8
+ Base class - for creating and managing prompts in ActiveAgent.
9
+ Tool class - for representing the tool object sent to the Agent's generation provider.
10
+ Message - class for representing a single message within a prompt.
11
+ Prompt - class for representing a the context of a prompt, including messages, actions, and other attributes.
12
+
13
+ ### ActionPrompt::Base
14
+
15
+ The base class is used to create and manage prompts in Active Agent. It provides the core functionality for creating and managing contexts woth prompts, tools, and messages.
16
+
17
+ #### Core Methods
18
+ `prompt` - Creates a new prompt object with the given attributes.
19
+
20
+
21
+ ### ActionPrompt::Tool
22
+
23
+ ### ActionPrompt::Message
24
+
25
+ Represents a single message within a prompt.
26
+
27
+ ### ActionPrompt::Prompt
28
+
29
+ Manages the overall structure of a prompt, including multiple messages, actions, and other attributes.
30
+
31
+ ### ActionPrompt::Action
32
+
33
+ Represents an action that represents the tool object sent to the Agent's generation provider can be associated with a prompt or message.
34
+
35
+ ## Usage
36
+
37
+ To use ActionPrompt in your agent, include the module in your agent class:
38
+
39
+ ```ruby
40
+ class MyAgent
41
+ include ActiveAgent::ActionPrompt
42
+
43
+ # Your agent code here
44
+ end
File without changes
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "abstract_controller/collector"
4
+ require "active_support/core_ext/hash/reverse_merge"
5
+ require "active_support/core_ext/array/extract_options"
6
+
7
+ module ActiveAgent
8
+ module ActionPrompt
9
+ class Collector
10
+ include AbstractController::Collector
11
+ attr_reader :responses
12
+
13
+ def initialize(context, &block)
14
+ @context = context
15
+ @responses = []
16
+ @default_render = block
17
+ end
18
+
19
+ def any(*args, &block)
20
+ options = args.extract_options!
21
+ raise ArgumentError, "You have to supply at least one format" if args.empty?
22
+ args.each { |type| send(type, options.dup, &block) }
23
+ end
24
+ alias_method :all, :any
25
+
26
+ def custom(mime, options = {})
27
+ options.reverse_merge!(content_type: mime.to_s)
28
+ @context.formats = [mime.to_sym]
29
+ options[:body] = block_given? ? yield : @default_render.call
30
+ @responses << options
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,44 @@
1
+ module ActiveAgent
2
+ module ActionPrompt
3
+ class Message
4
+ VALID_ROLES = %w[system assistant user tool function].freeze
5
+
6
+ attr_accessor :content, :role, :name, :action_requested, :requested_actions
7
+
8
+ def initialize(attributes = {})
9
+ @content = attributes[:content] || ""
10
+ @role = attributes[:role] || "user"
11
+ @name = attributes[:name]
12
+ @action_requested = attributes[:function_call]
13
+ @requested_actions = attributes[:tool_calls] || []
14
+ validate_role
15
+ end
16
+
17
+ def to_h
18
+ hash = {role: role, content: content}
19
+ hash[:name] = name if name
20
+ hash[:action_requested] = action_requested if action_requested
21
+ hash[:requested_actions] = requested_actions if requested_actions.any?
22
+ hash
23
+ end
24
+
25
+ def perform_actions
26
+ requested_actions.each do |action|
27
+ action.call(self) if action.respond_to?(:call)
28
+ end
29
+ end
30
+
31
+ def action_requested?
32
+ action_requested.present? || requested_actions.any?
33
+ end
34
+
35
+ private
36
+
37
+ def validate_role
38
+ unless VALID_ROLES.include?(role.to_s)
39
+ raise ArgumentError, "Invalid role: #{role}. Valid roles are: #{VALID_ROLES.join(", ")}"
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,79 @@
1
+ # lib/active_agent/action_prompt/prompt.rb
2
+ require_relative "message"
3
+
4
+ module ActiveAgent
5
+ module ActionPrompt
6
+ class Prompt
7
+ attr_accessor :actions, :body, :content_type, :instructions, :message, :messages, :options, :mime_version, :charset, :context
8
+
9
+ def initialize(attributes = {})
10
+ @options = attributes.fetch(:options, {})
11
+ @actions = attributes.fetch(:actions, [])
12
+ @action_choice = attributes.fetch(:action_choice, "")
13
+ @instructions = attributes.fetch(:instructions, "")
14
+ @body = attributes.fetch(:body, "")
15
+ @content_type = attributes.fetch(:content_type, "text/plain")
16
+ @message = attributes.fetch(:message, Message.new)
17
+ @messages = attributes.fetch(:messages, [])
18
+ @params = attributes.fetch(:params, {})
19
+ @mime_version = attributes.fetch(:mime_version, "1.0")
20
+ @charset = attributes.fetch(:charset, "UTF-8")
21
+ @context = attributes.fetch(:context, [])
22
+ @headers = attributes.fetch(:headers, {})
23
+ @parts = attributes.fetch(:parts, [])
24
+
25
+ set_message if attributes[:message].is_a?(String) || @body.is_a?(String) && @message.content
26
+ set_messages if @messages.any? || @instructions.present?
27
+ end
28
+
29
+ # Generate the prompt as a string (for debugging or sending to the provider)
30
+ def to_s
31
+ @message.to_s
32
+ end
33
+
34
+ def add_part(part)
35
+ message = Message.new(content: part[:body], role: :user)
36
+ prompt_part = self.class.new(message: message, content: message.content, content_type: part[:content_type], chartset: part[:charset])
37
+
38
+ set_message if @content_type == part[:content_type] && @message.content
39
+
40
+ @parts << prompt_part
41
+ end
42
+
43
+ def multipart?
44
+ @parts.any?
45
+ end
46
+
47
+ def to_h
48
+ {
49
+ actions: @actions,
50
+ action: @action_choice,
51
+ instructions: @instructions,
52
+ message: @message.to_h,
53
+ messages: @messages.map(&:to_h),
54
+ headers: @headers,
55
+ context: @context
56
+ }
57
+ end
58
+
59
+ def headers(headers = {})
60
+ @headers.merge!(headers)
61
+ end
62
+
63
+ private
64
+
65
+ def set_messages
66
+ @messages = [Message.new(content: @instructions, role: :system)] + @messages
67
+ end
68
+
69
+ def set_message
70
+ if @body.is_a?(String) && !@message.content
71
+ @message = Message.new(content: @body, role: :user)
72
+ elsif @message.is_a? String
73
+ @message = Message.new(content: @message, role: :user)
74
+ end
75
+ @messages = [@message]
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,127 @@
1
+ require "abstract_controller"
2
+ require "active_support/core_ext/string/inflections"
3
+
4
+ module ActiveAgent
5
+ module ActionPrompt
6
+ extend ::ActiveSupport::Autoload
7
+
8
+ eager_autoload do
9
+ autoload :Collector
10
+ autoload :Message
11
+ autoload :Prompt
12
+ autoload :PromptHelper
13
+ end
14
+
15
+ autoload :Base
16
+
17
+ extend ActiveSupport::Concern
18
+
19
+ included do
20
+ include AbstractController::Rendering
21
+ include AbstractController::Layouts
22
+ include AbstractController::Helpers
23
+ include AbstractController::Translation
24
+ include AbstractController::AssetPaths
25
+ include AbstractController::Callbacks
26
+ include AbstractController::Caching
27
+
28
+ include ActionView::Layouts
29
+
30
+ helper ActiveAgent::PromptHelper
31
+ # class_attribute :default_params, default: {
32
+ # content_type: "text/plain",
33
+ # parts_order: ["text/plain", "text/html", "application/json"]
34
+ # }.freeze
35
+ end
36
+
37
+ # # def self.prompt(headers = {}, &)
38
+ # # new.prompt(headers, &)
39
+ # # end
40
+
41
+ # def prompt(headers = {}, &block)
42
+ # return @_message if @_prompt_was_called && headers.blank? && !block
43
+
44
+ # headers = apply_defaults(headers)
45
+
46
+ # @_message = ActiveAgent::ActionPrompt::Prompt.new
47
+
48
+ # assign_headers_to_message(@_message, headers)
49
+
50
+ # responses = collect_responses(headers, &block)
51
+
52
+ # @_prompt_was_called = true
53
+
54
+ # create_parts_from_responses(@_message, responses)
55
+
56
+ # @_message
57
+ # end
58
+
59
+ private
60
+
61
+ def apply_defaults(headers)
62
+ headers.reverse_merge(self.class.default_params)
63
+ end
64
+
65
+ def assign_headers_to_message(message, headers)
66
+ assignable = headers.except(:parts_order, :content_type, :body, :template_name, :template_path)
67
+ assignable.each { |k, v| message.send(:"#{k}=", v) }
68
+ end
69
+
70
+ def collect_responses(headers, &block)
71
+ if block
72
+ collect_responses_from_block(headers, &block)
73
+ elsif headers[:body]
74
+ collect_responses_from_text(headers)
75
+ else
76
+ collect_responses_from_templates(headers)
77
+ end
78
+ end
79
+
80
+ def collect_responses_from_block(headers, &block)
81
+ templates_name = headers[:template_name] || action_name
82
+ collector = ActiveAgent::ActionPrompt::Collector.new(lookup_context) { render(templates_name) }
83
+ yield(collector)
84
+ collector.responses
85
+ end
86
+
87
+ def collect_responses_from_text(headers)
88
+ [{
89
+ body: headers.delete(:body),
90
+ content_type: headers[:content_type] || "text/plain"
91
+ }]
92
+ end
93
+
94
+ def collect_responses_from_templates(headers)
95
+ templates_path = headers[:template_path] || self.class.name.sub(/Agent$/, "").underscore
96
+ templates_name = headers[:template_name] || action_name
97
+ each_template(Array(templates_path), templates_name).map do |template|
98
+ format = template.format || formats.first
99
+ {
100
+ body: render(template: template, formats: [format]),
101
+ content_type: Mime[format].to_s
102
+ }
103
+ end
104
+ end
105
+
106
+ def each_template(paths, name, &)
107
+ templates = lookup_context.find_all(name, paths)
108
+ if templates.empty?
109
+ raise ActionView::MissingTemplate.new(paths, name, paths, false, "prompt")
110
+ else
111
+ templates.uniq(&:format).each(&)
112
+ end
113
+ end
114
+
115
+ def create_parts_from_responses(message, responses)
116
+ responses.each do |response|
117
+ message.add_part(response[:body], content_type: response[:content_type])
118
+ end
119
+ end
120
+
121
+ class TestAgent
122
+ class << self
123
+ attr_accessor :generations
124
+ end
125
+ end
126
+ end
127
+ end