self_agency 0.0.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 +7 -0
- data/.envrc +1 -0
- data/.github/workflows/deploy-github-pages.yml +40 -0
- data/.irbrc +22 -0
- data/CHANGELOG.md +5 -0
- data/COMMITS.md +196 -0
- data/LICENSE.txt +21 -0
- data/README.md +177 -0
- data/Rakefile +8 -0
- data/docs/api/configuration.md +85 -0
- data/docs/api/errors.md +166 -0
- data/docs/api/index.md +37 -0
- data/docs/api/self-agency-module.md +198 -0
- data/docs/architecture/overview.md +181 -0
- data/docs/architecture/security.md +101 -0
- data/docs/assets/images/self_agency.gif +0 -0
- data/docs/assets/images/self_agency.mp4 +0 -0
- data/docs/development/contributing.md +45 -0
- data/docs/development/setup.md +81 -0
- data/docs/development/testing.md +70 -0
- data/docs/examples/autonomous-robots.md +109 -0
- data/docs/examples/basic-examples.md +237 -0
- data/docs/examples/collaborative-robots.md +98 -0
- data/docs/examples/full-workflow.md +100 -0
- data/docs/examples/index.md +36 -0
- data/docs/getting-started/installation.md +71 -0
- data/docs/getting-started/quick-start.md +94 -0
- data/docs/guide/configuration.md +113 -0
- data/docs/guide/generating-methods.md +146 -0
- data/docs/guide/how-to-use.md +144 -0
- data/docs/guide/lifecycle-hooks.md +86 -0
- data/docs/guide/prompt-templates.md +189 -0
- data/docs/guide/saving-methods.md +84 -0
- data/docs/guide/scopes.md +74 -0
- data/docs/guide/source-inspection.md +96 -0
- data/docs/index.md +77 -0
- data/examples/01_basic_usage.rb +27 -0
- data/examples/02_multiple_methods.rb +43 -0
- data/examples/03_scopes.rb +40 -0
- data/examples/04_source_inspection.rb +46 -0
- data/examples/05_lifecycle_hook.rb +55 -0
- data/examples/06_configuration.rb +97 -0
- data/examples/07_error_handling.rb +103 -0
- data/examples/08_class_context.rb +64 -0
- data/examples/09_method_override.rb +52 -0
- data/examples/10_full_workflow.rb +118 -0
- data/examples/11_collaborative_robots/atlas.rb +31 -0
- data/examples/11_collaborative_robots/echo.rb +30 -0
- data/examples/11_collaborative_robots/main.rb +190 -0
- data/examples/11_collaborative_robots/nova.rb +71 -0
- data/examples/11_collaborative_robots/robot.rb +119 -0
- data/examples/12_autonomous_robots/analyst.rb +193 -0
- data/examples/12_autonomous_robots/collector.rb +78 -0
- data/examples/12_autonomous_robots/main.rb +166 -0
- data/examples/12_autonomous_robots/planner.rb +125 -0
- data/examples/12_autonomous_robots/robot.rb +284 -0
- data/examples/generated/from_range_class.rb +3 -0
- data/examples/generated/mean_instance.rb +4 -0
- data/examples/generated/median_instance.rb +15 -0
- data/examples/generated/report_singleton.rb +3 -0
- data/examples/generated/standard_deviation_instance.rb +8 -0
- data/examples/lib/message_bus.rb +57 -0
- data/examples/lib/setup.rb +8 -0
- data/lib/self_agency/configuration.rb +76 -0
- data/lib/self_agency/errors.rb +35 -0
- data/lib/self_agency/generator.rb +47 -0
- data/lib/self_agency/prompts/generate/system.txt.erb +15 -0
- data/lib/self_agency/prompts/generate/user.txt.erb +13 -0
- data/lib/self_agency/prompts/shape/system.txt.erb +26 -0
- data/lib/self_agency/prompts/shape/user.txt.erb +10 -0
- data/lib/self_agency/sandbox.rb +17 -0
- data/lib/self_agency/saver.rb +62 -0
- data/lib/self_agency/validator.rb +64 -0
- data/lib/self_agency/version.rb +5 -0
- data/lib/self_agency.rb +315 -0
- data/mkdocs.yml +156 -0
- data/sig/self_agency.rbs +4 -0
- metadata +163 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Lifecycle Hooks
|
|
2
|
+
|
|
3
|
+
SelfAgency provides a lifecycle hook that fires each time a method is generated. Override it in your class to persist, log, or react to generated methods.
|
|
4
|
+
|
|
5
|
+
## `on_method_generated`
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
def on_method_generated(method_name, scope, code)
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
| Parameter | Type | Description |
|
|
12
|
+
|-----------|------|-------------|
|
|
13
|
+
| `method_name` | `Symbol` | Name of the generated method |
|
|
14
|
+
| `scope` | `Symbol` | `:instance`, `:singleton`, or `:class` |
|
|
15
|
+
| `code` | `String` | The generated source code |
|
|
16
|
+
|
|
17
|
+
By default, `on_method_generated` is a no-op. Override it in your class:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
class PersistentCalculator
|
|
21
|
+
include SelfAgency
|
|
22
|
+
|
|
23
|
+
attr_reader :generation_log
|
|
24
|
+
|
|
25
|
+
def initialize
|
|
26
|
+
@generation_log = []
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def on_method_generated(method_name, scope, code)
|
|
30
|
+
@generation_log << { method_name: method_name, scope: scope, code: code }
|
|
31
|
+
puts "[hook] Generated :#{method_name} (scope: #{scope})"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## When It Fires
|
|
37
|
+
|
|
38
|
+
The hook fires **once per method** after successful validation and installation. If `_()` generates multiple methods in a single call, the hook fires for each one:
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
calc = PersistentCalculator.new
|
|
42
|
+
|
|
43
|
+
calc._("an instance method named 'increment' that returns n + 1")
|
|
44
|
+
# [hook] Generated :increment (scope: instance)
|
|
45
|
+
|
|
46
|
+
calc._(
|
|
47
|
+
"two methods: 'min_of(a, b)' returns the smaller, " \
|
|
48
|
+
"'max_of(a, b)' returns the larger"
|
|
49
|
+
)
|
|
50
|
+
# [hook] Generated :min_of (scope: instance)
|
|
51
|
+
# [hook] Generated :max_of (scope: instance)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Common Patterns
|
|
55
|
+
|
|
56
|
+
### Save to Files
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
def on_method_generated(method_name, scope, code)
|
|
60
|
+
filepath = "generated/#{method_name}_#{scope}.rb"
|
|
61
|
+
File.write(filepath, code)
|
|
62
|
+
end
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Log to a Database
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
def on_method_generated(method_name, scope, code)
|
|
69
|
+
GeneratedMethod.create!(
|
|
70
|
+
name: method_name.to_s,
|
|
71
|
+
scope: scope.to_s,
|
|
72
|
+
source: code,
|
|
73
|
+
class_name: self.class.name
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Collect for Later Inspection
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
def on_method_generated(method_name, scope, code)
|
|
82
|
+
@generation_log << { method_name: method_name, scope: scope, code: code }
|
|
83
|
+
end
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
This pattern is used in the [collaborative robots](../examples/collaborative-robots.md) and [autonomous robots](../examples/autonomous-robots.md) examples.
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# Prompt Templates
|
|
2
|
+
|
|
3
|
+
SelfAgency uses [ruby_llm-template](https://github.com/danielfriis/ruby_llm-template) for prompt management. The two-stage pipeline (shape and generate) each use a pair of ERB templates.
|
|
4
|
+
|
|
5
|
+
## Default Template Layout
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
lib/self_agency/prompts/
|
|
9
|
+
shape/
|
|
10
|
+
system.txt.erb # System prompt for the shape stage
|
|
11
|
+
user.txt.erb # User prompt for the shape stage
|
|
12
|
+
generate/
|
|
13
|
+
system.txt.erb # System prompt for the generate stage
|
|
14
|
+
user.txt.erb # User prompt for the generate stage
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Shape Stage Templates
|
|
18
|
+
|
|
19
|
+
The **shape** stage rewrites a casual language description into a precise Ruby method specification.
|
|
20
|
+
|
|
21
|
+
**`shape/system.txt.erb`** -- Instructs the LLM to act as a prompt engineer, with rules for rewriting descriptions:
|
|
22
|
+
|
|
23
|
+
- State the exact method name (snake_case)
|
|
24
|
+
- State the method signature with parameter names, types, defaults
|
|
25
|
+
- State the return type and value
|
|
26
|
+
- Describe the algorithm step by step
|
|
27
|
+
- Translate vague terms into concrete Ruby operations
|
|
28
|
+
- Output only plain language, no code
|
|
29
|
+
|
|
30
|
+
**`shape/user.txt.erb`** -- Provides class context and the user's request:
|
|
31
|
+
|
|
32
|
+
```erb
|
|
33
|
+
Rewrite the following casual request into a precise Ruby method specification.
|
|
34
|
+
|
|
35
|
+
Class context:
|
|
36
|
+
- Class name: <%= class_name %>
|
|
37
|
+
- Instance variables: <%= ivars %>
|
|
38
|
+
- Public methods: <%= methods %>
|
|
39
|
+
- Scope: <%= scope_instruction %>
|
|
40
|
+
|
|
41
|
+
User request:
|
|
42
|
+
<%= raw_prompt %>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Available Variables (Shape)
|
|
46
|
+
|
|
47
|
+
| Variable | Description |
|
|
48
|
+
|----------|-------------|
|
|
49
|
+
| `class_name` | Name of the including class |
|
|
50
|
+
| `ivars` | Comma-separated list of instance variables |
|
|
51
|
+
| `methods` | Comma-separated list of public instance methods |
|
|
52
|
+
| `scope_instruction` | Human-readable scope description |
|
|
53
|
+
| `raw_prompt` | The user's original description |
|
|
54
|
+
|
|
55
|
+
## Generate Stage Templates
|
|
56
|
+
|
|
57
|
+
The **generate** stage produces Ruby code from the shaped specification.
|
|
58
|
+
|
|
59
|
+
**`generate/system.txt.erb`** -- Instructs the LLM to act as a Ruby code generator:
|
|
60
|
+
|
|
61
|
+
- Return exactly one `def method_name ... end` block
|
|
62
|
+
- Do not use dangerous methods (`system`, `exec`, `File`, `IO`, `eval`, etc.)
|
|
63
|
+
- Do not wrap code in markdown fences
|
|
64
|
+
- The method must be self-contained
|
|
65
|
+
|
|
66
|
+
!!! note
|
|
67
|
+
The template instructs the LLM to return "exactly one" method definition. However, when describing multiple methods in a single `_()` call, some LLMs return multiple `def...end` blocks despite this instruction. SelfAgency handles this gracefully -- it splits the output into individual method blocks and installs each one separately.
|
|
68
|
+
|
|
69
|
+
**`generate/user.txt.erb`** -- Passes the shaped specification. On retries, includes the previous error and code so the LLM can self-correct:
|
|
70
|
+
|
|
71
|
+
```erb
|
|
72
|
+
<%= shaped_spec %>
|
|
73
|
+
<%% if previous_error %>
|
|
74
|
+
|
|
75
|
+
Your previous attempt produced this code:
|
|
76
|
+
|
|
77
|
+
<%= previous_code %>
|
|
78
|
+
|
|
79
|
+
It failed with the following error:
|
|
80
|
+
|
|
81
|
+
<%= previous_error %>
|
|
82
|
+
|
|
83
|
+
Please fix the issue and generate corrected code.
|
|
84
|
+
<%% end %>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Available Variables (Generate)
|
|
88
|
+
|
|
89
|
+
| Variable | Description |
|
|
90
|
+
|----------|-------------|
|
|
91
|
+
| `class_name` | Name of the including class |
|
|
92
|
+
| `ivars` | Comma-separated list of instance variables |
|
|
93
|
+
| `methods` | Comma-separated list of public instance methods |
|
|
94
|
+
| `shaped_spec` | Output from the shape stage |
|
|
95
|
+
| `previous_error` | Error message from the previous attempt (`nil` on first attempt) |
|
|
96
|
+
| `previous_code` | Generated code from the previous attempt (`nil` on first attempt) |
|
|
97
|
+
|
|
98
|
+
!!! warning
|
|
99
|
+
If you customize the generate templates, your `user.txt.erb` must handle the `previous_error` and `previous_code` variables (they are passed on every call). Use a conditional `<%% if previous_error %>` block to include them only during retries.
|
|
100
|
+
|
|
101
|
+
## Custom Templates
|
|
102
|
+
|
|
103
|
+
### Why Customize?
|
|
104
|
+
|
|
105
|
+
The default prompts are tuned for general-purpose code generation, but different providers and models respond best to different prompt styles. You may want to customize templates when:
|
|
106
|
+
|
|
107
|
+
- **Switching providers or models** -- A prompt that works well with Ollama's Qwen may produce poor results with OpenAI's GPT-4o or Anthropic's Claude. Some models need more explicit instructions; others perform better with fewer constraints. Smaller models may need step-by-step algorithmic guidance that a larger model would find redundant.
|
|
108
|
+
- **Domain-specific generation** -- If your class operates in a specific domain (financial calculations, data science, text processing), you can add domain rules and conventions directly into the system prompt so every generated method follows them.
|
|
109
|
+
- **Code style enforcement** -- You may want generated methods to follow your project's conventions: frozen string literals, specific naming patterns, guard clauses, or particular error handling styles.
|
|
110
|
+
- **Controlling output format** -- Some models wrap output in markdown fences or include chain-of-thought reasoning despite instructions not to. Tailoring the prompt to your model's quirks reduces the sanitization needed.
|
|
111
|
+
|
|
112
|
+
### Setup
|
|
113
|
+
|
|
114
|
+
Start by copying the default prompts into your project:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
cp -r $(bundle show self_agency)/lib/self_agency/prompts my_prompts
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Then point SelfAgency at your copy:
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
SelfAgency.configure do |config|
|
|
124
|
+
config.template_directory = File.expand_path("my_prompts", __dir__)
|
|
125
|
+
# ...
|
|
126
|
+
end
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Your custom directory must follow the same layout:
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
my_prompts/
|
|
133
|
+
shape/
|
|
134
|
+
system.txt.erb
|
|
135
|
+
user.txt.erb
|
|
136
|
+
generate/
|
|
137
|
+
system.txt.erb
|
|
138
|
+
user.txt.erb
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
All ERB variables listed above are available in your custom templates.
|
|
142
|
+
|
|
143
|
+
### Example: Adapting the Generate Prompt for a Different Model
|
|
144
|
+
|
|
145
|
+
The default `generate/system.txt.erb` is concise:
|
|
146
|
+
|
|
147
|
+
```erb
|
|
148
|
+
You are a Ruby code generator. You MUST respond with ONLY a Ruby method
|
|
149
|
+
definition — nothing else. No explanation, no markdown fences, no comments
|
|
150
|
+
outside the method, no extra text.
|
|
151
|
+
|
|
152
|
+
Context for the class you are writing a method for:
|
|
153
|
+
- Class name: <%= class_name %>
|
|
154
|
+
- Instance variables: <%= ivars %>
|
|
155
|
+
- Public methods: <%= methods %>
|
|
156
|
+
|
|
157
|
+
Rules:
|
|
158
|
+
- Return exactly one `def method_name ... end` block.
|
|
159
|
+
- Use the EXACT method name, parameter names, and Hash key names from the specification. Do NOT rename or abbreviate any identifier.
|
|
160
|
+
- Do NOT use any of these forbidden patterns: system, exec, spawn, fork, abort, exit, backticks, %x, File., IO., Kernel., Open3., Process., require, load, __send__, eval, send, public_send, method(), const_get, class_eval, module_eval, instance_eval, instance_variable_set, instance_variable_get, define_method, Binding, BasicObject, remove_method, undef_method.
|
|
161
|
+
- Do NOT wrap the code in markdown fences.
|
|
162
|
+
- The method must be self-contained.
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
A smaller or less instruction-following model might ignore the "no markdown fences" rule, or include conversational preamble before the code. You could adapt it with stronger guardrails:
|
|
166
|
+
|
|
167
|
+
```erb
|
|
168
|
+
TASK: Generate a Ruby method definition.
|
|
169
|
+
|
|
170
|
+
CRITICAL FORMAT RULES:
|
|
171
|
+
1. Your ENTIRE response must be a single `def ... end` block.
|
|
172
|
+
2. Do NOT output any text before `def` or after `end`.
|
|
173
|
+
3. Do NOT use ``` fences. Do NOT use <think> tags.
|
|
174
|
+
4. Do NOT include comments, explanations, or notes.
|
|
175
|
+
|
|
176
|
+
If you output anything other than a method definition, the parse will fail.
|
|
177
|
+
|
|
178
|
+
Class: <%= class_name %>
|
|
179
|
+
Instance variables: <%= ivars %>
|
|
180
|
+
Existing methods: <%= methods %>
|
|
181
|
+
|
|
182
|
+
FORBIDDEN constructs (will be rejected):
|
|
183
|
+
system, exec, spawn, fork, backticks, File, IO, Kernel, Open3,
|
|
184
|
+
Process, require, load, eval, send, __send__, remove_method, undef_method
|
|
185
|
+
|
|
186
|
+
Write ONLY the def ... end block now.
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
This version uses imperative language, repeats the format constraint, and explicitly warns about parse failure -- techniques that help smaller models stay on track.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Saving Methods
|
|
2
|
+
|
|
3
|
+
`_save!` writes an object's generated methods as a subclass in a Ruby source file. This lets you capture LLM-generated methods as permanent, version-controlled code.
|
|
4
|
+
|
|
5
|
+
## Basic Usage
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
foo = Foo.new
|
|
9
|
+
foo._("an instance method to add two integers")
|
|
10
|
+
foo._("an instance method to subtract two integers")
|
|
11
|
+
|
|
12
|
+
foo._save!(as: :calculator)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
This writes `calculator.rb`:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
# frozen_string_literal: true
|
|
19
|
+
|
|
20
|
+
require_relative "foo"
|
|
21
|
+
|
|
22
|
+
class Calculator < Foo
|
|
23
|
+
# an instance method to add two integers
|
|
24
|
+
def add(a, b)
|
|
25
|
+
a + b
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# an instance method to subtract two integers
|
|
29
|
+
def subtract(a, b)
|
|
30
|
+
a - b
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## The `as:` Parameter
|
|
36
|
+
|
|
37
|
+
`as:` is required. It sets the subclass name and (by default) the output filename. It accepts a String or Symbol.
|
|
38
|
+
|
|
39
|
+
Snake case is automatically converted to CamelCase:
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
foo._save!(as: :weather_analyst)
|
|
43
|
+
# Writes weather_analyst.rb with class WeatherAnalyst < Foo
|
|
44
|
+
|
|
45
|
+
foo._save!(as: "WeatherAnalyst")
|
|
46
|
+
# Same result
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Custom File Path
|
|
50
|
+
|
|
51
|
+
Override the default file path with `path:`:
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
foo._save!(as: :calculator, path: "lib/calculator.rb")
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Require Path
|
|
58
|
+
|
|
59
|
+
The generated file includes a `require_relative` pointing back to the parent class source file. The path is computed relative to the output file location.
|
|
60
|
+
|
|
61
|
+
## Multiple Instances, Different Methods
|
|
62
|
+
|
|
63
|
+
Each instance of a class can have different generated methods. `_save!` captures only the methods generated on that specific instance:
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
collector = Robot.new(name: "Collector")
|
|
67
|
+
analyst = Robot.new(name: "Analyst")
|
|
68
|
+
|
|
69
|
+
# Each robot generates different methods via _()
|
|
70
|
+
collector._save!(as: collector.name) # collector.rb with class Collector < Robot
|
|
71
|
+
analyst._save!(as: analyst.name) # analyst.rb with class Analyst < Robot
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Description Comments
|
|
75
|
+
|
|
76
|
+
Each method in the saved file includes the original natural language description as a comment header, so the intent behind the generated code is preserved.
|
|
77
|
+
|
|
78
|
+
## Errors
|
|
79
|
+
|
|
80
|
+
| Error | Condition |
|
|
81
|
+
|-------|-----------|
|
|
82
|
+
| `ArgumentError` | `as:` is not a String or Symbol |
|
|
83
|
+
| `SelfAgency::Error` | No generated methods to save |
|
|
84
|
+
| `SelfAgency::Error` | Parent class is anonymous (no name) |
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Scopes
|
|
2
|
+
|
|
3
|
+
SelfAgency supports three scopes for generated methods: `:instance`, `:singleton`, and `:class`. The scope is passed as the `scope:` keyword argument to `_()`.
|
|
4
|
+
|
|
5
|
+
## Instance Scope (Default)
|
|
6
|
+
|
|
7
|
+
Instance methods are available on **all instances** of the class. This is the default scope.
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
class Greeter
|
|
11
|
+
include SelfAgency
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
alice = Greeter.new
|
|
15
|
+
bob = Greeter.new
|
|
16
|
+
|
|
17
|
+
alice._("an instance method named 'hello' that returns the string 'Hello, world!'")
|
|
18
|
+
|
|
19
|
+
alice.hello #=> "Hello, world!"
|
|
20
|
+
bob.hello #=> "Hello, world!" -- available to all instances
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Internally, SelfAgency creates an anonymous module containing the generated method and uses `prepend` to add it to the class. This means the method appears on all current and future instances.
|
|
24
|
+
|
|
25
|
+
## Singleton Scope
|
|
26
|
+
|
|
27
|
+
Singleton methods are available on **only one specific instance**. Other instances of the same class do not have the method.
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
alice._("a method named 'secret' that returns 'Alice only'", scope: :singleton)
|
|
31
|
+
|
|
32
|
+
alice.secret #=> "Alice only"
|
|
33
|
+
bob.respond_to?(:secret) #=> false
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Internally, singleton-scoped methods are prepended to the instance's singleton class.
|
|
37
|
+
|
|
38
|
+
## Class Scope
|
|
39
|
+
|
|
40
|
+
Class methods are available on the **class itself**, not on instances.
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
alice._("a class method named 'self.class_greeting' that returns 'Greetings from Greeter'", scope: :class)
|
|
44
|
+
|
|
45
|
+
Greeter.class_greeting #=> "Greetings from Greeter"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
!!! note
|
|
49
|
+
For class methods, the LLM generates `def self.method_name`. SelfAgency strips the `self.` prefix before evaluating, since it prepends the method to the class's singleton class.
|
|
50
|
+
|
|
51
|
+
## Scope Comparison
|
|
52
|
+
|
|
53
|
+
| Scope | Keyword | Available On | Mechanism |
|
|
54
|
+
|-------|---------|-------------|-----------|
|
|
55
|
+
| Instance | `scope: :instance` | All instances of the class | `self.class.prepend(module)` |
|
|
56
|
+
| Singleton | `scope: :singleton` | One specific instance | `singleton_class.prepend(module)` |
|
|
57
|
+
| Class | `scope: :class` | The class itself | `self.class.singleton_class.prepend(module)` |
|
|
58
|
+
|
|
59
|
+
## Combining Scopes
|
|
60
|
+
|
|
61
|
+
You can mix scopes freely on the same class:
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
calc = StatisticsCalculator.new([10, 20, 30])
|
|
65
|
+
|
|
66
|
+
# Instance methods -- available to all instances
|
|
67
|
+
calc._("an instance method named 'mean' ...")
|
|
68
|
+
|
|
69
|
+
# Class method -- available on StatisticsCalculator itself
|
|
70
|
+
calc._("a class method named 'self.from_range' ...", scope: :class)
|
|
71
|
+
|
|
72
|
+
# Singleton method -- available only on this calc instance
|
|
73
|
+
calc._("a method named 'report' ...", scope: :singleton)
|
|
74
|
+
```
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Source Inspection
|
|
2
|
+
|
|
3
|
+
`_source_for` returns the source code for any method -- both LLM-generated and file-defined methods.
|
|
4
|
+
|
|
5
|
+
## Instance-Level Inspection
|
|
6
|
+
|
|
7
|
+
Call `_source_for` on an instance to retrieve source code:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
class MathHelper
|
|
11
|
+
include SelfAgency
|
|
12
|
+
|
|
13
|
+
# Multiplies a number by two.
|
|
14
|
+
def double(n)
|
|
15
|
+
n * 2
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
helper = MathHelper.new
|
|
20
|
+
helper._("an instance method named 'square' that accepts an integer n and returns n * n")
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
For LLM-generated methods, `_source_for` returns the code with the original description as a comment header:
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
puts helper._source_for(:square)
|
|
27
|
+
# >> # an instance method named 'square' that accepts an integer n and returns n * n
|
|
28
|
+
# >> def square(n)
|
|
29
|
+
# >> n * n
|
|
30
|
+
# >> end
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Class-Level Inspection
|
|
34
|
+
|
|
35
|
+
`_source_for` is also available as a class method:
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
puts MathHelper._source_for(:square)
|
|
39
|
+
# >> # an instance method named 'square' that accepts an integer n and returns n * n
|
|
40
|
+
# >> def square(n)
|
|
41
|
+
# >> n * n
|
|
42
|
+
# >> end
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Both instance and class level lookups return the same source for LLM-generated methods.
|
|
46
|
+
|
|
47
|
+
## File-Defined Methods
|
|
48
|
+
|
|
49
|
+
For methods defined in source files (not generated by the LLM), `_source_for` falls back to the `method_source` gem. It includes any comments directly above the method definition:
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
puts helper._source_for(:double)
|
|
53
|
+
# >> # Multiplies a number by two.
|
|
54
|
+
# >> def double(n)
|
|
55
|
+
# >> n * 2
|
|
56
|
+
# >> end
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Unknown Methods
|
|
60
|
+
|
|
61
|
+
If a method doesn't exist or its source is unavailable, `_source_for` returns `nil`:
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
helper._source_for(:nonexistent) #=> nil
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Version History
|
|
68
|
+
|
|
69
|
+
When a method is regenerated (e.g., with a refined description), SelfAgency tracks each version. Use `_source_versions_for` at the class level to retrieve the history:
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
helper._("an instance method named 'square' that accepts n and returns n * n")
|
|
73
|
+
helper._("an instance method named 'square' that accepts n, raises ArgumentError if n < 0, and returns n * n")
|
|
74
|
+
|
|
75
|
+
versions = MathHelper._source_versions_for(:square)
|
|
76
|
+
versions.size #=> 2
|
|
77
|
+
|
|
78
|
+
versions.each do |v|
|
|
79
|
+
puts "#{v[:at]} — #{v[:description]}"
|
|
80
|
+
puts v[:code]
|
|
81
|
+
puts
|
|
82
|
+
end
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Each version entry is a Hash with keys `:code`, `:description`, `:instance_id`, and `:at`.
|
|
86
|
+
|
|
87
|
+
Returns an empty array if no versions exist for the method.
|
|
88
|
+
|
|
89
|
+
## How It Works
|
|
90
|
+
|
|
91
|
+
`_source_for` checks two sources in order:
|
|
92
|
+
|
|
93
|
+
1. **LLM-generated source** -- Stored in an internal hash when `_()` creates the method
|
|
94
|
+
2. **File source** -- Falls back to `method_source` gem for methods defined in `.rb` files
|
|
95
|
+
|
|
96
|
+
If both lookups fail (method doesn't exist or source file can't be located), `nil` is returned.
|
data/docs/index.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<h1>SelfAgency</h1>
|
|
3
|
+
Describe what you want in plain language, get working methods back.<br/>
|
|
4
|
+
SelfAgency is a mixin module that gives any Ruby class the ability to<br/>
|
|
5
|
+
generate and install methods at runtime via an LLM.<br/>
|
|
6
|
+
<br/>
|
|
7
|
+
<img src="assets/images/self_agency.gif" alt="SelfAgency Demo" width="100%">
|
|
8
|
+
<h2>Key Features</h2>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<table>
|
|
12
|
+
<tr>
|
|
13
|
+
<td width="50%" valign="top">
|
|
14
|
+
<ul>
|
|
15
|
+
<li><strong>Natural language to Ruby methods</strong> — describe what you want, get working code</li>
|
|
16
|
+
<li><strong>Multiple methods at once</strong> — generate related methods in a single call</li>
|
|
17
|
+
<li><strong>Three scopes</strong> — instance, singleton, and class methods</li>
|
|
18
|
+
<li><strong>Two-stage LLM pipeline</strong> — shape the prompt, then generate code</li>
|
|
19
|
+
</ul>
|
|
20
|
+
</td>
|
|
21
|
+
<td width="50%" valign="top">
|
|
22
|
+
<ul>
|
|
23
|
+
<li><strong>Security by default</strong> — 26 static patterns + runtime sandbox</li>
|
|
24
|
+
<li><strong>Automatic retries</strong> — self-corrects on validation failure</li>
|
|
25
|
+
<li><strong>Source inspection & versioning</strong> — view code and track history</li>
|
|
26
|
+
<li><strong>Provider agnostic</strong> — any LLM via <a href="https://github.com/crmne/ruby_llm">ruby_llm</a></li>
|
|
27
|
+
</ul>
|
|
28
|
+
</td>
|
|
29
|
+
</tr>
|
|
30
|
+
</table>
|
|
31
|
+
|
|
32
|
+
> [!CAUTION]
|
|
33
|
+
> This is an experiment. It may not be fit for any specific purpose. Its micro-prompting. Instead of asking Claude Code, CodeX or Gemini to create an entire application, you can use SelfAgency to generate individual methods. So far the experiments are showing good success with methods that perform math stuff on its input.
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
## Quick Example
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
require "self_agency"
|
|
40
|
+
|
|
41
|
+
SelfAgency.configure do |config|
|
|
42
|
+
config.provider = :ollama
|
|
43
|
+
config.model = "qwen3-coder:30b"
|
|
44
|
+
config.api_base = "http://localhost:11434/v1"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class Calculator
|
|
48
|
+
include SelfAgency
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
calc = Calculator.new
|
|
52
|
+
calc._("an instance method to add two integers, return the result")
|
|
53
|
+
#=> [:add]
|
|
54
|
+
|
|
55
|
+
calc.add(3, 7)
|
|
56
|
+
#=> 10
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## How It Works
|
|
60
|
+
|
|
61
|
+
Your casual description is first "shaped" into a precise Ruby method specification, then passed through a multi-stage pipeline:
|
|
62
|
+
|
|
63
|
+
1. **Shape** -- Rewrites your casual description into a precise Ruby method specification
|
|
64
|
+
2. **Generate** -- Produces `def...end` block(s) from the shaped spec
|
|
65
|
+
3. **Sanitize** -- Strips markdown fences and `<think>` blocks
|
|
66
|
+
4. **Validate** -- Checks for empty code, missing `def...end`, syntax errors, and dangerous patterns
|
|
67
|
+
5. **Retry** -- On validation/security failure, feeds the error back to the LLM for self-correction (up to `generation_retries` attempts)
|
|
68
|
+
6. **Sandbox Eval** -- Evaluates code inside a cached sandbox module that shadows dangerous Kernel methods
|
|
69
|
+
|
|
70
|
+
## Requirements
|
|
71
|
+
|
|
72
|
+
- Ruby >= 3.2.0
|
|
73
|
+
- An LLM provider (Ollama by default, or any provider supported by ruby_llm)
|
|
74
|
+
|
|
75
|
+
## Getting Started
|
|
76
|
+
|
|
77
|
+
Head to the [Installation](getting-started/installation.md) guide to add SelfAgency to your project, then follow the [Quick Start](getting-started/quick-start.md) for a complete walkthrough. For a broader perspective on where SelfAgency fits in your development process, see [How to Use SelfAgency](guide/how-to-use.md).
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# 01_basic_usage.rb — Single method generation
|
|
5
|
+
#
|
|
6
|
+
# Demonstrates:
|
|
7
|
+
# - include SelfAgency
|
|
8
|
+
# - _() to generate a method from a description
|
|
9
|
+
# - Calling the generated method
|
|
10
|
+
#
|
|
11
|
+
# Requires a running Ollama instance with the configured model.
|
|
12
|
+
|
|
13
|
+
require_relative "lib/setup"
|
|
14
|
+
|
|
15
|
+
class Calculator
|
|
16
|
+
include SelfAgency
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
calc = Calculator.new
|
|
20
|
+
|
|
21
|
+
puts "Generating a method to add two integers..."
|
|
22
|
+
method_names = calc._("an instance method named 'add' that accepts two integer parameters a and b, and returns their sum")
|
|
23
|
+
|
|
24
|
+
puts "Generated methods: #{method_names.inspect}"
|
|
25
|
+
puts "calc.add(3, 7) = #{calc.add(3, 7)}"
|
|
26
|
+
puts "calc.add(-1, 1) = #{calc.add(-1, 1)}"
|
|
27
|
+
puts "calc.add(100, 200) = #{calc.add(100, 200)}"
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# 02_multiple_methods.rb — Multiple methods from one call
|
|
5
|
+
#
|
|
6
|
+
# Demonstrates:
|
|
7
|
+
# - Generating several related methods in a single _() call
|
|
8
|
+
# - _() returns an Array of Symbols
|
|
9
|
+
# - Calling each generated method
|
|
10
|
+
#
|
|
11
|
+
# Requires a running Ollama instance with the configured model.
|
|
12
|
+
|
|
13
|
+
require_relative "lib/setup"
|
|
14
|
+
|
|
15
|
+
class Arithmetic
|
|
16
|
+
include SelfAgency
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
arith = Arithmetic.new
|
|
20
|
+
|
|
21
|
+
puts "Generating four arithmetic methods in a single call..."
|
|
22
|
+
method_names = arith._(
|
|
23
|
+
"create four instance methods: " \
|
|
24
|
+
"'add(a, b)' returns a + b, " \
|
|
25
|
+
"'subtract(a, b)' returns a - b, " \
|
|
26
|
+
"'multiply(a, b)' returns a * b, " \
|
|
27
|
+
"'divide(a, b)' returns a.to_f / b (raises ZeroDivisionError if b is zero)"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
puts "_() returned: #{method_names.inspect}"
|
|
31
|
+
puts "Type: #{method_names.class}"
|
|
32
|
+
puts "Count: #{method_names.length} methods generated"
|
|
33
|
+
puts
|
|
34
|
+
|
|
35
|
+
method_names.each do |name|
|
|
36
|
+
puts " #{name} is now defined"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
puts
|
|
40
|
+
puts "arith.add(10, 3) = #{arith.add(10, 3)}"
|
|
41
|
+
puts "arith.subtract(10, 3) = #{arith.subtract(10, 3)}"
|
|
42
|
+
puts "arith.multiply(10, 3) = #{arith.multiply(10, 3)}"
|
|
43
|
+
puts "arith.divide(10, 3) = #{arith.divide(10, 3)}"
|