agent_c 2.9
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/.rubocop.yml +10 -0
- data/.ruby-version +1 -0
- data/CLAUDE.md +21 -0
- data/README.md +360 -0
- data/Rakefile +16 -0
- data/TODO.md +105 -0
- data/agent_c.gemspec +38 -0
- data/docs/batch.md +503 -0
- data/docs/chat-methods.md +156 -0
- data/docs/cost-reporting.md +86 -0
- data/docs/pipeline-tips-and-tricks.md +453 -0
- data/docs/session-configuration.md +274 -0
- data/docs/testing.md +747 -0
- data/docs/tools.md +103 -0
- data/docs/versioned-store.md +840 -0
- data/lib/agent_c/agent/chat.rb +211 -0
- data/lib/agent_c/agent/chat_response.rb +38 -0
- data/lib/agent_c/agent/chats/anthropic_bedrock.rb +48 -0
- data/lib/agent_c/batch.rb +102 -0
- data/lib/agent_c/configs/repo.rb +90 -0
- data/lib/agent_c/context.rb +56 -0
- data/lib/agent_c/costs/data.rb +39 -0
- data/lib/agent_c/costs/report.rb +219 -0
- data/lib/agent_c/db/store.rb +162 -0
- data/lib/agent_c/errors.rb +19 -0
- data/lib/agent_c/pipeline.rb +152 -0
- data/lib/agent_c/pipelines/agent.rb +219 -0
- data/lib/agent_c/processor.rb +98 -0
- data/lib/agent_c/prompts.yml +53 -0
- data/lib/agent_c/schema.rb +71 -0
- data/lib/agent_c/session.rb +206 -0
- data/lib/agent_c/store.rb +72 -0
- data/lib/agent_c/test_helpers.rb +173 -0
- data/lib/agent_c/tools/dir_glob.rb +46 -0
- data/lib/agent_c/tools/edit_file.rb +114 -0
- data/lib/agent_c/tools/file_metadata.rb +43 -0
- data/lib/agent_c/tools/git_status.rb +30 -0
- data/lib/agent_c/tools/grep.rb +119 -0
- data/lib/agent_c/tools/paths.rb +36 -0
- data/lib/agent_c/tools/read_file.rb +94 -0
- data/lib/agent_c/tools/run_rails_test.rb +87 -0
- data/lib/agent_c/tools.rb +61 -0
- data/lib/agent_c/utils/git.rb +87 -0
- data/lib/agent_c/utils/shell.rb +58 -0
- data/lib/agent_c/version.rb +5 -0
- data/lib/agent_c.rb +32 -0
- data/lib/versioned_store/base.rb +314 -0
- data/lib/versioned_store/config.rb +26 -0
- data/lib/versioned_store/stores/schema.rb +127 -0
- data/lib/versioned_store/version.rb +5 -0
- data/lib/versioned_store.rb +5 -0
- data/template/Gemfile +9 -0
- data/template/Gemfile.lock +152 -0
- data/template/README.md +61 -0
- data/template/Rakefile +50 -0
- data/template/bin/rake +27 -0
- data/template/lib/autoload.rb +10 -0
- data/template/lib/config.rb +59 -0
- data/template/lib/pipeline.rb +19 -0
- data/template/lib/prompts.yml +57 -0
- data/template/lib/store.rb +17 -0
- data/template/test/pipeline_test.rb +221 -0
- data/template/test/test_helper.rb +18 -0
- metadata +194 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: d647a877e7e6660d1c9ab7f5e2ce9e00d8f932c78d9ca6a47467265cc2c62e4e
|
|
4
|
+
data.tar.gz: 83e0796a0545c207c5afdebd457ac3134ea0b8bd5b9cd327a02de5f2049cc02b
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: a248627251111087a4814c86a5c980cdafa7936f0ae2b420b397485b367c7e0f693b2d4f6dcc6a46070c4381d2e2eaff775e31615bfdd5c0ea72e984655bec74
|
|
7
|
+
data.tar.gz: ce5adb048f393142b008a15943e2337597f2969a76cd8446a1890e5440ad9c73d39af74052085d916b58fbbbd551a23385db2b6285f68e359f676b5fb5fa67d4
|
data/.rubocop.yml
ADDED
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.2.9
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Rules
|
|
2
|
+
|
|
3
|
+
- Leave modules modules if they do not need state. If they apply behaviors to other schema/classes leave them modules.
|
|
4
|
+
- Prefer to not store lambdas as variables unless necessary. If they are just going to be passed to other methods, leave them as blocks
|
|
5
|
+
- If a class is not trivial (eg, more than one method and/or more than like 30 lines) then extract it to its own file.
|
|
6
|
+
- This project is using Zeitwerk. You should not use require_relative, just match the module names to file path and it will load automatically.
|
|
7
|
+
- When you commit, use the --no-gpg-sign flag. Start commit messages with "claude: "
|
|
8
|
+
- DO NOT add example scripts. Either add it to the readme or make a test.
|
|
9
|
+
- DO NOT add documentation outside of the README
|
|
10
|
+
- DO NOT program defensively. If something should respond_to?() a method then just invoke the method. An error is better than a false positive
|
|
11
|
+
- DO NOT write a one-off script for debugging, write a test-case and run it instead.
|
|
12
|
+
- DO NOT edit the singleton class of an object. If you think you need to do this, ideas for avoiding: inject an object, create a module and include it, make a base class.
|
|
13
|
+
|
|
14
|
+
# TESTING
|
|
15
|
+
|
|
16
|
+
- We do not use stubbing in our test. If you need to stub something (or monkey-patch it) to test it, that thing should be injectable.
|
|
17
|
+
- Run tests with `bin/rake test` You can pass TESTOPTS to run a specific file.
|
|
18
|
+
|
|
19
|
+
# Style
|
|
20
|
+
|
|
21
|
+
- For multiline Strings always use a HEREDOC
|
data/README.md
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
# AgentC
|
|
2
|
+
|
|
3
|
+
A small Ruby wrapper around [RubyLLM](https://github.com/alexrudall/ruby_llm) that helps you write a pipeline of AI prompts and run it many times, in bulk. Built for automating repetitive refactors across a large codebase.
|
|
4
|
+
|
|
5
|
+
<small>Most of what's below is generated by an LLM. I take no responsibility for any of it, unless it's awesome... then it was pure prompting skills which I will take credit for.</small>
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
AgentC provides batch processing and pipeline orchestration for AI-powered tasks:
|
|
10
|
+
|
|
11
|
+
- **Batch Processing** - Execute pipelines across multiple records with automatic parallelization via worktrees
|
|
12
|
+
- **Pipeline Orchestration** - Define multi-step workflows with AI-powered agent steps and custom logic
|
|
13
|
+
- **Resumable Execution** - Automatically skip completed steps when pipelines are rerun
|
|
14
|
+
- **Automatic query persistence** - All interactions saved to SQLite
|
|
15
|
+
- **Cost tracking** - Detailed reports on token usage and costs
|
|
16
|
+
- **Custom tools** - File operations, grep, Rails tests, and more
|
|
17
|
+
- **Schema validation** - RubyLLM Schema support for structured responses
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
This gem is not pushed to rubygems. Instead, you should add a git reference to your Gemfile (use a revision because I'm going to make changes with complete disregard for backwards compatibility).
|
|
22
|
+
|
|
23
|
+
## Example template
|
|
24
|
+
|
|
25
|
+
See an [example template](./template) you can run in the `template/` directory of this repo. Poke around there after perusing this section.
|
|
26
|
+
|
|
27
|
+
You can copy this template to start building your own.
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
A "Pipeline" is a series of prompts for Claude to perform. Data gathered from prior steps are fed into subsequent steps (you'll define an ActiveRecord class to capture the data). If any step fails, the pipeline aborts.
|
|
32
|
+
|
|
33
|
+
A "Batch" is a collection of pipelines to be run. They can be run against a single directory in series, or concurrently across multiple git worktrees. If a pipeline fails, the failure will be recorded but the batch will continue.
|
|
34
|
+
|
|
35
|
+
### The necessary structures
|
|
36
|
+
|
|
37
|
+
In this example, we'll have Claude choose a random file, summarize its contents in a language of our choosing, then write it to disk and commit.
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
# Define the records your agent will interact with.
|
|
41
|
+
# Normally you'd only have one record.
|
|
42
|
+
#
|
|
43
|
+
# A versioned store saves a full db backup per-transaction
|
|
44
|
+
# so that you can recover from any step of the process.
|
|
45
|
+
# Just trying to save tokens...
|
|
46
|
+
class MyStore < VersionedStore::Base
|
|
47
|
+
include AgentC::Store
|
|
48
|
+
|
|
49
|
+
record(:summary) do
|
|
50
|
+
|
|
51
|
+
# the migration schema is defined in line
|
|
52
|
+
schema do |t|
|
|
53
|
+
# we'll input this data
|
|
54
|
+
t.string(:language)
|
|
55
|
+
|
|
56
|
+
# claude will generate this data
|
|
57
|
+
t.string(:input_path)
|
|
58
|
+
t.text(:summary_text)
|
|
59
|
+
t.text(:summary_path)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# this is the body of your ActiveRecord class
|
|
63
|
+
# add methods here as needed
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# A "pipeline" processes a single record
|
|
68
|
+
class MyPipeline < AgentC::Pipeline
|
|
69
|
+
# The prompts for these steps will
|
|
70
|
+
# live in our prompts.yml file
|
|
71
|
+
agent_step(:analyze_code)
|
|
72
|
+
agent_step(:write_summary_to_file)
|
|
73
|
+
|
|
74
|
+
step(:finalize) do
|
|
75
|
+
repo.commit_all("claude: analyzed code")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# if this pipeline fails, we want to
|
|
79
|
+
# leave the repo in a clean state
|
|
80
|
+
# for the next pipeline.
|
|
81
|
+
on_failure do
|
|
82
|
+
repo.reset_hard_all
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
```
|
|
86
|
+
```yaml
|
|
87
|
+
# define your prompts in a prompts.yml file:
|
|
88
|
+
|
|
89
|
+
en:
|
|
90
|
+
|
|
91
|
+
# the key names must match up to the `agent_step` invocation above
|
|
92
|
+
analyze_code:
|
|
93
|
+
# prompts here will be cached across pipelines.
|
|
94
|
+
# These prompts cannot interpolate any attributes.
|
|
95
|
+
# Suggested use is to put as much in the cached_prompts
|
|
96
|
+
# as possible and put variable data in the prompt.
|
|
97
|
+
cached_prompts:
|
|
98
|
+
- "Choose a random file. Read it and summarize it in the provided language."
|
|
99
|
+
|
|
100
|
+
# You can interpolate any attribute from your record class
|
|
101
|
+
prompt: "lanuage: %{language}"
|
|
102
|
+
|
|
103
|
+
# Tools available:
|
|
104
|
+
# - dir_glob
|
|
105
|
+
# - read_file
|
|
106
|
+
# - edit_file
|
|
107
|
+
# - grep
|
|
108
|
+
# - run_rails_test
|
|
109
|
+
# you can add more...
|
|
110
|
+
tools: [read_file, dir_glob]
|
|
111
|
+
|
|
112
|
+
# The response schema defines what Claude will return.
|
|
113
|
+
# The keys must be attributes from your record. What Claude
|
|
114
|
+
# returns will automatically be saved to your record.
|
|
115
|
+
response_schema:
|
|
116
|
+
summary_text:
|
|
117
|
+
type: string # this is the default
|
|
118
|
+
required: true # this is the default
|
|
119
|
+
description: "The summary text"
|
|
120
|
+
input_path:
|
|
121
|
+
type: string # this is the default
|
|
122
|
+
required: true # this is the default
|
|
123
|
+
description: "The path of the file you summarized"
|
|
124
|
+
|
|
125
|
+
write_summary_to_file:
|
|
126
|
+
cached_prompts:
|
|
127
|
+
- |
|
|
128
|
+
You will be given some text.
|
|
129
|
+
Choose a well-named file and write the text to it"
|
|
130
|
+
|
|
131
|
+
prompt: "Here is the text to write: %{summary_text}"
|
|
132
|
+
tools: [edit_file]
|
|
133
|
+
response_schema:
|
|
134
|
+
summary_path:
|
|
135
|
+
description: "the path of the file you wrote"
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Now, make a Batch and invoke it. A batch requires a lot of configuration, related to data storage, where your repo is, and claude API credentials:
|
|
139
|
+
|
|
140
|
+
```ruby
|
|
141
|
+
|
|
142
|
+
batch = Batch.new(
|
|
143
|
+
record_type: :summary, # the class name you want to work on
|
|
144
|
+
pipeline: Pipeline, # the Pipeline class you made
|
|
145
|
+
|
|
146
|
+
# A batch has a "project" and a "run". These are ways
|
|
147
|
+
# to track Claude usage. Your Batch will have a
|
|
148
|
+
# "project". Each time you call batch.new you get
|
|
149
|
+
# a new "run".
|
|
150
|
+
project: "TemplateProject",
|
|
151
|
+
|
|
152
|
+
# We'll set some spending limits. Once these are
|
|
153
|
+
# reached, the Batch will abort.
|
|
154
|
+
max_spend_project: 100.0,
|
|
155
|
+
max_spend_run: 20.0,
|
|
156
|
+
|
|
157
|
+
store: {
|
|
158
|
+
class: Store, # the Store class you made
|
|
159
|
+
config: {
|
|
160
|
+
logger: Logger.new("/dev/null"), # a logger for the store
|
|
161
|
+
dir: "/where/you/want/your/store/saved"
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
# Where Claude will work
|
|
166
|
+
workspace: {
|
|
167
|
+
dir: "/where/claude/will/be/working",
|
|
168
|
+
env: {
|
|
169
|
+
# available to your tools
|
|
170
|
+
# only used by run_rails_test currently
|
|
171
|
+
SOME_ENV_VAR: "1"
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
# If you prefer, you can have the Batch manage
|
|
176
|
+
# some git worktrees for you. It will parallelize
|
|
177
|
+
# your tasks across your worktrees for MAXIMUM
|
|
178
|
+
# TOKEN BURN.
|
|
179
|
+
#
|
|
180
|
+
# Worktrees will be created for you if you are
|
|
181
|
+
# starting a new Batch. If you are continuing an
|
|
182
|
+
# existing Batch (after an error, for example),
|
|
183
|
+
# the worktrees will be left in their current
|
|
184
|
+
# state.
|
|
185
|
+
#
|
|
186
|
+
# You must pass *either* a workspace or a repo
|
|
187
|
+
repo: {
|
|
188
|
+
dir: "/path/to/your/repo",
|
|
189
|
+
|
|
190
|
+
# an existing git revision or branch name
|
|
191
|
+
initial_revision: "main",
|
|
192
|
+
|
|
193
|
+
# optional: limit Claude to a subdir from your repo
|
|
194
|
+
working_subdir: "./",
|
|
195
|
+
|
|
196
|
+
# Where to put your worktrees
|
|
197
|
+
worktrees_root_dir: "/tmp/example-worktrees",
|
|
198
|
+
|
|
199
|
+
# Each worktree gets a branch, they'll be suffixed
|
|
200
|
+
# with a counter
|
|
201
|
+
worktree_branch_prefix: "summary-examples",
|
|
202
|
+
|
|
203
|
+
# Currently, this defines how many worktrees to
|
|
204
|
+
# create. It's obnoxious I know, but hey, it works.
|
|
205
|
+
worktree_envs: [{}, {}],
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
# The claude configuration:
|
|
209
|
+
session: {
|
|
210
|
+
# all chats with claude are saved to a sqlite db.
|
|
211
|
+
# this is separate than your Store's db because
|
|
212
|
+
# why throw anything away. Can be useful for
|
|
213
|
+
# debugging why Claude did what it did
|
|
214
|
+
agent_db_path: "/path/to/your/claude/db.sqlite",
|
|
215
|
+
logger: Logger.new("/dev/null"), # probably use the same logger for everything...
|
|
216
|
+
i18n_path: "/path/to/your/prompts.yml",
|
|
217
|
+
|
|
218
|
+
# as you debug your pipeline, you'll probably run it
|
|
219
|
+
# many times. We tag all Claude chat records with a
|
|
220
|
+
# project so you can track costs.
|
|
221
|
+
project: "SomeProject",
|
|
222
|
+
|
|
223
|
+
# only available for Bedrock...
|
|
224
|
+
ruby_llm: {
|
|
225
|
+
bedrock_api_key: ENV.fetch("AWS_ACCESS_KEY_ID"),
|
|
226
|
+
bedrock_secret_key: ENV.fetch("AWS_SECRET_ACCESS_KEY"),
|
|
227
|
+
bedrock_session_token: ENV.fetch("AWS_SESSION_TOKEN"),
|
|
228
|
+
bedrock_region: ENV.fetch("AWS_REGION", "us-west-2"),
|
|
229
|
+
default_model: ENV.fetch("LLM_MODEL", "us.anthropic.claude-sonnet-4-5-20250929-v1:0")
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
# WHEW that's a lot of config,
|
|
235
|
+
|
|
236
|
+
# Now we add some records for processing.
|
|
237
|
+
# The batches "store" is just a bunch of
|
|
238
|
+
# ActiveRecord classes, but you reference
|
|
239
|
+
# them by the name you gave them in the
|
|
240
|
+
# store.
|
|
241
|
+
#
|
|
242
|
+
# We'll add some summary records.
|
|
243
|
+
# This seeded data represents the input
|
|
244
|
+
# into your pipelines.
|
|
245
|
+
#
|
|
246
|
+
# Because your batch can be stopped and
|
|
247
|
+
# restarted, we need our data creation
|
|
248
|
+
# to be idemptotent.
|
|
249
|
+
record_1 = (
|
|
250
|
+
batch
|
|
251
|
+
.store
|
|
252
|
+
.summary
|
|
253
|
+
.find_or_create_by!(language: "english")
|
|
254
|
+
)
|
|
255
|
+
record_2 = (
|
|
256
|
+
batch
|
|
257
|
+
.store
|
|
258
|
+
.summary
|
|
259
|
+
.find_or_create_by!(language: "spanish")
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
# Add the records to be processed.
|
|
263
|
+
# add_task is idempotent
|
|
264
|
+
batch.add_task(record_1)
|
|
265
|
+
batch.add_task(record_2)
|
|
266
|
+
|
|
267
|
+
batch.call
|
|
268
|
+
|
|
269
|
+
# See the details of what happened
|
|
270
|
+
puts batch.report
|
|
271
|
+
# =>
|
|
272
|
+
# Summary report:
|
|
273
|
+
# Succeeded: 2
|
|
274
|
+
# Pending: 0
|
|
275
|
+
# Failed: 0
|
|
276
|
+
# Run cost: $2.34
|
|
277
|
+
# Project total cost: $10.40
|
|
278
|
+
# ---
|
|
279
|
+
# task: 1 - wrote summary to /tmp/example-worktrees/summary-examples-0/CHAT_TEST_SUMMARY.md
|
|
280
|
+
# task: 2 - wrote summary to /tmp/example-worktrees/summary-examples-1/RESUMEN_BASE.md
|
|
281
|
+
|
|
282
|
+
# Get a more detailed breakdown
|
|
283
|
+
cost = batch.cost
|
|
284
|
+
|
|
285
|
+
# Explore the records created
|
|
286
|
+
tasks = batch.store.task.all
|
|
287
|
+
summaries = batch.store.summary.all
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
You can tail your logs to see what's happening. The full text of you Claude chats are logged to DEBUG.
|
|
291
|
+
|
|
292
|
+
If you just want to see your pipeline's progression:
|
|
293
|
+
|
|
294
|
+
```shell
|
|
295
|
+
# Only see INFO
|
|
296
|
+
tail -f /path/to/log.log | grep INFO
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
#### Batch errors
|
|
300
|
+
|
|
301
|
+
If your batch is interupted (by an exception or you kill it), you can continue it by simply running your batch again. The progress is persisted in the Batch's store.
|
|
302
|
+
|
|
303
|
+
If you need to correct any data or go back in time, you can peruse the store's versions by doing:
|
|
304
|
+
|
|
305
|
+
```ruby
|
|
306
|
+
# see how many versions
|
|
307
|
+
puts batch.store.versions.count
|
|
308
|
+
|
|
309
|
+
# peruse your store:
|
|
310
|
+
batch.store.versions[12].summary.count
|
|
311
|
+
|
|
312
|
+
# restore a prior version
|
|
313
|
+
batch.store.version[12].restore
|
|
314
|
+
|
|
315
|
+
# re-run the batch
|
|
316
|
+
batch.call
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### Resetting a Batch
|
|
320
|
+
|
|
321
|
+
You can delete the sqlite database for your store.
|
|
322
|
+
|
|
323
|
+
Delete the database you configured at `store: { config: { dir: "/path/to/db" } }`
|
|
324
|
+
|
|
325
|
+
### Debugging a Batch
|
|
326
|
+
|
|
327
|
+
If you make multiple worktrees, they will be processed concurrently. This makes things hard to debug using `binding.irb`.
|
|
328
|
+
|
|
329
|
+
I suggest making one worktree until it's running successfully.
|
|
330
|
+
|
|
331
|
+
### Structuring your project
|
|
332
|
+
|
|
333
|
+
I suggest following the structure of the [example template](./template).
|
|
334
|
+
|
|
335
|
+
# Detailed Documentation
|
|
336
|
+
|
|
337
|
+
Detailed guides for all features:
|
|
338
|
+
|
|
339
|
+
- **[Batch](docs/batch.md)** - Batch configuration, methods, and pipeline integration
|
|
340
|
+
- **[Pipeline Tips and Tricks](docs/pipeline-tips-and-tricks.md)** - Useful patterns and techniques for pipelines
|
|
341
|
+
- **[Chat Methods](docs/chat-methods.md)** - Using session.prompt and session.chat for direct interactions
|
|
342
|
+
- **[Tools](docs/tools.md)** - Built-in tools for file operations and code interaction
|
|
343
|
+
- **[Testing](docs/testing.md)** - Using TestHelpers::DummyChat for testing without real LLM calls
|
|
344
|
+
- **[Cost Reporting](docs/cost-reporting.md)** - Track token usage and costs
|
|
345
|
+
- **[Session Configuration](docs/session-configuration.md)** - All configuration options
|
|
346
|
+
- **[Store Versioning](docs/versioned-store.md)** - All configuration options
|
|
347
|
+
|
|
348
|
+
## Requirements
|
|
349
|
+
|
|
350
|
+
- Ruby >= 3.0.0
|
|
351
|
+
- AWS credentials configured for Bedrock access
|
|
352
|
+
- SQLite3
|
|
353
|
+
|
|
354
|
+
## License
|
|
355
|
+
|
|
356
|
+
WTFPL - Do What The Fuck You Want To Public License
|
|
357
|
+
|
|
358
|
+
## Author
|
|
359
|
+
|
|
360
|
+
Pete Kinnecom (git@k7u7.com)
|
data/Rakefile
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/setup"
|
|
4
|
+
require "rake/testtask"
|
|
5
|
+
|
|
6
|
+
Rake::TestTask.new(:test) do |t|
|
|
7
|
+
t.libs << "test"
|
|
8
|
+
t.libs << "."
|
|
9
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
|
10
|
+
t.verbose = false
|
|
11
|
+
t.warning = false
|
|
12
|
+
# Enable parallel test execution based on number of processors
|
|
13
|
+
ENV["MT_CPU"] ||= Etc.nprocessors.to_s
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
task default: :test
|
data/TODO.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# TODOs
|
|
2
|
+
|
|
3
|
+
Things I'd like to work on:
|
|
4
|
+
|
|
5
|
+
- Make injecting a Chat record simpler.
|
|
6
|
+
- Make injecting Git simpler (make injecting anything easier)
|
|
7
|
+
- Add a request queue to AgentC::Chat so that we can rate-limit and retry on error
|
|
8
|
+
- Use spring for run_rails_test, but add a timeout condition where it kills the process if no stdout appears for a while and tries again without spring.
|
|
9
|
+
- tool calls should write the full results to file (except for readfile) and pass back a reference for future queries. For example, if RunRailsTest gives way too much output, we have to truncate but how to see the rest?
|
|
10
|
+
|
|
11
|
+
## Immplement plan, implement, review looping
|
|
12
|
+
|
|
13
|
+
Some scratch:
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
agent_iterate_loop(
|
|
17
|
+
:implement_query_object,
|
|
18
|
+
max_tries: 17,
|
|
19
|
+
plan: [
|
|
20
|
+
:plan_step_1,
|
|
21
|
+
:plan_step_2,
|
|
22
|
+
],
|
|
23
|
+
implement: [
|
|
24
|
+
:implement_step_1,
|
|
25
|
+
:implement_step_2,
|
|
26
|
+
],
|
|
27
|
+
# optional: defaults to implement
|
|
28
|
+
iterate: [
|
|
29
|
+
:address_feedback_1
|
|
30
|
+
]
|
|
31
|
+
review: [
|
|
32
|
+
:review_step_1,
|
|
33
|
+
:review_step_2
|
|
34
|
+
],
|
|
35
|
+
)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Thoughts:
|
|
39
|
+
|
|
40
|
+
- This makes a demand on the `response_schema` of the prompts.
|
|
41
|
+
- Does the `task` track the looping here or the `record`?
|
|
42
|
+
- Is the progress tracked in memory? This can be one step...
|
|
43
|
+
- What if you have multiple `agent_iterate_loop` invocations? How do we store diffs/reviews for each of those?
|
|
44
|
+
- The `agent_step` applies the result to the record. If we track it on the task, we could get the attributes to the right place. Can we extend the response schema? The `plan` step definitely needs to update the record, so it needs to specify response_schema. The `implement/iterate` steps *might* need to accept response_schema (eg, path of file created? Or not necessary?, maybe not necessary.)
|
|
45
|
+
- Does a failed review trigger plan -> iterate -> review or just iterate -> review
|
|
46
|
+
- The implement/iterate arrays could include "plan" steps so that they know if they're starting from scratch or not.
|
|
47
|
+
- Do we run every review or stop at the first one?
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
def agent_iterate_loop(
|
|
52
|
+
name,
|
|
53
|
+
max_tries: 3,
|
|
54
|
+
implement: [],
|
|
55
|
+
iterate: implement,
|
|
56
|
+
review: [],
|
|
57
|
+
)
|
|
58
|
+
step(name) do
|
|
59
|
+
tries = 0
|
|
60
|
+
|
|
61
|
+
while(tries < max_tries)
|
|
62
|
+
if tries == 0
|
|
63
|
+
implement.each do |name|
|
|
64
|
+
process_prompt(name)
|
|
65
|
+
end
|
|
66
|
+
else
|
|
67
|
+
iterate.each do |name|
|
|
68
|
+
process_prompt(
|
|
69
|
+
name,
|
|
70
|
+
additional_i18n_attrs: {
|
|
71
|
+
feedback: feedback.join("\n---\n")
|
|
72
|
+
}
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
feedbacks = []
|
|
78
|
+
|
|
79
|
+
diff = git.diff
|
|
80
|
+
review.each do |name|
|
|
81
|
+
result = process_prompt(
|
|
82
|
+
name,
|
|
83
|
+
schema: -> {
|
|
84
|
+
t.boolean(:pass)
|
|
85
|
+
t.string(:feedback)
|
|
86
|
+
},
|
|
87
|
+
additional_i18n_attrs: {
|
|
88
|
+
diff:
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if !result.fetch("pass")
|
|
93
|
+
feedbacks << result.fetch("feedback")
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
if record.respond_to?(:add_review)
|
|
98
|
+
record.add_review(diff:, feedbacks:)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
break if feedbacks.empty?
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
```
|
data/agent_c.gemspec
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/agent_c/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "agent_c"
|
|
7
|
+
spec.version = AgentC::VERSION
|
|
8
|
+
spec.authors = ["Pete Kinnecom"]
|
|
9
|
+
spec.email = ["git@k7u7.com"]
|
|
10
|
+
|
|
11
|
+
spec.summary = <<~TEXT.strip
|
|
12
|
+
Batch processing for pipelines of steps for AI. AgentC, get it?
|
|
13
|
+
TEXT
|
|
14
|
+
spec.homepage = "https://github.com/petekinnecom/agent_c"
|
|
15
|
+
spec.license = "WTFPL"
|
|
16
|
+
spec.required_ruby_version = ">= 3.0.0"
|
|
17
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
|
18
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
19
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
|
20
|
+
spec.metadata["changelog_uri"] = spec.homepage
|
|
21
|
+
|
|
22
|
+
spec.files = Dir.chdir(__dir__) do
|
|
23
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
|
24
|
+
(File.expand_path(f) == __FILE__) ||
|
|
25
|
+
f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor Gemfile])
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
spec.bindir = "exe"
|
|
29
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
30
|
+
spec.require_paths = ["lib"]
|
|
31
|
+
|
|
32
|
+
spec.add_dependency("zeitwerk", "~> 2.7")
|
|
33
|
+
spec.add_dependency("activerecord", "~> 8.1")
|
|
34
|
+
spec.add_dependency("sqlite3", "~> 2.9")
|
|
35
|
+
spec.add_dependency("async", "~> 2.35")
|
|
36
|
+
spec.add_dependency("ruby_llm", "~> 1.9")
|
|
37
|
+
spec.add_dependency("json-schema", "~> 6.1")
|
|
38
|
+
end
|