active_agent_rails 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../provider"
4
+
5
+ module ActiveAgent
6
+ module Provider
7
+ class OpenAI < Base
8
+ def chat(messages, tools: [], &block)
9
+ api_model = model || "gpt-4o-mini"
10
+ url = "https://api.openai.com/v1/chat/completions"
11
+
12
+ body = {
13
+ model: api_model,
14
+ messages: format_messages(messages)
15
+ }
16
+
17
+ if tools.any?
18
+ body[:tools] = format_tools(tools)
19
+ end
20
+
21
+ headers = {
22
+ "Content-Type" => "application/json",
23
+ "Authorization" => "Bearer #{api_key}"
24
+ }
25
+
26
+ if block_given?
27
+ body[:stream] = true
28
+ text_accumulator = ""
29
+ tool_calls_accumulator = {}
30
+
31
+ post_request(url, headers, body) do |line|
32
+ next unless line.start_with?("data:")
33
+ json_str = line.sub("data:", "").strip
34
+ next if json_str == "[DONE]"
35
+ next if json_str.empty?
36
+
37
+ begin
38
+ chunk = JSON.parse(json_str)
39
+ choice = chunk.dig("choices", 0)
40
+ next unless choice
41
+
42
+ delta = choice["delta"]
43
+ next unless delta
44
+
45
+ if delta["content"]
46
+ text_accumulator += delta["content"]
47
+ yield(delta["content"])
48
+ end
49
+
50
+ if delta["tool_calls"]
51
+ delta["tool_calls"].each do |tc|
52
+ index = tc["index"]
53
+ tool_calls_accumulator[index] ||= { id: "", name: "", arguments_str: "" }
54
+
55
+ tool_calls_accumulator[index][:id] = tc["id"] if tc["id"]
56
+ tool_calls_accumulator[index][:name] = tc.dig("function", "name") if tc.dig("function", "name")
57
+
58
+ if tc.dig("function", "arguments")
59
+ tool_calls_accumulator[index][:arguments_str] += tc.dig("function", "arguments")
60
+ end
61
+ end
62
+ end
63
+ rescue JSON::ParserError
64
+ # Ignore json parse errors for incomplete lines
65
+ end
66
+ end
67
+
68
+ # Format accumulated tool calls back to standard structure
69
+ tool_calls = tool_calls_accumulator.values.map do |tc|
70
+ args = {}
71
+ begin
72
+ args = JSON.parse(tc[:arguments_str], symbolize_names: true) unless tc[:arguments_str].empty?
73
+ rescue JSON::ParserError
74
+ ActiveAgent.logger.warn("OpenAI could not parse streaming arguments JSON: #{tc[:arguments_str]}")
75
+ end
76
+
77
+ {
78
+ id: tc[:id],
79
+ name: tc[:name],
80
+ args: args
81
+ }
82
+ end
83
+
84
+ result = { role: "assistant" }
85
+ result[:content] = text_accumulator unless text_accumulator.empty?
86
+ result[:tool_calls] = tool_calls if tool_calls.any?
87
+ result
88
+ else
89
+ response_json = post_request(url, headers, body)
90
+ parse_response(response_json)
91
+ end
92
+ end
93
+
94
+ def format_tools(tools)
95
+ tools.map do |tool|
96
+ properties = {}
97
+ tool.parameters.each do |name, info|
98
+ properties[name] = {
99
+ type: Tool.map_type(info[:type], uppercase: false),
100
+ description: info[:description]
101
+ }
102
+ end
103
+
104
+ {
105
+ type: "function",
106
+ function: {
107
+ name: tool.name,
108
+ description: tool.description,
109
+ parameters: {
110
+ type: "object",
111
+ properties: properties,
112
+ required: tool.required_parameters
113
+ }
114
+ }
115
+ }
116
+ end
117
+ end
118
+
119
+ private
120
+
121
+ def format_messages(messages)
122
+ messages.map do |msg|
123
+ formatted = {
124
+ role: msg[:role].to_s,
125
+ content: msg[:content]
126
+ }
127
+
128
+ if msg[:role].to_s == "assistant" && msg[:tool_calls]&.any?
129
+ formatted[:tool_calls] = msg[:tool_calls].map do |tc|
130
+ {
131
+ id: tc[:id] || tc[:name],
132
+ type: "function",
133
+ function: {
134
+ name: tc[:name],
135
+ arguments: tc[:args].to_json
136
+ }
137
+ }
138
+ end
139
+ formatted[:content] = nil # OpenAI requires content to be nil if tool_calls is present
140
+ elsif msg[:role].to_s == "tool"
141
+ formatted[:tool_call_id] = msg[:tool_call_id] || msg[:name]
142
+ formatted[:name] = msg[:name]
143
+ end
144
+
145
+ formatted
146
+ end
147
+ end
148
+
149
+ def parse_response(response)
150
+ choice = response.dig("choices", 0, "message")
151
+ return { role: "assistant", content: "Error: No response from model." } unless choice
152
+
153
+ result = { role: "assistant" }
154
+ result[:content] = choice["content"] if choice["content"]
155
+
156
+ if choice["tool_calls"]
157
+ result[:tool_calls] = choice["tool_calls"].map do |tc|
158
+ args_json = tc.dig("function", "arguments")
159
+ args = {}
160
+ begin
161
+ args = JSON.parse(args_json, symbolize_names: true) if args_json
162
+ rescue JSON::ParserError
163
+ ActiveAgent.logger.warn("OpenAI could not parse arguments JSON: #{args_json}")
164
+ end
165
+
166
+ {
167
+ id: tc["id"],
168
+ name: tc.dig("function", "name"),
169
+ args: args
170
+ }
171
+ end
172
+ end
173
+
174
+ result
175
+ end
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAgent
4
+ class Tool
5
+ attr_reader :name, :description, :parameters, :required_parameters
6
+
7
+ def initialize(name, description: "", &block)
8
+ @name = name.to_s
9
+ @description = description
10
+ @parameters = {}
11
+ @required_parameters = []
12
+ instance_eval(&block) if block_given?
13
+ end
14
+
15
+ def parameter(param_name, type: :string, description: "", required: true)
16
+ @parameters[param_name.to_s] = {
17
+ type: type,
18
+ description: description
19
+ }
20
+ @required_parameters << param_name.to_s if required
21
+ end
22
+
23
+ # Translates ruby types to API-specific type strings
24
+ def self.map_type(type, uppercase: false)
25
+ mapped = case type.to_sym
26
+ when :string then "string"
27
+ when :integer then "integer"
28
+ when :number, :float then "number"
29
+ when :boolean then "boolean"
30
+ when :array then "array"
31
+ when :object then "object"
32
+ else "string"
33
+ end
34
+ uppercase ? mapped.upcase : mapped
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAgent
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support"
4
+ require "active_support/core_ext"
5
+
6
+ require_relative "active_agent/version"
7
+ require_relative "active_agent/configuration"
8
+ require_relative "active_agent/tool"
9
+ require_relative "active_agent/memory/base"
10
+ require_relative "active_agent/provider"
11
+ require_relative "active_agent/base"
12
+
13
+ if defined?(Rails)
14
+ require_relative "active_agent/engine"
15
+ end
16
+
17
+ module ActiveAgent
18
+ # Global helper to resolve an agent class by name or symbol
19
+ def self.find_agent(name)
20
+ agent_class_name = "#{name.to_s.camelize}Agent"
21
+ agent_class_name.constantize
22
+ rescue NameError
23
+ raise "Could not find agent class: #{agent_class_name}. Ensure it is defined in app/agents/."
24
+ end
25
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "active_agent"
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module ActiveAgent
6
+ module Generators
7
+ class AgentGenerator < ::Rails::Generators::NamedBase
8
+ source_root File.expand_path("templates", __dir__)
9
+
10
+ def create_agent_file
11
+ template "agent.rb.erb", "app/agents/#{file_name}_agent.rb"
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= class_name %>Agent < ActiveAgent::Base
4
+ # Configure provider and model (defaults to configuration settings if omitted)
5
+ provider :gemini # Options: :gemini, :openai, :anthropic
6
+ model "gemini-2.5-flash"
7
+
8
+ system_prompt "You are a helpful assistant."
9
+
10
+ # Expose tools to the agent using the `tool` DSL.
11
+ # Example:
12
+ #
13
+ # tool :get_current_time, description: "Retrieve the current server time and timezone" do
14
+ # # Parameters are documented for the LLM
15
+ # parameter :format, type: :string, description: "Optional strftime format", required: false
16
+ # end
17
+ #
18
+ # # Define the tool implementation as a standard Ruby instance method:
19
+ # def get_current_time(format: "%Y-%m-%d %H:%M:%S %Z")
20
+ # Time.current.strftime(format)
21
+ # end
22
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/migration"
5
+
6
+ module ActiveAgent
7
+ module Generators
8
+ class InstallGenerator < ::Rails::Generators::Base
9
+ include ::Rails::Generators::Migration
10
+
11
+ source_root File.expand_path("templates", __dir__)
12
+
13
+ def self.next_migration_number(path)
14
+ next_num = Time.now.utc.strftime("%Y%m%d%H%M%S")
15
+ begin
16
+ ActiveRecord::Generators::Base.next_migration_number(path)
17
+ rescue StandardError
18
+ next_num
19
+ end
20
+ end
21
+
22
+ def create_initializer
23
+ template "active_agent.rb", "config/initializers/active_agent.rb"
24
+ end
25
+
26
+ def create_migration_file
27
+ migration_template "create_active_agent_messages.rb", "db/migrate/create_active_agent_messages.rb"
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ ActiveAgent.configure do |config|
4
+ # Default provider: :gemini, :openai, or :anthropic
5
+ config.default_provider = :gemini
6
+
7
+ # API Keys for providers (recommend storing in Rails credentials or environment variables)
8
+ config.gemini_api_key = ENV["GEMINI_API_KEY"]
9
+ config.openai_api_key = ENV["OPENAI_API_KEY"]
10
+ config.anthropic_api_key = ENV["ANTHROPIC_API_KEY"]
11
+
12
+ # Memory store for chat conversations: :in_memory or :active_record
13
+ # Note: If using :active_record, ensure you run: rails db:migrate
14
+ config.memory_store = :active_record
15
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateActiveAgentMessages < ActiveRecord::Migration[<%= ActiveRecord::VERSION::MAJOR %>.<%= ActiveRecord::VERSION::MINOR %>]
4
+ def change
5
+ create_table :active_agent_messages do |t|
6
+ t.string :conversation_id, null: false
7
+ t.string :role, null: false
8
+ t.text :content
9
+ t.string :tool_call_id
10
+ t.string :name
11
+ t.text :tool_calls # serialized JSON array
12
+
13
+ t.timestamps
14
+ end
15
+
16
+ add_index :active_agent_messages, :conversation_id
17
+ end
18
+ end
metadata ADDED
@@ -0,0 +1,144 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: active_agent_rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Shiboshree Roy
8
+ - Antigravity AI
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 1980-01-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: railties
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 6.0.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 6.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 6.0.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 6.0.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: activerecord
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 6.0.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 6.0.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: actionpack
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 6.0.0
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 6.0.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: actionview
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: 6.0.0
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: 6.0.0
83
+ description: ActiveAgent provides a clean, unified interface to build AI agents with
84
+ automatic tool/function calling, ActiveRecord-backed conversation memory, and an
85
+ engine to stream real-time responses to a floating UI widget in your views.
86
+ email:
87
+ - support@example.com
88
+ executables: []
89
+ extensions: []
90
+ extra_rdoc_files: []
91
+ files:
92
+ - Gemfile
93
+ - LICENSE.txt
94
+ - README.md
95
+ - Rakefile
96
+ - app/controllers/active_agent/chats_controller.rb
97
+ - app/helpers/active_agent/chat_helper.rb
98
+ - config/routes.rb
99
+ - lib/active_agent.rb
100
+ - lib/active_agent/base.rb
101
+ - lib/active_agent/configuration.rb
102
+ - lib/active_agent/engine.rb
103
+ - lib/active_agent/memory/active_record.rb
104
+ - lib/active_agent/memory/base.rb
105
+ - lib/active_agent/memory/in_memory.rb
106
+ - lib/active_agent/provider.rb
107
+ - lib/active_agent/providers/anthropic.rb
108
+ - lib/active_agent/providers/gemini.rb
109
+ - lib/active_agent/providers/openai.rb
110
+ - lib/active_agent/tool.rb
111
+ - lib/active_agent/version.rb
112
+ - lib/active_agent_rails.rb
113
+ - lib/generators/active_agent/agent/agent_generator.rb
114
+ - lib/generators/active_agent/agent/templates/agent.rb.erb
115
+ - lib/generators/active_agent/install/install_generator.rb
116
+ - lib/generators/active_agent/install/templates/active_agent.rb
117
+ - lib/generators/active_agent/install/templates/create_active_agent_messages.rb
118
+ homepage: https://github.com/active-agent/active_agent_rails
119
+ licenses:
120
+ - MIT
121
+ metadata:
122
+ allowed_push_host: https://rubygems.org
123
+ source_code_uri: https://github.com/active-agent/active_agent_rails
124
+ changelog_uri: https://github.com/active-agent/active_agent_rails/releases
125
+ bug_tracker_uri: https://github.com/active-agent/active_agent_rails/issues
126
+ rdoc_options: []
127
+ require_paths:
128
+ - lib
129
+ required_ruby_version: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - ">="
132
+ - !ruby/object:Gem::Version
133
+ version: 3.0.0
134
+ required_rubygems_version: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ requirements: []
140
+ rubygems_version: 4.0.11
141
+ specification_version: 4
142
+ summary: A Rails-like framework to integrate LLMs, AI agents, and custom tools into
143
+ Ruby on Rails applications easily.
144
+ test_files: []