solid_agent 0.0.0 → 0.2.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.
Files changed (103) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +68 -0
  3. data/LICENSE +21 -0
  4. data/README.md +321 -0
  5. data/Rakefile +32 -0
  6. data/docs/agent-md-spec.md +803 -0
  7. data/docs/parser-design.md +1369 -0
  8. data/docs/registry-api.md +882 -0
  9. data/examples/README.md +60 -0
  10. data/examples/manifests/changelog_writer.agent.md +81 -0
  11. data/examples/manifests/usage.rb +96 -0
  12. data/examples/memory_handoff/app/agents/researcher_agent.rb +36 -0
  13. data/examples/memory_handoff/app/agents/writer_agent.rb +41 -0
  14. data/examples/memory_handoff/usage.rb +45 -0
  15. data/examples/persistent_conversation/app/agents/support_agent.rb +59 -0
  16. data/examples/persistent_conversation/app/controllers/support_conversations_controller.rb +24 -0
  17. data/examples/persistent_conversation/app/views/agents/support/instructions.md.erb +8 -0
  18. data/examples/persistent_conversation/usage.rb +51 -0
  19. data/examples/reasoning/app/agents/analysis_agent.rb +52 -0
  20. data/examples/reasoning/usage.rb +52 -0
  21. data/examples/run_tracking/app/agents/report_agent.rb +30 -0
  22. data/examples/run_tracking/app/controllers/agent_runs_controller.rb +43 -0
  23. data/examples/run_tracking/app/jobs/document_analysis_job.rb +17 -0
  24. data/examples/run_tracking/app/services/document_analysis_run.rb +68 -0
  25. data/examples/run_tracking/usage.rb +85 -0
  26. data/examples/tool_streaming/app/agents/browser_agent.rb +65 -0
  27. data/examples/tool_streaming/app/channels/tool_status_channel.rb +24 -0
  28. data/examples/tool_streaming/app/views/browser_agent/tools/fetch_url.json.erb +15 -0
  29. data/examples/tool_streaming/usage.rb +47 -0
  30. data/lib/generators/solid_agent/agent/agent_generator.rb +95 -0
  31. data/lib/generators/solid_agent/agent/templates/action.text.erb +10 -0
  32. data/lib/generators/solid_agent/agent/templates/agent.rb.erb +93 -0
  33. data/lib/generators/solid_agent/context/context_generator.rb +124 -0
  34. data/lib/generators/solid_agent/context/templates/context_model.rb.erb +134 -0
  35. data/lib/generators/solid_agent/context/templates/create_context.rb.erb +32 -0
  36. data/lib/generators/solid_agent/context/templates/create_generations.rb.erb +46 -0
  37. data/lib/generators/solid_agent/context/templates/create_messages.rb.erb +37 -0
  38. data/lib/generators/solid_agent/context/templates/generation_model.rb.erb +51 -0
  39. data/lib/generators/solid_agent/context/templates/message_model.rb.erb +47 -0
  40. data/lib/generators/solid_agent/install/install_generator.rb +92 -0
  41. data/lib/generators/solid_agent/install/templates/agent_context.rb.erb +171 -0
  42. data/lib/generators/solid_agent/install/templates/agent_generation.rb.erb +76 -0
  43. data/lib/generators/solid_agent/install/templates/agent_memory.rb.erb +51 -0
  44. data/lib/generators/solid_agent/install/templates/agent_memory_entry.rb.erb +12 -0
  45. data/lib/generators/solid_agent/install/templates/agent_message.rb.erb +76 -0
  46. data/lib/generators/solid_agent/install/templates/agent_run.rb.erb +122 -0
  47. data/lib/generators/solid_agent/install/templates/create_agent_contexts.rb.erb +32 -0
  48. data/lib/generators/solid_agent/install/templates/create_agent_generations.rb.erb +51 -0
  49. data/lib/generators/solid_agent/install/templates/create_agent_memories.rb.erb +35 -0
  50. data/lib/generators/solid_agent/install/templates/create_agent_messages.rb.erb +38 -0
  51. data/lib/generators/solid_agent/install/templates/create_agent_runs.rb.erb +46 -0
  52. data/lib/generators/solid_agent/install/templates/initializer.rb.erb +51 -0
  53. data/lib/generators/solid_agent/manifest/manifest_generator.rb +209 -0
  54. data/lib/generators/solid_agent/manifest/templates/agent.md.erb +39 -0
  55. data/lib/generators/solid_agent/manifest/templates/prompt.erb +13 -0
  56. data/lib/generators/solid_agent/reasons/reasons_generator.rb +83 -0
  57. data/lib/generators/solid_agent/reasons/templates/add_reasoning_columns.rb.erb +12 -0
  58. data/lib/generators/solid_agent/tool/templates/tool.json.erb +19 -0
  59. data/lib/generators/solid_agent/tool/tool_generator.rb +117 -0
  60. data/lib/solid_agent/agent_manifest/agent_builder.rb +323 -0
  61. data/lib/solid_agent/agent_manifest/errors.rb +26 -0
  62. data/lib/solid_agent/agent_manifest/exporter_registry.rb +117 -0
  63. data/lib/solid_agent/agent_manifest/exporters/agent_md_exporter.rb +115 -0
  64. data/lib/solid_agent/agent_manifest/exporters/base_exporter.rb +152 -0
  65. data/lib/solid_agent/agent_manifest/exporters/crewai_exporter.rb +125 -0
  66. data/lib/solid_agent/agent_manifest/exporters/dotprompt_exporter.rb +92 -0
  67. data/lib/solid_agent/agent_manifest/input_schema.rb +154 -0
  68. data/lib/solid_agent/agent_manifest/manifest.rb +306 -0
  69. data/lib/solid_agent/agent_manifest/parser_registry.rb +185 -0
  70. data/lib/solid_agent/agent_manifest/parsers/agent_md_parser.rb +87 -0
  71. data/lib/solid_agent/agent_manifest/parsers/base_parser.rb +223 -0
  72. data/lib/solid_agent/agent_manifest/parsers/crewai_parser.rb +201 -0
  73. data/lib/solid_agent/agent_manifest/parsers/dotprompt_parser.rb +122 -0
  74. data/lib/solid_agent/agent_manifest/parsers/github_prompt_parser.rb +143 -0
  75. data/lib/solid_agent/agent_manifest/picoschema.rb +254 -0
  76. data/lib/solid_agent/agent_manifest/registry/auth.rb +103 -0
  77. data/lib/solid_agent/agent_manifest/registry/client.rb +384 -0
  78. data/lib/solid_agent/agent_manifest/resource.rb +103 -0
  79. data/lib/solid_agent/agent_manifest/tool.rb +160 -0
  80. data/lib/solid_agent/agent_manifest/validator.rb +368 -0
  81. data/lib/solid_agent/agent_manifest.rb +381 -0
  82. data/lib/solid_agent/engine.rb +16 -0
  83. data/lib/solid_agent/has_context.rb +670 -0
  84. data/lib/solid_agent/has_memory.rb +136 -0
  85. data/lib/solid_agent/has_reasons.rb +230 -0
  86. data/lib/solid_agent/has_tools.rb +257 -0
  87. data/lib/solid_agent/model_naming.rb +42 -0
  88. data/lib/solid_agent/model_pricing.rb +93 -0
  89. data/lib/solid_agent/reasonable/reason.rb +205 -0
  90. data/lib/solid_agent/reasonable.rb +181 -0
  91. data/lib/solid_agent/records/agent.rb +520 -0
  92. data/lib/solid_agent/records/agent_run.rb +520 -0
  93. data/lib/solid_agent/records/agent_template.rb +142 -0
  94. data/lib/solid_agent/records/agent_version.rb +141 -0
  95. data/lib/solid_agent/records/ownable.rb +130 -0
  96. data/lib/solid_agent/records.rb +152 -0
  97. data/lib/solid_agent/run_fingerprint.rb +51 -0
  98. data/lib/solid_agent/streams_tool_updates.rb +178 -0
  99. data/lib/solid_agent/tool_cache.rb +91 -0
  100. data/lib/solid_agent/version.rb +5 -0
  101. data/lib/solid_agent.rb +95 -0
  102. data/sig/solid_agent.rbs +4 -0
  103. metadata +174 -14
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateAgentRuns < ActiveRecord::Migration<%= migration_version %>
4
+ def change
5
+ create_table :agent_runs do |t|
6
+ # What the run executed against (an Agent record, a workflow, any
7
+ # AR subject) — optional, so bare executors can still record runs.
8
+ t.references :runnable, polymorphic: true, index: true
9
+
10
+ # Correlation keys shared with agent_contexts/agent_generations and
11
+ # telemetry traces.
12
+ t.string :agent_name
13
+ t.string :action_name
14
+ t.string :trace_id
15
+
16
+ t.string :status, null: false, default: "pending"
17
+
18
+ t.text :input_prompt
19
+ t.jsonb :input_params, default: {}
20
+ t.text :output
21
+ t.jsonb :output_metadata, default: {}
22
+ t.text :error_message
23
+
24
+ # Append-only progress events ({at, eid, kind, label, status,
25
+ # detail, duration_ms}) streamed to pollers mid-run.
26
+ t.jsonb :events, default: []
27
+
28
+ # 8-char fingerprint of the instructions the run executed under —
29
+ # the cohort grouping key for instruction/model comparisons.
30
+ t.string :instructions_digest
31
+
32
+ t.integer :input_tokens, default: 0
33
+ t.integer :output_tokens, default: 0
34
+ t.integer :duration_ms
35
+ t.datetime :started_at
36
+ t.datetime :completed_at
37
+
38
+ t.timestamps
39
+ end
40
+
41
+ add_index :agent_runs, :status
42
+ add_index :agent_runs, :trace_id
43
+ add_index :agent_runs, :instructions_digest
44
+ add_index :agent_runs, :created_at
45
+ end
46
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SolidAgent Configuration
4
+ #
5
+ # This initializer configures SolidAgent for your application.
6
+ # SolidAgent provides database-backed context management, tool schemas,
7
+ # and real-time streaming updates for ActiveAgent agents.
8
+
9
+ # You can customize the default model classes used by has_context:
10
+ #
11
+ # SolidAgent.configure do |config|
12
+ # config.context_class = "AgentContext"
13
+ # config.message_class = "AgentMessage"
14
+ # config.generation_class = "AgentGeneration"
15
+ # end
16
+
17
+ # Include SolidAgent concerns in your ApplicationAgent:
18
+ #
19
+ # class ApplicationAgent < ActiveAgent::Base
20
+ # include SolidAgent::HasContext
21
+ # include SolidAgent::HasTools
22
+ # include SolidAgent::StreamsToolUpdates
23
+ # end
24
+ #
25
+ # Or include them selectively in specific agents:
26
+ #
27
+ # class WritingAssistantAgent < ApplicationAgent
28
+ # include SolidAgent::HasContext
29
+ # has_context
30
+ #
31
+ # def improve
32
+ # create_context(contextable: params[:document])
33
+ # prompt
34
+ # end
35
+ # end
36
+ #
37
+ # class ResearchAgent < ApplicationAgent
38
+ # include SolidAgent::HasContext
39
+ # include SolidAgent::HasTools
40
+ # include SolidAgent::StreamsToolUpdates
41
+ #
42
+ # has_context
43
+ # has_tools :navigate, :extract_text
44
+ #
45
+ # tool_description :navigate, ->(args) { "Visiting #{args[:url]}..." }
46
+ #
47
+ # def research
48
+ # create_context(contextable: params[:user])
49
+ # prompt(tools: tools)
50
+ # end
51
+ # end
@@ -0,0 +1,209 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module SolidAgent
6
+ module Generators
7
+ class ManifestGenerator < Rails::Generators::NamedBase
8
+ source_root File.expand_path("templates", __dir__)
9
+
10
+ desc "Generates a .agent.md manifest file for an agent"
11
+
12
+ class_option :model, type: :string, default: "anthropic/claude-sonnet-4-20250514",
13
+ desc: "Model identifier (provider/model format)"
14
+
15
+ class_option :tools, type: :array, default: [],
16
+ desc: "Tool names to include"
17
+
18
+ class_option :template, type: :string, default: nil,
19
+ desc: "Use a preset template (research, assistant, reviewer, chat)"
20
+
21
+ class_option :format, type: :string, default: "agent_md",
22
+ desc: "Output format (agent_md, dotprompt)"
23
+
24
+ class_option :context, type: :string, default: nil,
25
+ desc: "Contextual param key for HasContext (e.g., user, document)"
26
+
27
+ class_option :description, type: :string, default: nil,
28
+ desc: "Agent description"
29
+
30
+ def create_manifest_file
31
+ @model = options[:model]
32
+ @tools = options[:tools]
33
+ @preset = options[:template]
34
+ @format = options[:format].to_sym
35
+ @contextual = options[:context]
36
+ @description = options[:description] || default_description
37
+
38
+ # Apply preset template configuration
39
+ apply_preset if @preset
40
+
41
+ case @format
42
+ when :agent_md
43
+ template "agent.md.erb", manifest_path(".agent.md")
44
+ when :dotprompt
45
+ template "prompt.erb", manifest_path(".prompt")
46
+ else
47
+ template "agent.md.erb", manifest_path(".agent.md")
48
+ end
49
+ end
50
+
51
+ def show_next_steps
52
+ say ""
53
+ say "Manifest created successfully!", :green
54
+ say ""
55
+ say "File generated:"
56
+ say " #{manifest_path(extension_for_format)}"
57
+ say ""
58
+ say "Usage:", :yellow
59
+ say " # Parse the manifest"
60
+ say " manifest = SolidAgent::AgentManifest.parse(\"#{manifest_path(extension_for_format)}\")"
61
+ say ""
62
+ say " # Load as agent class"
63
+ say " klass = SolidAgent::AgentManifest.load_agent(\"#{manifest_path(extension_for_format)}\")"
64
+ say ""
65
+ say " # Validate the manifest"
66
+ say " SolidAgent::AgentManifest.validate!(\"#{manifest_path(extension_for_format)}\")"
67
+ say ""
68
+
69
+ if @tools.any?
70
+ say "Tool stubs added. Implement the tool methods in your agent class:", :yellow
71
+ @tools.each do |tool|
72
+ say " def #{tool}(args)"
73
+ say " # Implement #{tool} logic"
74
+ say " end"
75
+ end
76
+ say ""
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ def file_name
83
+ name.underscore
84
+ end
85
+
86
+ def class_name
87
+ name.camelize
88
+ end
89
+
90
+ def agent_class_name
91
+ "#{class_name}Agent"
92
+ end
93
+
94
+ def agent_title
95
+ class_name.gsub(/([A-Z])/, ' \1').strip
96
+ end
97
+
98
+ def manifest_path(extension)
99
+ "app/views/#{file_name}_agent/agent#{extension}"
100
+ end
101
+
102
+ def extension_for_format
103
+ case @format
104
+ when :agent_md then ".agent.md"
105
+ when :dotprompt then ".prompt"
106
+ else ".agent.md"
107
+ end
108
+ end
109
+
110
+ def default_description
111
+ "#{agent_title} agent"
112
+ end
113
+
114
+ def apply_preset
115
+ case @preset.to_s
116
+ when "research"
117
+ @description ||= "Research and analyze topics thoroughly, providing well-sourced information"
118
+ @tools = %w[search fetch analyze] if @tools.empty?
119
+ @preset_instructions = research_instructions
120
+ when "assistant"
121
+ @description ||= "General-purpose assistant for answering questions and helping with tasks"
122
+ @preset_instructions = assistant_instructions
123
+ when "reviewer"
124
+ @description ||= "Review and provide feedback on content, code, or documents"
125
+ @tools = %w[read_file analyze_diff] if @tools.empty?
126
+ @preset_instructions = reviewer_instructions
127
+ when "chat"
128
+ @description ||= "Conversational agent for multi-turn dialogue"
129
+ @contextual ||= "user"
130
+ @preset_instructions = chat_instructions
131
+ end
132
+ end
133
+
134
+ def research_instructions
135
+ <<~INSTRUCTIONS
136
+ You are a thorough research assistant. Your goal is to provide accurate,
137
+ well-sourced information on any topic.
138
+
139
+ ## Guidelines
140
+
141
+ - Always cite your sources when providing information
142
+ - Present multiple perspectives when topics are controversial
143
+ - Acknowledge uncertainty when information is incomplete
144
+ - Break down complex topics into understandable explanations
145
+ - Verify facts before presenting them
146
+ INSTRUCTIONS
147
+ end
148
+
149
+ def assistant_instructions
150
+ <<~INSTRUCTIONS
151
+ You are a helpful assistant. Your goal is to assist users with their
152
+ questions and tasks effectively.
153
+
154
+ ## Guidelines
155
+
156
+ - Be concise but thorough in your responses
157
+ - Ask clarifying questions when requests are ambiguous
158
+ - Provide step-by-step guidance for complex tasks
159
+ - Offer alternatives when appropriate
160
+ INSTRUCTIONS
161
+ end
162
+
163
+ def reviewer_instructions
164
+ <<~INSTRUCTIONS
165
+ You are a thorough reviewer. Your goal is to provide constructive feedback
166
+ that helps improve the quality of the work.
167
+
168
+ ## Guidelines
169
+
170
+ - Focus on both strengths and areas for improvement
171
+ - Be specific with your feedback
172
+ - Provide actionable suggestions
173
+ - Maintain a constructive and respectful tone
174
+ - Consider the context and goals of the work
175
+ INSTRUCTIONS
176
+ end
177
+
178
+ def chat_instructions
179
+ <<~INSTRUCTIONS
180
+ You are a conversational assistant. Engage in helpful, natural dialogue
181
+ while maintaining context from previous messages.
182
+
183
+ ## Guidelines
184
+
185
+ - Remember context from earlier in the conversation
186
+ - Ask follow-up questions to better understand the user's needs
187
+ - Be friendly and approachable
188
+ - Know when to be concise vs. detailed based on the question
189
+ INSTRUCTIONS
190
+ end
191
+
192
+ def default_instructions
193
+ @preset_instructions || <<~INSTRUCTIONS
194
+ You are the #{agent_title} agent.
195
+
196
+ ## Instructions
197
+
198
+ TODO - Add your agent's instructions here.
199
+
200
+ ## Guidelines
201
+
202
+ - Be helpful and accurate
203
+ - Follow the user's instructions carefully
204
+ - Ask for clarification when needed
205
+ INSTRUCTIONS
206
+ end
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,39 @@
1
+ ---
2
+ name: <%= file_name %>
3
+ version: 1.0.0
4
+ description: <%= @description %>
5
+ model: <%= @model %>
6
+ <% if @tools.any? -%>
7
+
8
+ tools:
9
+ <% @tools.each do |tool| -%>
10
+ - name: <%= tool %>
11
+ description: "TODO - Describe what <%= tool %> does"
12
+ inputSchema:
13
+ type: object
14
+ properties:
15
+ query:
16
+ type: string
17
+ description: "Input for <%= tool %>"
18
+ required:
19
+ - query
20
+ <% end -%>
21
+ <% end -%>
22
+
23
+ activeagent:
24
+ class_name: <%= agent_class_name %>
25
+ concerns:
26
+ <% if @contextual -%>
27
+ - has_context:
28
+ contextual: <%= @contextual %>
29
+ <% end -%>
30
+ <% if @tools.any? -%>
31
+ - has_tools: [<%= @tools.join(', ') %>]
32
+ <% end -%>
33
+ ---
34
+
35
+ # <%= agent_title %>
36
+
37
+ <%= @description %>
38
+
39
+ <%= default_instructions %>
@@ -0,0 +1,13 @@
1
+ ---
2
+ name: <%= file_name %>
3
+ model: <%= @model %>
4
+ description: <%= @description %>
5
+ <% if @tools.any? -%>
6
+ tools:
7
+ <% @tools.each do |tool| -%>
8
+ - <%= tool %>
9
+ <% end -%>
10
+ <% end -%>
11
+ ---
12
+
13
+ <%= default_instructions %>
@@ -0,0 +1,83 @@
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 ReasonsGenerator < Rails::Generators::Base
9
+ include Rails::Generators::Migration
10
+ source_root File.expand_path("templates", __dir__)
11
+
12
+ desc "Adds reasoning columns to an existing generation model for extended thinking support"
13
+
14
+ argument :model_name, type: :string, default: "AgentGeneration",
15
+ desc: "The model to add reasoning columns to"
16
+
17
+ class_option :content_column, type: :string, default: "reasoning_content",
18
+ desc: "Column name for storing reasoning content"
19
+
20
+ class_option :tokens_column, type: :string, default: "reasoning_tokens",
21
+ desc: "Column name for storing reasoning token count"
22
+
23
+ class_option :metadata_column, type: :string, default: "reasoning_metadata",
24
+ desc: "Column name for storing reasoning metadata (JSON)"
25
+
26
+ def self.next_migration_number(dirname)
27
+ ActiveRecord::Generators::Base.next_migration_number(dirname)
28
+ end
29
+
30
+ def create_migration
31
+ @table_name = model_name.underscore.pluralize
32
+ @content_column = options[:content_column]
33
+ @tokens_column = options[:tokens_column]
34
+ @metadata_column = options[:metadata_column]
35
+
36
+ migration_template(
37
+ "add_reasoning_columns.rb.erb",
38
+ "db/migrate/add_reasoning_to_#{@table_name}.rb"
39
+ )
40
+ end
41
+
42
+ def add_concern_to_model
43
+ model_file = "app/models/#{model_name.underscore}.rb"
44
+
45
+ return unless File.exist?(model_file)
46
+
47
+ inject_into_class model_file, model_name do
48
+ " include SolidAgent::Reasonable\n\n"
49
+ end
50
+
51
+ say "Added SolidAgent::Reasonable to #{model_name}", :green
52
+ end
53
+
54
+ def show_post_install_message
55
+ say ""
56
+ say "Reasoning support added to #{model_name}!", :green
57
+ say ""
58
+ say "Next steps:", :yellow
59
+ say " 1. Run migrations: rails db:migrate"
60
+ say " 2. Use reasoning in your agents:"
61
+ say ""
62
+ say " class MyAgent < ApplicationAgent"
63
+ say " include SolidAgent::HasReasons"
64
+ say " include SolidAgent::HasContext"
65
+ say ""
66
+ say " has_reasons auto_capture: true, persist: true"
67
+ say ""
68
+ say " def analyze"
69
+ say " result = prompt("
70
+ say " messages: messages,"
71
+ say " extended_thinking: true"
72
+ say " )"
73
+ say ""
74
+ say " # Access reasoning"
75
+ say " last_reasoning.content"
76
+ say " total_reasoning_tokens"
77
+ say " end"
78
+ say " end"
79
+ say ""
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddReasoningTo<%= @table_name.camelize %> < ActiveRecord::Migration[7.0]
4
+ def change
5
+ add_column :<%= @table_name %>, :<%= @content_column %>, :text
6
+ add_column :<%= @table_name %>, :<%= @tokens_column %>, :integer, default: 0
7
+ add_column :<%= @table_name %>, :<%= @metadata_column %>, :jsonb, default: {}
8
+
9
+ add_index :<%= @table_name %>, :<%= @tokens_column %>,
10
+ name: "index_<%= @table_name %>_on_<%= @tokens_column %>"
11
+ end
12
+ end
@@ -0,0 +1,19 @@
1
+ {
2
+ "type": "function",
3
+ "name": "<%= name %>",
4
+ "description": "<%= @tool_description %>",
5
+ "parameters": {
6
+ "type": "object",
7
+ "properties": {
8
+ <%- @parameters.each_with_index do |param, index| -%>
9
+ "<%= param[:name] %>": {
10
+ "type": "<%= param[:type] %>",
11
+ "description": "<%= param[:description] %>"
12
+ }<%= index < @parameters.length - 1 ? ',' : '' %>
13
+ <%- end -%>
14
+ <%- if @parameters.empty? -%>
15
+ <%- end -%>
16
+ },
17
+ "required": [<%= @parameters.select { |p| p[:required] }.map { |p| "\"#{p[:name]}\"" }.join(", ") %>]
18
+ }
19
+ }
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module SolidAgent
6
+ module Generators
7
+ class ToolGenerator < Rails::Generators::NamedBase
8
+ source_root File.expand_path("templates", __dir__)
9
+
10
+ desc "Generates a tool schema JSON template and optional tool method for an agent"
11
+
12
+ argument :agent_name, type: :string, required: true,
13
+ desc: "The agent class name (e.g., ResearchAgent)"
14
+
15
+ class_option :parameters, type: :array, default: [],
16
+ desc: "Tool parameters in name:type:required format (e.g., url:string:required query:string)"
17
+
18
+ class_option :description, type: :string, default: "",
19
+ desc: "Tool description"
20
+
21
+ class_option :inline, type: :boolean, default: false,
22
+ desc: "Generate inline tool definition instead of JSON template"
23
+
24
+ def create_tool_template
25
+ if options[:inline]
26
+ say "Inline tool definition for #{agent_name}:", :green
27
+ say ""
28
+ say inline_tool_definition
29
+ say ""
30
+ else
31
+ @tool_description = options[:description].presence || "#{name.humanize} tool"
32
+ @parameters = parse_parameters
33
+
34
+ template "tool.json.erb", tool_template_path
35
+ say "Created tool template: #{tool_template_path}", :green
36
+ end
37
+ end
38
+
39
+ def show_method_stub
40
+ say ""
41
+ say "Add this method to your #{agent_name}:", :yellow
42
+ say ""
43
+ say tool_method_stub
44
+ say ""
45
+
46
+ if options[:inline]
47
+ say "Add this to your agent class:", :yellow
48
+ say ""
49
+ say inline_tool_definition
50
+ say ""
51
+ else
52
+ say "Don't forget to register the tool in your agent:", :yellow
53
+ say ""
54
+ say " has_tools :#{name}"
55
+ say ""
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ def tool_template_path
62
+ "app/views/#{agent_name.underscore}/tools/#{name}.json.erb"
63
+ end
64
+
65
+ def parse_parameters
66
+ options[:parameters].map do |param|
67
+ parts = param.split(":")
68
+ {
69
+ name: parts[0],
70
+ type: parts[1] || "string",
71
+ required: parts[2] == "required",
72
+ description: parts[3] || "The #{parts[0].humanize.downcase}"
73
+ }
74
+ end
75
+ end
76
+
77
+ def tool_method_stub
78
+ params_signature = @parameters.map do |p|
79
+ if p[:required]
80
+ "#{p[:name]}:"
81
+ else
82
+ "#{p[:name]}: nil"
83
+ end
84
+ end.join(", ")
85
+
86
+ params_log = @parameters.map { |p| "\#{#{p[:name]}}" }.join(", ")
87
+
88
+ <<~RUBY
89
+ # Tool method: #{name.humanize}
90
+ def #{name}(#{params_signature})
91
+ Rails.logger.info "[#{agent_name}] Tool called: #{name}(#{params_log})"
92
+
93
+ # TODO: Implement tool logic
94
+ { success: true }
95
+ rescue => e
96
+ Rails.logger.error "[#{agent_name}] #{name.humanize} error: \#{e.message}"
97
+ { success: false, error: e.message }
98
+ end
99
+ RUBY
100
+ end
101
+
102
+ def inline_tool_definition
103
+ params_block = @parameters.map do |p|
104
+ required_str = p[:required] ? ", required: true" : ""
105
+ " parameter :#{p[:name]}, type: :#{p[:type]}#{required_str}, description: \"#{p[:description]}\""
106
+ end.join("\n")
107
+
108
+ <<~RUBY
109
+ tool :#{name} do
110
+ description "#{@tool_description}"
111
+ #{params_block}
112
+ end
113
+ RUBY
114
+ end
115
+ end
116
+ end
117
+ end