solid_agent 0.0.0 → 0.1.1
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 +4 -4
- data/README.md +130 -0
- data/Rakefile +12 -0
- data/lib/generators/solid_agent/agent/agent_generator.rb +95 -0
- data/lib/generators/solid_agent/agent/templates/action.text.erb +10 -0
- data/lib/generators/solid_agent/agent/templates/agent.rb.erb +93 -0
- data/lib/generators/solid_agent/context/context_generator.rb +124 -0
- data/lib/generators/solid_agent/context/templates/context_model.rb.erb +100 -0
- data/lib/generators/solid_agent/context/templates/create_context.rb.erb +32 -0
- data/lib/generators/solid_agent/context/templates/create_generations.rb.erb +38 -0
- data/lib/generators/solid_agent/context/templates/create_messages.rb.erb +33 -0
- data/lib/generators/solid_agent/context/templates/generation_model.rb.erb +40 -0
- data/lib/generators/solid_agent/context/templates/message_model.rb.erb +47 -0
- data/lib/generators/solid_agent/install/install_generator.rb +83 -0
- data/lib/generators/solid_agent/install/templates/agent_context.rb.erb +128 -0
- data/lib/generators/solid_agent/install/templates/agent_generation.rb.erb +59 -0
- data/lib/generators/solid_agent/install/templates/agent_message.rb.erb +76 -0
- data/lib/generators/solid_agent/install/templates/create_agent_contexts.rb.erb +32 -0
- data/lib/generators/solid_agent/install/templates/create_agent_generations.rb.erb +38 -0
- data/lib/generators/solid_agent/install/templates/create_agent_messages.rb.erb +33 -0
- data/lib/generators/solid_agent/install/templates/initializer.rb.erb +51 -0
- data/lib/generators/solid_agent/tool/templates/tool.json.erb +19 -0
- data/lib/generators/solid_agent/tool/tool_generator.rb +117 -0
- data/lib/solid_agent/engine.rb +16 -0
- data/lib/solid_agent/has_context.rb +449 -0
- data/lib/solid_agent/has_tools.rb +257 -0
- data/lib/solid_agent/streams_tool_updates.rb +178 -0
- data/lib/solid_agent/version.rb +5 -0
- data/lib/solid_agent.rb +28 -0
- data/sig/solid_agent.rbs +4 -0
- metadata +88 -14
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 52f173d6c45fc416105fb4a93760dc7aacc81061b75bada9820fce835e9652dd
|
|
4
|
+
data.tar.gz: '07348df42b87220dea84691d61a11cac6e6576cebbcbe2272c702a0bed6f539c'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ffd6386b0d71a95df1ac198c9590bbf337bf7e1a37ccc19e17eb57afbbcf544260fe7e2e552ba8470d4091d636a5b7b074aa5e7745d9d17021af839704b81f46
|
|
7
|
+
data.tar.gz: bcb89c355432829006b38157c727c271b4b63c86aa09a67d14611b16896f515406dfbf5d84f45c1b2b53a9e4adae6d4cfa250397271f3bb0f8e029f15417d5b4
|
data/README.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# SolidAgent
|
|
2
|
+
|
|
3
|
+
SolidAgent extends the [ActiveAgent](https://github.com/activeagents/activeagent) framework with enterprise-grade features for building robust AI agents in Rails applications. It provides three core concerns that add database-backed persistence, declarative tool schemas, and real-time streaming capabilities to your agents.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **HasContext** - Database-backed prompt context management for maintaining conversation history and agent state
|
|
8
|
+
- **HasTools** - Declarative, schema-based tool definitions compatible with LLM function-calling APIs
|
|
9
|
+
- **StreamsToolUpdates** - Real-time UI feedback during tool execution via ActionCable
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
Add this line to your application's Gemfile:
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
gem "solid_agent"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
And then execute:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
$ bundle install
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Or install it yourself as:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
$ gem install solid_agent
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
### Quick Start
|
|
34
|
+
|
|
35
|
+
Generate a new agent with context support:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
$ rails generate solid_agent:agent WritingAssistant --context --context_name conversation --contextable user
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### HasContext - Persistent Conversation History
|
|
42
|
+
|
|
43
|
+
Add database-backed context management to your agents:
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
class WritingAssistantAgent < ApplicationAgent
|
|
47
|
+
include SolidAgent::HasContext
|
|
48
|
+
|
|
49
|
+
has_context :conversation, contextable: :user
|
|
50
|
+
|
|
51
|
+
def improve
|
|
52
|
+
load_conversation(contextable: current_user)
|
|
53
|
+
add_conversation_user_message(params[:message])
|
|
54
|
+
prompt messages: conversation_messages
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
This generates helper methods like:
|
|
60
|
+
- `load_conversation(contextable:)` - Load or create a context
|
|
61
|
+
- `conversation_messages` - Get formatted message history
|
|
62
|
+
- `add_conversation_user_message(content)` - Add a user message
|
|
63
|
+
- `add_conversation_assistant_message(content)` - Add an AI response
|
|
64
|
+
- `conversation_result` - Get the last assistant message
|
|
65
|
+
|
|
66
|
+
### HasTools - Declarative Tool Schemas
|
|
67
|
+
|
|
68
|
+
Define tools inline with a clean DSL:
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
class ResearchAgent < ApplicationAgent
|
|
72
|
+
include SolidAgent::HasTools
|
|
73
|
+
|
|
74
|
+
tool :search do
|
|
75
|
+
description "Search for information"
|
|
76
|
+
parameter :query, type: :string, required: true
|
|
77
|
+
parameter :limit, type: :integer, default: 10
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def research
|
|
81
|
+
prompt tools: tools
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def search(query:, limit: 10)
|
|
85
|
+
# Tool implementation
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Or use JSON templates in `app/views/research_agent/tools/search.json.erb`.
|
|
91
|
+
|
|
92
|
+
### StreamsToolUpdates - Real-Time Feedback
|
|
93
|
+
|
|
94
|
+
Broadcast tool execution status to your UI:
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
class BrowserAgent < ApplicationAgent
|
|
98
|
+
include SolidAgent::HasTools
|
|
99
|
+
include SolidAgent::StreamsToolUpdates
|
|
100
|
+
|
|
101
|
+
has_tools :navigate, :click
|
|
102
|
+
tool_description :navigate, ->(args) { "Visiting #{args[:url]}..." }
|
|
103
|
+
end
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Generators
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
# Generate a new agent
|
|
110
|
+
$ rails generate solid_agent:agent MyAgent
|
|
111
|
+
|
|
112
|
+
# Generate with context support
|
|
113
|
+
$ rails generate solid_agent:agent MyAgent --context --context_name session
|
|
114
|
+
|
|
115
|
+
# Generate a tool template
|
|
116
|
+
$ rails generate solid_agent:tool search MyAgent --parameters query:string:required
|
|
117
|
+
|
|
118
|
+
# Generate context models
|
|
119
|
+
$ rails generate solid_agent:context conversation
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Development
|
|
123
|
+
|
|
124
|
+
After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
125
|
+
|
|
126
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
127
|
+
|
|
128
|
+
## Contributing
|
|
129
|
+
|
|
130
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/solid_agent.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
|
|
5
|
+
module SolidAgent
|
|
6
|
+
module Generators
|
|
7
|
+
class AgentGenerator < Rails::Generators::NamedBase
|
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
|
9
|
+
|
|
10
|
+
desc "Generates a new ActiveAgent agent with SolidAgent concerns"
|
|
11
|
+
|
|
12
|
+
class_option :context, type: :boolean, default: true,
|
|
13
|
+
desc: "Include HasContext concern"
|
|
14
|
+
|
|
15
|
+
class_option :context_name, type: :string, default: nil,
|
|
16
|
+
desc: "Custom context name (e.g., 'conversation', 'research_session')"
|
|
17
|
+
|
|
18
|
+
class_option :contextable, type: :string, default: nil,
|
|
19
|
+
desc: "Param key for auto-context (e.g., 'user', 'document')"
|
|
20
|
+
|
|
21
|
+
class_option :tools, type: :boolean, default: false,
|
|
22
|
+
desc: "Include HasTools concern"
|
|
23
|
+
|
|
24
|
+
class_option :streaming, type: :boolean, default: false,
|
|
25
|
+
desc: "Include StreamsToolUpdates concern"
|
|
26
|
+
|
|
27
|
+
class_option :actions, type: :array, default: ["perform"],
|
|
28
|
+
desc: "Agent actions to generate"
|
|
29
|
+
|
|
30
|
+
class_option :parent, type: :string, default: "ApplicationAgent",
|
|
31
|
+
desc: "Parent class for the agent"
|
|
32
|
+
|
|
33
|
+
def create_agent_file
|
|
34
|
+
@parent_class = options[:parent]
|
|
35
|
+
@include_context = options[:context]
|
|
36
|
+
@context_name = options[:context_name]
|
|
37
|
+
@contextable = options[:contextable]
|
|
38
|
+
@include_tools = options[:tools]
|
|
39
|
+
@include_streaming = options[:streaming]
|
|
40
|
+
@actions = options[:actions]
|
|
41
|
+
|
|
42
|
+
template "agent.rb.erb", "app/agents/#{file_name}_agent.rb"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def create_view_directory
|
|
46
|
+
empty_directory "app/views/#{file_name}_agent"
|
|
47
|
+
|
|
48
|
+
@actions.each do |action|
|
|
49
|
+
template "action.text.erb", "app/views/#{file_name}_agent/#{action}.text.erb",
|
|
50
|
+
action_name: action
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def create_tools_directory
|
|
55
|
+
return unless @include_tools
|
|
56
|
+
|
|
57
|
+
empty_directory "app/views/#{file_name}_agent/tools"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def show_next_steps
|
|
61
|
+
say ""
|
|
62
|
+
say "Agent created successfully!", :green
|
|
63
|
+
say ""
|
|
64
|
+
say "Files generated:"
|
|
65
|
+
say " app/agents/#{file_name}_agent.rb"
|
|
66
|
+
say " app/views/#{file_name}_agent/"
|
|
67
|
+
@actions.each do |action|
|
|
68
|
+
say " app/views/#{file_name}_agent/#{action}.text.erb"
|
|
69
|
+
end
|
|
70
|
+
say " app/views/#{file_name}_agent/tools/" if @include_tools
|
|
71
|
+
say ""
|
|
72
|
+
|
|
73
|
+
if @include_tools
|
|
74
|
+
say "To add tools, run:", :yellow
|
|
75
|
+
say " rails g solid_agent:tool search #{class_name}Agent --parameters query:string:required --description \"Search for content\""
|
|
76
|
+
say ""
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
say "Example usage:", :yellow
|
|
80
|
+
say " #{class_name}Agent.with(content: \"Hello\").#{@actions.first}.generate_now"
|
|
81
|
+
say ""
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def file_name
|
|
87
|
+
name.underscore
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def class_name
|
|
91
|
+
name.camelize
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<%% # Action: <%= config[:action_name] %> %>
|
|
2
|
+
<%% # This template is rendered as the user message for the prompt %>
|
|
3
|
+
|
|
4
|
+
<%% if @content.present? %>
|
|
5
|
+
Please process the following content:
|
|
6
|
+
|
|
7
|
+
<%%= @content %>
|
|
8
|
+
<%% else %>
|
|
9
|
+
Please help me with my request.
|
|
10
|
+
<%% end %>
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class <%= class_name %>Agent < <%= @parent_class %>
|
|
4
|
+
<%- if @include_context || @include_tools || @include_streaming -%>
|
|
5
|
+
# SolidAgent concerns
|
|
6
|
+
<%- end -%>
|
|
7
|
+
<%- if @include_context -%>
|
|
8
|
+
include SolidAgent::HasContext
|
|
9
|
+
<%- end -%>
|
|
10
|
+
<%- if @include_tools -%>
|
|
11
|
+
include SolidAgent::HasTools
|
|
12
|
+
<%- end -%>
|
|
13
|
+
<%- if @include_streaming -%>
|
|
14
|
+
include SolidAgent::StreamsToolUpdates
|
|
15
|
+
<%- end -%>
|
|
16
|
+
|
|
17
|
+
# Configure the generation provider and model
|
|
18
|
+
generate_with :openai, model: "gpt-4o"
|
|
19
|
+
|
|
20
|
+
<%- if @include_context -%>
|
|
21
|
+
# Enable database-backed context persistence
|
|
22
|
+
# Context is auto-created from params[:<%= @contextable || 'contextable' %>]
|
|
23
|
+
<%- if @context_name -%>
|
|
24
|
+
has_context :<%= @context_name %>, contextable: :<%= @contextable || 'contextable' %>
|
|
25
|
+
<%- else -%>
|
|
26
|
+
has_context contextable: :<%= @contextable || 'contextable' %>
|
|
27
|
+
<%- end -%>
|
|
28
|
+
<%- end -%>
|
|
29
|
+
|
|
30
|
+
<%- if @include_tools -%>
|
|
31
|
+
# Declare tools (auto-discover from app/views/<%= file_name %>_agent/tools/*.json.erb)
|
|
32
|
+
# has_tools
|
|
33
|
+
#
|
|
34
|
+
# Or declare specific tools:
|
|
35
|
+
# has_tools :search, :analyze
|
|
36
|
+
#
|
|
37
|
+
# Or define inline:
|
|
38
|
+
# tool :example do
|
|
39
|
+
# description "An example tool"
|
|
40
|
+
# parameter :input, type: :string, required: true
|
|
41
|
+
# end
|
|
42
|
+
<%- end -%>
|
|
43
|
+
|
|
44
|
+
<%- if @include_streaming -%>
|
|
45
|
+
# Streaming callbacks for real-time updates
|
|
46
|
+
on_stream :broadcast_chunk
|
|
47
|
+
on_stream_close :broadcast_complete
|
|
48
|
+
|
|
49
|
+
# Tool descriptions for UI feedback
|
|
50
|
+
# tool_description :search, ->(args) { "Searching for '#{args[:query]}'..." }
|
|
51
|
+
<%- end -%>
|
|
52
|
+
|
|
53
|
+
<%- @actions.each do |action| -%>
|
|
54
|
+
# Action: <%= action %>
|
|
55
|
+
def <%= action %>
|
|
56
|
+
<%- if @include_tools -%>
|
|
57
|
+
# Include tools in the prompt (context auto-created)
|
|
58
|
+
prompt(tools: tools, tool_choice: "auto")
|
|
59
|
+
<%- else -%>
|
|
60
|
+
# Render the action template (context auto-created)
|
|
61
|
+
prompt
|
|
62
|
+
<%- end -%>
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
<%- end -%>
|
|
66
|
+
<%- if @include_streaming -%>
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def broadcast_chunk(chunk)
|
|
70
|
+
return unless chunk.delta
|
|
71
|
+
return unless params[:stream_id]
|
|
72
|
+
|
|
73
|
+
ActionCable.server.broadcast(params[:stream_id], { content: chunk.delta })
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def broadcast_complete(chunk)
|
|
77
|
+
return unless params[:stream_id]
|
|
78
|
+
|
|
79
|
+
ActionCable.server.broadcast(params[:stream_id], { done: true })
|
|
80
|
+
end
|
|
81
|
+
<%- end -%>
|
|
82
|
+
<%- if @include_tools && !@include_streaming -%>
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
# Tool implementations go here
|
|
86
|
+
# def search(query:)
|
|
87
|
+
# Rails.logger.info "[<%= class_name %>Agent] Tool called: search(#{query})"
|
|
88
|
+
# { success: true, results: [] }
|
|
89
|
+
# rescue => e
|
|
90
|
+
# { success: false, error: e.message }
|
|
91
|
+
# end
|
|
92
|
+
<%- end -%>
|
|
93
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/active_record"
|
|
5
|
+
|
|
6
|
+
module SolidAgent
|
|
7
|
+
module Generators
|
|
8
|
+
class ContextGenerator < Rails::Generators::NamedBase
|
|
9
|
+
include ActiveRecord::Generators::Migration
|
|
10
|
+
|
|
11
|
+
source_root File.expand_path("templates", __dir__)
|
|
12
|
+
|
|
13
|
+
desc "Generates a custom context model with migrations for domain-specific agent contexts"
|
|
14
|
+
|
|
15
|
+
argument :agent_name, type: :string, required: false,
|
|
16
|
+
desc: "The agent class name that will use this context (optional)"
|
|
17
|
+
|
|
18
|
+
class_option :skip_migrations, type: :boolean, default: false,
|
|
19
|
+
desc: "Skip generating migrations"
|
|
20
|
+
|
|
21
|
+
def create_migrations
|
|
22
|
+
return if options[:skip_migrations]
|
|
23
|
+
|
|
24
|
+
migration_template "create_context.rb.erb",
|
|
25
|
+
"db/migrate/create_#{table_name}.rb"
|
|
26
|
+
|
|
27
|
+
migration_template "create_messages.rb.erb",
|
|
28
|
+
"db/migrate/create_#{message_table_name}.rb"
|
|
29
|
+
|
|
30
|
+
migration_template "create_generations.rb.erb",
|
|
31
|
+
"db/migrate/create_#{generation_table_name}.rb"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def create_models
|
|
35
|
+
template "context_model.rb.erb", "app/models/#{file_name}.rb"
|
|
36
|
+
template "message_model.rb.erb", "app/models/#{message_file_name}.rb"
|
|
37
|
+
template "generation_model.rb.erb", "app/models/#{generation_file_name}.rb"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def show_usage
|
|
41
|
+
say ""
|
|
42
|
+
say "Context models created!", :green
|
|
43
|
+
say ""
|
|
44
|
+
say "Files generated:"
|
|
45
|
+
say " app/models/#{file_name}.rb"
|
|
46
|
+
say " app/models/#{message_file_name}.rb"
|
|
47
|
+
say " app/models/#{generation_file_name}.rb"
|
|
48
|
+
say " db/migrate/*_create_#{table_name}.rb"
|
|
49
|
+
say " db/migrate/*_create_#{message_table_name}.rb"
|
|
50
|
+
say " db/migrate/*_create_#{generation_table_name}.rb"
|
|
51
|
+
say ""
|
|
52
|
+
say "Next steps:", :yellow
|
|
53
|
+
say " 1. Run migrations: rails db:migrate"
|
|
54
|
+
say ""
|
|
55
|
+
say " 2. Add to your agent:"
|
|
56
|
+
say " class #{agent_class_name} < ApplicationAgent"
|
|
57
|
+
say " include SolidAgent::HasContext"
|
|
58
|
+
say " has_context :#{context_name}"
|
|
59
|
+
say ""
|
|
60
|
+
say " def perform"
|
|
61
|
+
say " create_#{context_name}(contextable: params[:user])"
|
|
62
|
+
say " prompt"
|
|
63
|
+
say " end"
|
|
64
|
+
say " end"
|
|
65
|
+
say ""
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def context_name
|
|
71
|
+
name.underscore.singularize
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def class_name
|
|
75
|
+
name.camelize
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def file_name
|
|
79
|
+
name.underscore
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def table_name
|
|
83
|
+
name.underscore.pluralize
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def message_class_name
|
|
87
|
+
"#{class_name}Message"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def message_file_name
|
|
91
|
+
"#{file_name}_message"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def message_table_name
|
|
95
|
+
"#{file_name}_messages"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def generation_class_name
|
|
99
|
+
"#{class_name}Generation"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def generation_file_name
|
|
103
|
+
"#{file_name}_generation"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def generation_table_name
|
|
107
|
+
"#{file_name}_generations"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def agent_class_name
|
|
111
|
+
agent_name&.camelize || "MyAgent"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def migration_version
|
|
115
|
+
"[#{ActiveRecord::Migration.current_version}]"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def self.next_migration_number(dirname)
|
|
119
|
+
next_migration_number = current_migration_number(dirname) + 1
|
|
120
|
+
ActiveRecord::Migration.next_migration_number(next_migration_number)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# <%= class_name %> stores context for <%= context_name %>-based agent interactions.
|
|
4
|
+
#
|
|
5
|
+
# @example Creating a <%= context_name %> for a user
|
|
6
|
+
# <%= context_name %> = <%= class_name %>.create!(
|
|
7
|
+
# contextable: current_user,
|
|
8
|
+
# agent_name: "MyAgent",
|
|
9
|
+
# action_name: "perform"
|
|
10
|
+
# )
|
|
11
|
+
#
|
|
12
|
+
# @example Using with has_context
|
|
13
|
+
# class MyAgent < ApplicationAgent
|
|
14
|
+
# include SolidAgent::HasContext
|
|
15
|
+
# has_context :<%= context_name %>
|
|
16
|
+
#
|
|
17
|
+
# def perform
|
|
18
|
+
# create_<%= context_name %>(contextable: params[:user])
|
|
19
|
+
# prompt
|
|
20
|
+
# end
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
class <%= class_name %> < ApplicationRecord
|
|
24
|
+
# Associations
|
|
25
|
+
belongs_to :contextable, polymorphic: true, optional: true
|
|
26
|
+
has_many :messages, class_name: "<%= message_class_name %>", dependent: :destroy
|
|
27
|
+
has_many :generations, class_name: "<%= generation_class_name %>", dependent: :destroy
|
|
28
|
+
|
|
29
|
+
# Validations
|
|
30
|
+
validates :agent_name, presence: true
|
|
31
|
+
validates :action_name, presence: true
|
|
32
|
+
|
|
33
|
+
# Scopes
|
|
34
|
+
scope :recent, -> { order(created_at: :desc) }
|
|
35
|
+
scope :for_agent, ->(name) { where(agent_name: name) }
|
|
36
|
+
scope :for_action, ->(name) { where(action_name: name) }
|
|
37
|
+
|
|
38
|
+
# Convenience method to get input_params from options
|
|
39
|
+
def input_params
|
|
40
|
+
options&.dig("input_params") || options&.dig(:input_params) || {}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Records a generation response and updates token counts
|
|
44
|
+
def record_generation!(response)
|
|
45
|
+
generation = generations.create!(
|
|
46
|
+
content: response.message&.content,
|
|
47
|
+
model: response.model,
|
|
48
|
+
provider: response.provider,
|
|
49
|
+
finish_reason: response.finish_reason,
|
|
50
|
+
input_tokens: response.usage&.input_tokens || 0,
|
|
51
|
+
output_tokens: response.usage&.output_tokens || 0,
|
|
52
|
+
tool_calls: extract_tool_calls(response),
|
|
53
|
+
raw_response: response.raw_response,
|
|
54
|
+
duration_seconds: response.duration
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
increment!(:total_input_tokens, generation.input_tokens)
|
|
58
|
+
increment!(:total_output_tokens, generation.output_tokens)
|
|
59
|
+
|
|
60
|
+
add_assistant_message(response.message&.content, tool_calls: generation.tool_calls)
|
|
61
|
+
|
|
62
|
+
generation
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def add_user_message(content, **attributes)
|
|
66
|
+
messages.create!(role: "user", content: content, **attributes)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def add_assistant_message(content, **attributes)
|
|
70
|
+
messages.create!(role: "assistant", content: content, **attributes)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def add_system_message(content)
|
|
74
|
+
messages.create!(role: "system", content: content)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def add_tool_message(tool_call_id:, tool_name:, result:)
|
|
78
|
+
messages.create!(
|
|
79
|
+
role: "tool",
|
|
80
|
+
tool_call_id: tool_call_id,
|
|
81
|
+
tool_name: tool_name,
|
|
82
|
+
tool_result: result,
|
|
83
|
+
content: result.is_a?(String) ? result : result.to_json
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def total_tokens
|
|
88
|
+
total_input_tokens + total_output_tokens
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def extract_tool_calls(response)
|
|
94
|
+
return [] unless response.message&.tool_calls.present?
|
|
95
|
+
|
|
96
|
+
response.message.tool_calls.map do |tc|
|
|
97
|
+
{ id: tc.id, name: tc.name, arguments: tc.arguments }
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Create<%= class_name.pluralize %> < ActiveRecord::Migration<%= migration_version %>
|
|
4
|
+
def change
|
|
5
|
+
create_table :<%= table_name %> do |t|
|
|
6
|
+
# Polymorphic association to any model (User, Document, Project, etc.)
|
|
7
|
+
t.references :contextable, polymorphic: true, index: true
|
|
8
|
+
|
|
9
|
+
# Agent identification
|
|
10
|
+
t.string :agent_name, null: false
|
|
11
|
+
t.string :action_name, null: false
|
|
12
|
+
|
|
13
|
+
# System instructions for the conversation
|
|
14
|
+
t.text :instructions
|
|
15
|
+
|
|
16
|
+
# Flexible options storage (input_params, model settings, etc.)
|
|
17
|
+
t.jsonb :options, default: {}
|
|
18
|
+
|
|
19
|
+
# Tracing/debugging
|
|
20
|
+
t.string :trace_id, index: true
|
|
21
|
+
|
|
22
|
+
# Token usage tracking
|
|
23
|
+
t.integer :total_input_tokens, default: 0
|
|
24
|
+
t.integer :total_output_tokens, default: 0
|
|
25
|
+
|
|
26
|
+
t.timestamps
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
add_index :<%= table_name %>, [:agent_name, :action_name]
|
|
30
|
+
add_index :<%= table_name %>, :created_at
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Create<%= generation_class_name.pluralize %> < ActiveRecord::Migration<%= migration_version %>
|
|
4
|
+
def change
|
|
5
|
+
create_table :<%= generation_table_name %> do |t|
|
|
6
|
+
t.references :<%= file_name %>, null: false, foreign_key: true, index: true
|
|
7
|
+
|
|
8
|
+
# The assistant's response content
|
|
9
|
+
t.text :content
|
|
10
|
+
|
|
11
|
+
# Model information
|
|
12
|
+
t.string :model
|
|
13
|
+
t.string :provider
|
|
14
|
+
|
|
15
|
+
# Finish reason: stop, tool_calls, length, etc.
|
|
16
|
+
t.string :finish_reason
|
|
17
|
+
|
|
18
|
+
# Token usage for this generation
|
|
19
|
+
t.integer :input_tokens, default: 0
|
|
20
|
+
t.integer :output_tokens, default: 0
|
|
21
|
+
|
|
22
|
+
# Tool calls made in this generation
|
|
23
|
+
t.jsonb :tool_calls, default: []
|
|
24
|
+
|
|
25
|
+
# Raw response from the provider
|
|
26
|
+
t.jsonb :raw_response
|
|
27
|
+
|
|
28
|
+
# Timing
|
|
29
|
+
t.float :duration_seconds
|
|
30
|
+
|
|
31
|
+
t.timestamps
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
add_index :<%= generation_table_name %>, :model
|
|
35
|
+
add_index :<%= generation_table_name %>, :finish_reason
|
|
36
|
+
add_index :<%= generation_table_name %>, [:<%= file_name %>_id, :created_at]
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Create<%= message_class_name.pluralize %> < ActiveRecord::Migration<%= migration_version %>
|
|
4
|
+
def change
|
|
5
|
+
create_table :<%= message_table_name %> do |t|
|
|
6
|
+
t.references :<%= file_name %>, null: false, foreign_key: true, index: true
|
|
7
|
+
|
|
8
|
+
# Message role: user, assistant, system, tool
|
|
9
|
+
t.string :role, null: false
|
|
10
|
+
|
|
11
|
+
# Message content
|
|
12
|
+
t.text :content
|
|
13
|
+
|
|
14
|
+
# For tool calls and results
|
|
15
|
+
t.string :tool_call_id
|
|
16
|
+
t.string :tool_name
|
|
17
|
+
t.jsonb :tool_arguments, default: {}
|
|
18
|
+
t.jsonb :tool_result
|
|
19
|
+
|
|
20
|
+
# For multimodal content
|
|
21
|
+
t.jsonb :attachments, default: []
|
|
22
|
+
|
|
23
|
+
# Metadata
|
|
24
|
+
t.jsonb :metadata, default: {}
|
|
25
|
+
|
|
26
|
+
t.timestamps
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
add_index :<%= message_table_name %>, :role
|
|
30
|
+
add_index :<%= message_table_name %>, :tool_call_id
|
|
31
|
+
add_index :<%= message_table_name %>, [:<%= file_name %>_id, :created_at]
|
|
32
|
+
end
|
|
33
|
+
end
|