agent_c 2.71828
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 +12 -0
- data/agent_c.gemspec +38 -0
- data/docs/chat-methods.md +157 -0
- data/docs/cost-reporting.md +86 -0
- data/docs/pipeline-tips-and-tricks.md +71 -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 +32 -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 +188 -0
- data/lib/agent_c/processor.rb +98 -0
- data/lib/agent_c/prompts.yml +53 -0
- data/lib/agent_c/schema.rb +85 -0
- data/lib/agent_c/session.rb +207 -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 +112 -0
- data/lib/agent_c/tools/file_metadata.rb +43 -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 +60 -0
- data/lib/agent_c/utils/git.rb +75 -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 +191 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 8321a1602d20f566b59365d641fecb934340b043d6544e43b01b2951e947282b
|
|
4
|
+
data.tar.gz: bf3cd0d58944294f4e83e080ac7aebfff8677798c96426411e559c8f7e7f6a7d
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 5dbe7a3d1ca921db961a5a3685c01f4e539b0358506cb45f9d6a04c462cb0657efa45eff16eea4f65c00bdb6f113b40551b38e71f5d61ceadf8463cec1ea9007
|
|
7
|
+
data.tar.gz: 4b407bc2cf7086536bce703b209552b8753c6b3eef37b096437728095945d247a5b6643113487ad739339e407b6e4c43c6b0cdd72aeca3f75551ab9e1ac763e0
|
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
|
+
- If you need to test a one-off script, write a test-case and run it instad of writing a temporary file or using a giant shell script
|
|
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
|
+
- **[Main README](../README.md)** - Batch and Pipeline usage (primary approach)
|
|
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,12 @@
|
|
|
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
|
|
9
|
+
process if no stdout appears for a while and tries again without spring.
|
|
10
|
+
- tool calls should write the full results to file (except for readfile) and pass
|
|
11
|
+
back a reference for future queries. For example, if RunRailsTest gives way too
|
|
12
|
+
much output, we have to truncate but how to see the rest?
|
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
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# Chat Methods
|
|
2
|
+
|
|
3
|
+
**Note:** For batch processing and structured workflows, use [Batch and Pipeline](../README.md) instead. The methods below are for direct chat interactions and one-off requests.
|
|
4
|
+
|
|
5
|
+
AgentC provides several methods for interacting with LLMs, each optimized for different use cases.
|
|
6
|
+
|
|
7
|
+
## Creating Chats
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
# See the [configuration](./session-configuration.md) for session args
|
|
11
|
+
session = Session.new(...)
|
|
12
|
+
|
|
13
|
+
chat = session.chat(
|
|
14
|
+
tools: [:read_file, :edit_file],
|
|
15
|
+
cached_prompts: ["You are a helpful assistant"],
|
|
16
|
+
workspace_dir: Dir.pwd
|
|
17
|
+
)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Chat.ask(message)
|
|
21
|
+
|
|
22
|
+
Basic interaction - send a message and get a response:
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
chat = session.chat
|
|
26
|
+
response = chat.ask("Explain recursion in simple terms")
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Chat.get(message, schema:, confirm:, out_of:)
|
|
30
|
+
|
|
31
|
+
Get a structured response with optional confirmation:
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
# Get a simple answer
|
|
35
|
+
answer = chat.get("What is 2 + 2?")
|
|
36
|
+
|
|
37
|
+
# Get structured response using AgentC::Schema.result
|
|
38
|
+
# This creates a schema that accepts either success or error responses
|
|
39
|
+
#
|
|
40
|
+
# You can make your own schema using RubyLLM::Schema, but
|
|
41
|
+
# this is a pretty standard approach. It will allow the LLM
|
|
42
|
+
# to indicate that they could not fulfill your request and
|
|
43
|
+
# give a reason why.
|
|
44
|
+
#
|
|
45
|
+
# The response will look like one of the following:
|
|
46
|
+
# {
|
|
47
|
+
# status: "success",
|
|
48
|
+
# name: "...",
|
|
49
|
+
# email: "...",
|
|
50
|
+
# }
|
|
51
|
+
# OR:
|
|
52
|
+
# {
|
|
53
|
+
# status: "failure",
|
|
54
|
+
# message: "some reason why it couldn't do it"
|
|
55
|
+
# }
|
|
56
|
+
|
|
57
|
+
schema = AgentC::Schema.result do
|
|
58
|
+
string(:name, description: "Person's name")
|
|
59
|
+
string(:email, description: "Person's email")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
result = chat.get(
|
|
63
|
+
"Extract the name and email from this text: 'Contact John at john@example.com'",
|
|
64
|
+
schema: schema
|
|
65
|
+
)
|
|
66
|
+
# => { "status" => "success", "name" => "John", "email" => "john@example.com" }
|
|
67
|
+
|
|
68
|
+
# If the LLM can't complete the task, it returns an error response:
|
|
69
|
+
# => { "status" => "error", "message" => "No email found in the text" }
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Using confirm and out_of for consensus
|
|
73
|
+
|
|
74
|
+
LLMs are non-deterministic and can give different answers to the same question. The `confirm` feature asks the question multiple times and only accepts an answer when it appears at least `confirm` times out of `out_of` attempts. This gives you much higher confidence the answer isn't a hallucination or random variation.
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
class YesOrNoSchema < RubyLLM::Schema
|
|
78
|
+
string(:value, enum: ["yes", "no"])
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
confirmed = chat.get(
|
|
82
|
+
"Is vanilla better than chocolate?",
|
|
83
|
+
confirm: 2, # Need 2 matching answers
|
|
84
|
+
out_of: 3, # Out of 3 attempts max
|
|
85
|
+
schema: YesOrNoSchema
|
|
86
|
+
)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Chat.refine(message, schema:, times:)
|
|
90
|
+
|
|
91
|
+
Iteratively refine a response by having the LLM review and improve its own answer.
|
|
92
|
+
|
|
93
|
+
The refine feature asks your question, gets an answer, then asks the LLM to review that answer for accuracy and improvements. This repeats for the specified number of times. Each iteration gives the LLM a chance to catch mistakes, add detail, or improve quality.
|
|
94
|
+
|
|
95
|
+
This works because LLMs are often better at *reviewing* content than generating it perfectly the first time - like having an editor review a draft. It's especially effective for creative tasks, complex analysis, or code generation where iterative improvement leads to higher quality outputs.
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
HaikuSchema = RubyLLM::Schema.object(
|
|
99
|
+
haiku: RubyLLM::Schema.string
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
refined_answer = chat.refine(
|
|
103
|
+
"Write a haiku about programming",
|
|
104
|
+
schema: HaikuSchema,
|
|
105
|
+
times: 3 # LLM reviews and refines its answer 3 times
|
|
106
|
+
)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Session.prompt() - One-Off Requests
|
|
110
|
+
|
|
111
|
+
For single-shot requests where you don't need a persistent chat, use `session.prompt()`:
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
# See the [configuration](./session-configuration.md) for session args
|
|
115
|
+
session = Session.new(...)
|
|
116
|
+
|
|
117
|
+
# Simple one-off request
|
|
118
|
+
result = session.prompt(
|
|
119
|
+
prompt: "What is the capital of France?",
|
|
120
|
+
schema: -> { string(:answer) }
|
|
121
|
+
)
|
|
122
|
+
# => ChatResponse with success/error status
|
|
123
|
+
|
|
124
|
+
# With tools and custom settings
|
|
125
|
+
result = session.prompt(
|
|
126
|
+
prompt: "Read the README file and summarize it",
|
|
127
|
+
schema: -> { string(:summary) },
|
|
128
|
+
tools: [:read_file],
|
|
129
|
+
tool_args: { workspace_dir: '/path/to/project' },
|
|
130
|
+
cached_prompt: ["You are a helpful documentation assistant"]
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
if result.success?
|
|
134
|
+
puts result.data['summary']
|
|
135
|
+
else
|
|
136
|
+
puts "Error: #{result.error_message}"
|
|
137
|
+
end
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
This is equivalent to creating a chat, calling `get()`, and handling the response, but more concise for one-off requests.
|
|
141
|
+
|
|
142
|
+
## Cached Prompts
|
|
143
|
+
|
|
144
|
+
To optimize token usage and reduce costs, you can use cached prompts. Cached prompts are stored in the API provider's cache and can significantly reduce the number of input tokens charged on subsequent requests.
|
|
145
|
+
|
|
146
|
+
```ruby
|
|
147
|
+
# Provide cached prompts that will be reused across conversations
|
|
148
|
+
cached_prompts = [
|
|
149
|
+
"You are a helpful coding assistant specialized in Ruby.",
|
|
150
|
+
"Always write idiomatic Ruby code following Ruby community best practices."
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
chat = session.chat(cached_prompts: cached_prompts)
|
|
154
|
+
response = chat.ask("Write a method to calculate fibonacci numbers")
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The first request will incur cache creation costs, but subsequent requests with the same cached prompts will use significantly fewer tokens, reducing overall API costs.
|