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
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AgentC
|
|
4
|
+
module Pipelines
|
|
5
|
+
class Agent
|
|
6
|
+
attr_reader :pipeline
|
|
7
|
+
def initialize(pipeline)
|
|
8
|
+
@pipeline = pipeline
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def agent_review_loop(
|
|
12
|
+
name,
|
|
13
|
+
max_tries: 10,
|
|
14
|
+
implement: [],
|
|
15
|
+
iterate: implement,
|
|
16
|
+
review:
|
|
17
|
+
)
|
|
18
|
+
implement = Array(implement)
|
|
19
|
+
iterate = Array(iterate)
|
|
20
|
+
review = Array(review)
|
|
21
|
+
|
|
22
|
+
unless implement.any? || iterate.any?
|
|
23
|
+
raise ArgumentError.new("Must pass implement or iterate prompts")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
tries = 0
|
|
27
|
+
review_passed = false
|
|
28
|
+
feedbacks = []
|
|
29
|
+
|
|
30
|
+
while(tries < max_tries && !review_passed && !task.failed?)
|
|
31
|
+
if tries == 0
|
|
32
|
+
implement.each do |name|
|
|
33
|
+
apply_prompt(name)
|
|
34
|
+
break if task.failed?
|
|
35
|
+
end
|
|
36
|
+
else
|
|
37
|
+
iterate.each do |name|
|
|
38
|
+
apply_prompt(
|
|
39
|
+
name,
|
|
40
|
+
additional_i18n_attributes: {
|
|
41
|
+
feedback: feedbacks.join("\n---\n")
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
break if task.failed?
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
tries += 1
|
|
49
|
+
|
|
50
|
+
unless task.failed?
|
|
51
|
+
feedbacks = []
|
|
52
|
+
diff = git.diff
|
|
53
|
+
review.each do |name|
|
|
54
|
+
params = i18n_params(
|
|
55
|
+
name,
|
|
56
|
+
additional_i18n_attributes: {
|
|
57
|
+
diff:
|
|
58
|
+
},
|
|
59
|
+
).merge(
|
|
60
|
+
schema: -> {
|
|
61
|
+
boolean(:approved)
|
|
62
|
+
string(:feedback)
|
|
63
|
+
},
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
result = prompt(name, **params)
|
|
67
|
+
|
|
68
|
+
if result.success?
|
|
69
|
+
if !result.data.fetch("approved")
|
|
70
|
+
feedbacks << result.data.fetch("feedback")
|
|
71
|
+
end
|
|
72
|
+
else
|
|
73
|
+
task.fail!(result.error_message)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
if record.respond_to?(:add_review)
|
|
78
|
+
record.add_review(diff:, feedbacks:)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
review_passed = feedbacks.empty?
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def agent_step(name, **params, &block)
|
|
87
|
+
apply_prompt(name, **params, &block)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def record
|
|
93
|
+
pipeline.record
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def task
|
|
97
|
+
pipeline.task
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def workspace
|
|
101
|
+
pipeline.workspace
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def session
|
|
105
|
+
pipeline.session
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def git
|
|
109
|
+
pipeline.git
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def apply_prompt(...)
|
|
113
|
+
result = prompt(...)
|
|
114
|
+
|
|
115
|
+
if result.success?
|
|
116
|
+
task.record.update!(result.data)
|
|
117
|
+
else
|
|
118
|
+
task.fail!(result.error_message)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def prompt(
|
|
123
|
+
name,
|
|
124
|
+
additional_i18n_attributes: {},
|
|
125
|
+
**params,
|
|
126
|
+
&block
|
|
127
|
+
)
|
|
128
|
+
resolved_params = (
|
|
129
|
+
if block
|
|
130
|
+
instance_exec(&block)
|
|
131
|
+
elsif params.empty?
|
|
132
|
+
i18n_params(name, additional_i18n_attributes:)
|
|
133
|
+
else
|
|
134
|
+
i18n_attributes = (
|
|
135
|
+
if record.respond_to?(:i18n_attributes)
|
|
136
|
+
record.i18n_attributes
|
|
137
|
+
else
|
|
138
|
+
record.attributes
|
|
139
|
+
end
|
|
140
|
+
).merge(additional_i18n_attributes)
|
|
141
|
+
|
|
142
|
+
{
|
|
143
|
+
tool_args: {
|
|
144
|
+
workspace_dir: workspace.dir,
|
|
145
|
+
env: workspace.env,
|
|
146
|
+
}
|
|
147
|
+
}.tap { |hash|
|
|
148
|
+
if params.key?(:prompt_key)
|
|
149
|
+
hash[:prompt] = I18n.t(params[:prompt_key], **i18n_attributes.symbolize_keys)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
if params.key?(:cached_prompt_keys)
|
|
153
|
+
hash[:cached_prompt] = params[:cached_prompt_keys].map { I18n.t(_1) }
|
|
154
|
+
end
|
|
155
|
+
}.merge(params.except(:cached_prompt_keys, :prompt_key))
|
|
156
|
+
end
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
session.prompt(
|
|
160
|
+
on_chat_created: -> (id) { task.chat_ids << id},
|
|
161
|
+
**resolved_params
|
|
162
|
+
)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def i18n_params(name, additional_i18n_attributes: {})
|
|
166
|
+
i18n_attributes = (
|
|
167
|
+
if record.respond_to?(:i18n_attributes)
|
|
168
|
+
record.i18n_attributes
|
|
169
|
+
else
|
|
170
|
+
record.attributes
|
|
171
|
+
end
|
|
172
|
+
).merge(additional_i18n_attributes)
|
|
173
|
+
|
|
174
|
+
{
|
|
175
|
+
tool_args: {
|
|
176
|
+
workspace_dir: workspace.dir,
|
|
177
|
+
env: workspace.env,
|
|
178
|
+
},
|
|
179
|
+
cached_prompt: (
|
|
180
|
+
if I18n.exists?("#{name}.cached_prompts")
|
|
181
|
+
I18n.t("#{name}.cached_prompts")
|
|
182
|
+
else
|
|
183
|
+
[]
|
|
184
|
+
end
|
|
185
|
+
),
|
|
186
|
+
prompt: I18n.t("#{name}.prompt", **i18n_attributes.symbolize_keys),
|
|
187
|
+
tools: (
|
|
188
|
+
if I18n.exists?("#{name}.tools")
|
|
189
|
+
I18n.t("#{name}.tools")
|
|
190
|
+
else
|
|
191
|
+
[]
|
|
192
|
+
end
|
|
193
|
+
),
|
|
194
|
+
schema: -> {
|
|
195
|
+
next unless I18n.exists?("#{name}.response_schema")
|
|
196
|
+
|
|
197
|
+
I18n.t("#{name}.response_schema").each do |name, spec|
|
|
198
|
+
extra = spec.except(:required, :description, :type)
|
|
199
|
+
|
|
200
|
+
if extra.key?(:of)
|
|
201
|
+
extra[:of] = extra[:of]&.to_sym
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
send(
|
|
205
|
+
spec.fetch(:type, "string"),
|
|
206
|
+
name,
|
|
207
|
+
required: spec.fetch(:required, true),
|
|
208
|
+
description: spec.fetch(:description),
|
|
209
|
+
**extra
|
|
210
|
+
)
|
|
211
|
+
end
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
rescue => e
|
|
215
|
+
binding.irb
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AgentC
|
|
4
|
+
class Processor
|
|
5
|
+
Handler = Data.define(:task, :handler) do
|
|
6
|
+
def call
|
|
7
|
+
handler.call(task)
|
|
8
|
+
if task.pending?
|
|
9
|
+
raise "Task Pending error"
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
attr_reader :context, :handlers
|
|
15
|
+
def initialize(context:, handlers:)
|
|
16
|
+
@context = context
|
|
17
|
+
@handlers = handlers.transform_keys(&:to_s)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def add_task(record, handler)
|
|
21
|
+
raise ArgumentError.new("invalid handler") unless handlers.include?(handler.to_s)
|
|
22
|
+
|
|
23
|
+
store.task.find_or_create_by!(record:, handler:)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def call(&)
|
|
27
|
+
raise "must provide at least one workspace" if workspace_count == 0
|
|
28
|
+
|
|
29
|
+
if workspace_count == 1
|
|
30
|
+
call_synchronous(context.workspaces.first, &)
|
|
31
|
+
else
|
|
32
|
+
call_asynchronous(&)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def abort!
|
|
37
|
+
@abort = true
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def abort?
|
|
41
|
+
@abort
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def call_asynchronous(&)
|
|
47
|
+
error = nil
|
|
48
|
+
|
|
49
|
+
Async { |task|
|
|
50
|
+
semaphore = Async::Semaphore.new(workspace_count)
|
|
51
|
+
|
|
52
|
+
context.workspaces.map { |workspace|
|
|
53
|
+
semaphore.async do
|
|
54
|
+
call_synchronous(workspace, &)
|
|
55
|
+
rescue => e
|
|
56
|
+
abort!
|
|
57
|
+
error = e
|
|
58
|
+
end
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
}.wait
|
|
62
|
+
|
|
63
|
+
raise error if error
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def call_synchronous(workspace)
|
|
67
|
+
while(handler = next_handler(workspace))
|
|
68
|
+
handler.call
|
|
69
|
+
yield(handler.task) if block_given? # allow the invoker to do work inbetween handler calls
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def next_handler(workspace)
|
|
74
|
+
return nil if abort?
|
|
75
|
+
|
|
76
|
+
task = (
|
|
77
|
+
store
|
|
78
|
+
.task
|
|
79
|
+
.where("workspace_id = ? OR workspace_id IS NULL", workspace.id)
|
|
80
|
+
.order("created_at ASC")
|
|
81
|
+
.find_by(status: :pending)
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
if task
|
|
85
|
+
task.update!(workspace:) unless task.workspace
|
|
86
|
+
Handler.new(task:, handler: handlers.fetch(task.handler))
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def workspace_count
|
|
91
|
+
@workspace_count ||= context.workspaces.count
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def store
|
|
95
|
+
context.store
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
en:
|
|
2
|
+
agent:
|
|
3
|
+
chat:
|
|
4
|
+
refine:
|
|
5
|
+
system_message: |
|
|
6
|
+
---BEGIN-SYSTEM-MESSAGE---
|
|
7
|
+
Hello, this conversation is currently being "refined". That means,
|
|
8
|
+
this prompt was already asked to an AI and an answer was received.
|
|
9
|
+
|
|
10
|
+
The user is requesting that you "refine" the answer given by the
|
|
11
|
+
prior AI conversation. Your goal is to review the prior answer,
|
|
12
|
+
evaluate its accuracy. If you determine that some changes should be
|
|
13
|
+
made, then adjust the answer and respond with the corrected version.
|
|
14
|
+
It is possible that the prior answer is accurate and acceptable. In
|
|
15
|
+
that case, simply respond with the prior answer as-is.
|
|
16
|
+
|
|
17
|
+
Here is the answer previously given to this prompt:
|
|
18
|
+
|
|
19
|
+
---BEGIN-PRIOR-ANSWER---
|
|
20
|
+
%{prior_answer}
|
|
21
|
+
---END-PRIOR-ANSWER---
|
|
22
|
+
---END-SYSTEM-MESSAGE---
|
|
23
|
+
|
|
24
|
+
%{original_message}
|
|
25
|
+
get_result:
|
|
26
|
+
json_instructions: |
|
|
27
|
+
Your response will be parsed as JSON directly by passing it to JSON.parse.
|
|
28
|
+
|
|
29
|
+
DO NOT add any commentary to the JSON response.
|
|
30
|
+
DO NOT format your response as markdown
|
|
31
|
+
DO NOT begin your response with "```" OR "```json"
|
|
32
|
+
|
|
33
|
+
YOUR RESPONSE SHOULD BE PURE JSON AND BEGIN WITH ONE OF THE FOLLOWING: {["
|
|
34
|
+
schema_requirement: |
|
|
35
|
+
Your answer to the following request must be JSON that adheres to
|
|
36
|
+
the following schema.
|
|
37
|
+
|
|
38
|
+
---BEGIN-JSON-SCHEMA---
|
|
39
|
+
%{json_schema}
|
|
40
|
+
---END-JSON-SCHEMA---
|
|
41
|
+
request_wrapper: |
|
|
42
|
+
---BEGIN-REQUEST---
|
|
43
|
+
%{msg}
|
|
44
|
+
---END-REQUEST---
|
|
45
|
+
schema_validation_error: |
|
|
46
|
+
Your response did not adhere to the given schema. Its errors:
|
|
47
|
+
%{errors}
|
|
48
|
+
json_parse_error: |
|
|
49
|
+
Your response failed to parse as JSON. RESPOND WITH ONLY VALID JSON
|
|
50
|
+
NOTHING ELSE.
|
|
51
|
+
task_1:
|
|
52
|
+
pending: |
|
|
53
|
+
Translate the word "%{word}" to %{language}.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ruby_llm/schema"
|
|
4
|
+
|
|
5
|
+
module AgentC
|
|
6
|
+
module Schema
|
|
7
|
+
class AnyOneOf
|
|
8
|
+
attr_reader :schemas
|
|
9
|
+
def initialize(*schemas)
|
|
10
|
+
@schemas = schemas
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def to_nested_schema
|
|
14
|
+
to_json_schema.fetch(:schema).fetch(:oneOf)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def schema_jsons
|
|
18
|
+
@schema_jsons ||= (
|
|
19
|
+
schemas.flat_map do |schema|
|
|
20
|
+
if schema.is_a?(AnyOneOf)
|
|
21
|
+
to_nested_schema
|
|
22
|
+
elsif schema.is_a?(Hash)
|
|
23
|
+
schema
|
|
24
|
+
elsif schema.ancestors.include?(RubyLLM::Schema)
|
|
25
|
+
schema.new.to_json_schema.fetch(:schema)
|
|
26
|
+
else
|
|
27
|
+
raise ArgumentError, "Invalid schema class: #{schema}"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def to_json_schema
|
|
34
|
+
{
|
|
35
|
+
schema: {
|
|
36
|
+
oneOf: schema_jsons
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class ErrorSchema < RubyLLM::Schema
|
|
43
|
+
string(
|
|
44
|
+
:unable_to_fulfill_request_error,
|
|
45
|
+
description: <<~TXT
|
|
46
|
+
Only fill out this field if you are unable to perform the requested
|
|
47
|
+
task and/or unable to fulfill the other schema provided.
|
|
48
|
+
|
|
49
|
+
Fill this in a clear message indicating why you were unable to fulfill
|
|
50
|
+
the request.
|
|
51
|
+
TXT
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.result(schema: nil, &)
|
|
56
|
+
# Create the success schema
|
|
57
|
+
success_schema = (
|
|
58
|
+
if block_given? || schema&.respond_to?(:call)
|
|
59
|
+
Class.new(RubyLLM::Schema) do
|
|
60
|
+
instance_exec(&) if block_given?
|
|
61
|
+
instance_exec(&schema) if schema
|
|
62
|
+
end
|
|
63
|
+
else
|
|
64
|
+
schema
|
|
65
|
+
end
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
AnyOneOf.new(success_schema, ErrorSchema)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module AgentC
|
|
6
|
+
class Session
|
|
7
|
+
Configuration = Data.define(
|
|
8
|
+
:agent_db_path,
|
|
9
|
+
:logger,
|
|
10
|
+
:i18n_path,
|
|
11
|
+
:workspace_dir,
|
|
12
|
+
:project,
|
|
13
|
+
:run_id,
|
|
14
|
+
:max_spend_project,
|
|
15
|
+
:max_spend_run,
|
|
16
|
+
:extra_tools,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
attr_reader :logger
|
|
20
|
+
|
|
21
|
+
def initialize(
|
|
22
|
+
agent_db_path:,
|
|
23
|
+
project:,
|
|
24
|
+
logger: Logger.new("/dev/null"),
|
|
25
|
+
workspace_dir: Dir.pwd,
|
|
26
|
+
run_id: Time.now.to_i,
|
|
27
|
+
i18n_path: nil,
|
|
28
|
+
max_spend_project: nil,
|
|
29
|
+
max_spend_run: nil,
|
|
30
|
+
ruby_llm: {},
|
|
31
|
+
extra_tools: {},
|
|
32
|
+
chat_provider: ->(**params) { create_chat(**params) }
|
|
33
|
+
)
|
|
34
|
+
@agent_db_path = agent_db_path
|
|
35
|
+
@project = project
|
|
36
|
+
@logger = logger
|
|
37
|
+
@workspace_dir = workspace_dir
|
|
38
|
+
@run_id = run_id
|
|
39
|
+
@i18n_path = i18n_path
|
|
40
|
+
@max_spend_project = max_spend_project
|
|
41
|
+
@max_spend_run = max_spend_run
|
|
42
|
+
@ruby_llm = ruby_llm
|
|
43
|
+
|
|
44
|
+
unless extra_tools.is_a?(Hash)
|
|
45
|
+
raise ArgumentError, "extra_tools must be a hash mapping name to class or instance"
|
|
46
|
+
end
|
|
47
|
+
@extra_tools = extra_tools
|
|
48
|
+
|
|
49
|
+
@chat_provider = chat_provider
|
|
50
|
+
|
|
51
|
+
unless agent_db_path.match(/.sqlite3?$/)
|
|
52
|
+
raise ArgumentError, "agent_db_path must end with '.sqlite3' or '.sqlite'"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Load i18n path if provided
|
|
56
|
+
if i18n_path
|
|
57
|
+
I18n.load_path << i18n_path
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def chat(
|
|
62
|
+
tools: Tools::NAMES.keys,
|
|
63
|
+
cached_prompts: [],
|
|
64
|
+
workspace_dir: nil,
|
|
65
|
+
record: nil
|
|
66
|
+
)
|
|
67
|
+
@chat_provider.call(
|
|
68
|
+
tools: tools,
|
|
69
|
+
cached_prompts: cached_prompts,
|
|
70
|
+
workspace_dir: workspace_dir || config.workspace_dir,
|
|
71
|
+
record: record,
|
|
72
|
+
session: self
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def prompt(
|
|
77
|
+
tool_args: {},
|
|
78
|
+
tools: Tools::NAMES.keys + config.extra_tools.keys,
|
|
79
|
+
cached_prompt: [],
|
|
80
|
+
prompt:,
|
|
81
|
+
schema:,
|
|
82
|
+
on_chat_created: ->(*) {}
|
|
83
|
+
)
|
|
84
|
+
workspace_dir = tool_args[:workspace_dir] || config.workspace_dir
|
|
85
|
+
|
|
86
|
+
resolved_tools = (
|
|
87
|
+
tools
|
|
88
|
+
.map { |value|
|
|
89
|
+
Tools.resolve(
|
|
90
|
+
value:,
|
|
91
|
+
available_tools: Tools::NAMES.merge(config.extra_tools),
|
|
92
|
+
args: tool_args,
|
|
93
|
+
workspace_dir: config.workspace_dir
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
chat_instance = chat(
|
|
99
|
+
tools: resolved_tools,
|
|
100
|
+
cached_prompts: cached_prompt,
|
|
101
|
+
workspace_dir: workspace_dir
|
|
102
|
+
)
|
|
103
|
+
on_chat_created.call(chat_instance.id)
|
|
104
|
+
|
|
105
|
+
message = Array(prompt).join("\n")
|
|
106
|
+
|
|
107
|
+
begin
|
|
108
|
+
result = chat_instance.get(message, schema: Schema.result(&schema))
|
|
109
|
+
|
|
110
|
+
Agent::ChatResponse.new(
|
|
111
|
+
chat_id: chat_instance.id,
|
|
112
|
+
raw_response: result,
|
|
113
|
+
)
|
|
114
|
+
rescue => e
|
|
115
|
+
Agent::ChatResponse.new(
|
|
116
|
+
chat_id: chat_instance.id,
|
|
117
|
+
raw_response: {
|
|
118
|
+
"unable_to_fulfill_request_error" => ["#{e.class.name}:#{e.message}", e.backtrace].join("\n")
|
|
119
|
+
},
|
|
120
|
+
)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def config
|
|
125
|
+
@config ||= Configuration.new(
|
|
126
|
+
agent_db_path: @agent_db_path,
|
|
127
|
+
logger: @logger,
|
|
128
|
+
i18n_path: @i18n_path,
|
|
129
|
+
workspace_dir: @workspace_dir,
|
|
130
|
+
project: @project,
|
|
131
|
+
run_id: @run_id,
|
|
132
|
+
max_spend_project: @max_spend_project,
|
|
133
|
+
max_spend_run: @max_spend_run,
|
|
134
|
+
extra_tools: @extra_tools,
|
|
135
|
+
)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def ruby_llm_context
|
|
139
|
+
@ruby_llm_context ||= RubyLLM.context do |ctx_config|
|
|
140
|
+
@ruby_llm.each do |key, value|
|
|
141
|
+
ctx_config.public_send("#{key}=", value)
|
|
142
|
+
end
|
|
143
|
+
ctx_config.use_new_acts_as = true
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def agent_store
|
|
148
|
+
@agent_store ||= Db::Store.new(
|
|
149
|
+
path: @agent_db_path,
|
|
150
|
+
logger: @logger,
|
|
151
|
+
versioned: false
|
|
152
|
+
)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def cost
|
|
156
|
+
Costs::Data.calculate(
|
|
157
|
+
agent_store: agent_store,
|
|
158
|
+
project: config.project,
|
|
159
|
+
run_id: config.run_id
|
|
160
|
+
)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
private
|
|
164
|
+
|
|
165
|
+
def create_chat(**params)
|
|
166
|
+
real_record = params[:record] || (
|
|
167
|
+
Agent::Chats::AnthropicBedrock.create(
|
|
168
|
+
project: config.project,
|
|
169
|
+
run_id: config.run_id,
|
|
170
|
+
prompts: params.fetch(:cached_prompts, []),
|
|
171
|
+
agent_store:,
|
|
172
|
+
ruby_llm_context:
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
real_record.on_end_message do |message|
|
|
177
|
+
check_abort_cost
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
Agent::Chat.new(
|
|
181
|
+
**params.except(:record),
|
|
182
|
+
record: real_record
|
|
183
|
+
)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def check_abort_cost
|
|
187
|
+
return unless config.max_spend_project || config.max_spend_run
|
|
188
|
+
|
|
189
|
+
if config.max_spend_project && cost.project >= config.max_spend_project
|
|
190
|
+
raise Errors::AbortCostExceeded.new(
|
|
191
|
+
cost_type: "project",
|
|
192
|
+
current_cost: cost.project,
|
|
193
|
+
threshold: config.max_spend_project
|
|
194
|
+
)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
if config.max_spend_run && cost.run >= config.max_spend_run
|
|
198
|
+
raise Errors::AbortCostExceeded.new(
|
|
199
|
+
cost_type: "run",
|
|
200
|
+
current_cost: cost.run,
|
|
201
|
+
threshold: config.max_spend_run
|
|
202
|
+
)
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AgentC
|
|
4
|
+
module Store
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
record(:workspace) do
|
|
9
|
+
schema do |t|
|
|
10
|
+
t.string(:dir, null: false)
|
|
11
|
+
t.json(:env, default: [])
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.ensure_created!(dir:, env:)
|
|
15
|
+
find_or_create_by!(dir:).tap { _1.update!(env:) }
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
record(:task) do
|
|
20
|
+
schema do |t|
|
|
21
|
+
t.string(:status, default: "pending")
|
|
22
|
+
t.json(:completed_steps, default: [])
|
|
23
|
+
t.string(:record_type)
|
|
24
|
+
t.integer(:record_id)
|
|
25
|
+
t.references(:workspace)
|
|
26
|
+
|
|
27
|
+
t.string(:handler)
|
|
28
|
+
|
|
29
|
+
t.string(:error_message)
|
|
30
|
+
t.json(:chat_ids, default: [])
|
|
31
|
+
|
|
32
|
+
t.timestamps
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
belongs_to(
|
|
36
|
+
:record,
|
|
37
|
+
polymorphic: true,
|
|
38
|
+
required: false
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
belongs_to(
|
|
42
|
+
:workspace,
|
|
43
|
+
class_name: class_name(:workspace),
|
|
44
|
+
required: false
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def fail!(message)
|
|
48
|
+
update!(
|
|
49
|
+
status: "failed",
|
|
50
|
+
error_message: message
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def done!
|
|
55
|
+
update!(status: "done")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def done?
|
|
59
|
+
status == "done"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def failed?
|
|
63
|
+
status == "failed"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def pending?
|
|
67
|
+
status == "pending"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|