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.
Files changed (62) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +10 -0
  3. data/.ruby-version +1 -0
  4. data/CLAUDE.md +21 -0
  5. data/README.md +360 -0
  6. data/Rakefile +16 -0
  7. data/TODO.md +12 -0
  8. data/agent_c.gemspec +38 -0
  9. data/docs/chat-methods.md +157 -0
  10. data/docs/cost-reporting.md +86 -0
  11. data/docs/pipeline-tips-and-tricks.md +71 -0
  12. data/docs/session-configuration.md +274 -0
  13. data/docs/testing.md +747 -0
  14. data/docs/tools.md +103 -0
  15. data/docs/versioned-store.md +840 -0
  16. data/lib/agent_c/agent/chat.rb +211 -0
  17. data/lib/agent_c/agent/chat_response.rb +32 -0
  18. data/lib/agent_c/agent/chats/anthropic_bedrock.rb +48 -0
  19. data/lib/agent_c/batch.rb +102 -0
  20. data/lib/agent_c/configs/repo.rb +90 -0
  21. data/lib/agent_c/context.rb +56 -0
  22. data/lib/agent_c/costs/data.rb +39 -0
  23. data/lib/agent_c/costs/report.rb +219 -0
  24. data/lib/agent_c/db/store.rb +162 -0
  25. data/lib/agent_c/errors.rb +19 -0
  26. data/lib/agent_c/pipeline.rb +188 -0
  27. data/lib/agent_c/processor.rb +98 -0
  28. data/lib/agent_c/prompts.yml +53 -0
  29. data/lib/agent_c/schema.rb +85 -0
  30. data/lib/agent_c/session.rb +207 -0
  31. data/lib/agent_c/store.rb +72 -0
  32. data/lib/agent_c/test_helpers.rb +173 -0
  33. data/lib/agent_c/tools/dir_glob.rb +46 -0
  34. data/lib/agent_c/tools/edit_file.rb +112 -0
  35. data/lib/agent_c/tools/file_metadata.rb +43 -0
  36. data/lib/agent_c/tools/grep.rb +119 -0
  37. data/lib/agent_c/tools/paths.rb +36 -0
  38. data/lib/agent_c/tools/read_file.rb +94 -0
  39. data/lib/agent_c/tools/run_rails_test.rb +87 -0
  40. data/lib/agent_c/tools.rb +60 -0
  41. data/lib/agent_c/utils/git.rb +75 -0
  42. data/lib/agent_c/utils/shell.rb +58 -0
  43. data/lib/agent_c/version.rb +5 -0
  44. data/lib/agent_c.rb +32 -0
  45. data/lib/versioned_store/base.rb +314 -0
  46. data/lib/versioned_store/config.rb +26 -0
  47. data/lib/versioned_store/stores/schema.rb +127 -0
  48. data/lib/versioned_store/version.rb +5 -0
  49. data/lib/versioned_store.rb +5 -0
  50. data/template/Gemfile +9 -0
  51. data/template/Gemfile.lock +152 -0
  52. data/template/README.md +61 -0
  53. data/template/Rakefile +50 -0
  54. data/template/bin/rake +27 -0
  55. data/template/lib/autoload.rb +10 -0
  56. data/template/lib/config.rb +59 -0
  57. data/template/lib/pipeline.rb +19 -0
  58. data/template/lib/prompts.yml +57 -0
  59. data/template/lib/store.rb +17 -0
  60. data/template/test/pipeline_test.rb +221 -0
  61. data/template/test/test_helper.rb +18 -0
  62. 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
@@ -0,0 +1,10 @@
1
+ AllCops:
2
+ Exclude: [ "bin/*" ]
3
+ TargetRubyVersion: 3.2
4
+ DisabledByDefault: true
5
+
6
+ Style/FrozenStringLiteralComment:
7
+ Enabled: true
8
+
9
+ Layout/EmptyLineAfterMagicComment:
10
+ Enabled: true
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.