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,211 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ruby_llm"
|
|
4
|
+
require "json-schema"
|
|
5
|
+
|
|
6
|
+
module AgentC
|
|
7
|
+
module Agent
|
|
8
|
+
class Chat
|
|
9
|
+
attr_reader :id, :logger, :tools, :cached_prompts, :session
|
|
10
|
+
|
|
11
|
+
def initialize(
|
|
12
|
+
tools: Tools.all,
|
|
13
|
+
cached_prompts: [],
|
|
14
|
+
workspace_dir: nil,
|
|
15
|
+
record:, # Can be used for testing or continuing existing chats
|
|
16
|
+
session: nil
|
|
17
|
+
)
|
|
18
|
+
@session = session
|
|
19
|
+
@logger = session&.config&.logger
|
|
20
|
+
@tools = tools.map do
|
|
21
|
+
if _1.is_a?(Symbol)
|
|
22
|
+
Tools.resolve(
|
|
23
|
+
value: _1,
|
|
24
|
+
available_tools: Tools::NAMES.merge(session&.config&.extra_tools || {}),
|
|
25
|
+
args: {},
|
|
26
|
+
workspace_dir: workspace_dir || session&.config&.workspace_dir
|
|
27
|
+
)
|
|
28
|
+
else
|
|
29
|
+
_1
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
@cached_prompts = cached_prompts
|
|
33
|
+
@record_param = record
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def ask(msg)
|
|
37
|
+
record.ask(msg)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def id
|
|
41
|
+
record.id
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def to_h
|
|
45
|
+
record
|
|
46
|
+
.messages
|
|
47
|
+
.map {
|
|
48
|
+
hash = _1.to_llm.to_h
|
|
49
|
+
|
|
50
|
+
if hash.key?(:tool_calls)
|
|
51
|
+
hash[:tool_calls] = hash.fetch(:tool_calls).values.map(&:to_h)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
hash
|
|
55
|
+
}
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
NONE = Object.new
|
|
59
|
+
|
|
60
|
+
def refine(msg, schema:, times: 2)
|
|
61
|
+
schema = normalize_schema(schema)
|
|
62
|
+
|
|
63
|
+
requests = 0
|
|
64
|
+
last_answer = NONE
|
|
65
|
+
|
|
66
|
+
while(requests < times)
|
|
67
|
+
requests += 1
|
|
68
|
+
full_msg = (
|
|
69
|
+
if last_answer == NONE
|
|
70
|
+
msg
|
|
71
|
+
else
|
|
72
|
+
I18n.t(
|
|
73
|
+
"agent.chat.refine.system_message",
|
|
74
|
+
prior_answer: last_answer,
|
|
75
|
+
original_message: msg
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
last_answer = get_result(full_msg, schema:)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
last_answer
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def get(msg, schema: nil, confirm: 1, out_of: 1)
|
|
87
|
+
requests = 0
|
|
88
|
+
answers = []
|
|
89
|
+
|
|
90
|
+
confirmed_answer = NONE
|
|
91
|
+
|
|
92
|
+
while(requests < out_of && confirmed_answer == NONE)
|
|
93
|
+
requests += 1
|
|
94
|
+
answers << get_result(msg, schema:)
|
|
95
|
+
|
|
96
|
+
current_result = answers.group_by { _1 }.find { _2.count >= confirm }
|
|
97
|
+
|
|
98
|
+
if current_result
|
|
99
|
+
confirmed_answer = current_result.first
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
if confirmed_answer == NONE
|
|
104
|
+
raise "Unable to confirm an answer:\n#{JSON.pretty_generate(answers)}"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
confirmed_answer
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def messages(...)
|
|
111
|
+
record.messages(...)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def record
|
|
115
|
+
@record ||= begin
|
|
116
|
+
# If a record was injected (for continuing existing chat), use it
|
|
117
|
+
# Otherwise create a new chat
|
|
118
|
+
|
|
119
|
+
# Set up callbacks and tools
|
|
120
|
+
@record_param
|
|
121
|
+
.with_tools(*tools.flatten)
|
|
122
|
+
.on_new_message { log("message start") }
|
|
123
|
+
.on_end_message do |message|
|
|
124
|
+
log("message end: #{serialize_for_log(message).to_json}")
|
|
125
|
+
end
|
|
126
|
+
.on_tool_call { log("tool_call: #{serialize_for_log(_1).to_json}") }
|
|
127
|
+
.on_tool_result { log("tool_result: #{_1}") }
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
def normalize_schema(schema)
|
|
134
|
+
if schema.is_a?(Schema::AnyOneOf)
|
|
135
|
+
schema
|
|
136
|
+
elsif schema.is_a?(Array)
|
|
137
|
+
Schema::AnyOneOf.new(*schema)
|
|
138
|
+
else
|
|
139
|
+
Schema::AnyOneOf.new(schema)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def log(msg)
|
|
144
|
+
logger&.debug("chat-#{id}: #{msg}")
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def serialize_for_log(message)
|
|
148
|
+
hash = message.to_h
|
|
149
|
+
|
|
150
|
+
return hash unless hash.key?(:tool_calls)
|
|
151
|
+
|
|
152
|
+
hash.merge(
|
|
153
|
+
tool_calls: hash.fetch(:tool_calls).values.map(&:to_h)
|
|
154
|
+
)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def get_result(msg, schema:)
|
|
158
|
+
tries = 0
|
|
159
|
+
success = false
|
|
160
|
+
json_schema = schema&.to_json_schema&.fetch(:schema)
|
|
161
|
+
result = nil
|
|
162
|
+
|
|
163
|
+
message = I18n.t("agent.chat.get_result.json_instructions")
|
|
164
|
+
|
|
165
|
+
if schema
|
|
166
|
+
message += I18n.t(
|
|
167
|
+
"agent.chat.get_result.schema_requirement",
|
|
168
|
+
json_schema: json_schema.to_json
|
|
169
|
+
)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
message += I18n.t("agent.chat.get_result.request_wrapper", msg: msg)
|
|
173
|
+
|
|
174
|
+
while (!success && tries < 5)
|
|
175
|
+
begin
|
|
176
|
+
response = ask(message)
|
|
177
|
+
json = JSON.parse(response.content.sub(/\A```json/, "").sub(/```\Z/, ""))
|
|
178
|
+
|
|
179
|
+
if json_schema.nil? || JSON::Validator.validate(json_schema, json)
|
|
180
|
+
success = true
|
|
181
|
+
result = json
|
|
182
|
+
else
|
|
183
|
+
errors = (
|
|
184
|
+
begin
|
|
185
|
+
JSON::Validator.fully_validate(json_schema, json)
|
|
186
|
+
rescue => e
|
|
187
|
+
e.message
|
|
188
|
+
end
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
message = I18n.t(
|
|
192
|
+
"agent.chat.get_result.schema_validation_error",
|
|
193
|
+
errors: errors
|
|
194
|
+
)
|
|
195
|
+
end
|
|
196
|
+
rescue JSON::ParserError => e
|
|
197
|
+
message = I18n.t("agent.chat.get_result.json_parse_error")
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
tries += 1
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
raise "Failed to get valid response" if !success
|
|
204
|
+
|
|
205
|
+
result
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AgentC
|
|
4
|
+
module Agent
|
|
5
|
+
class ChatResponse
|
|
6
|
+
attr_reader :chat_id, :raw_response
|
|
7
|
+
|
|
8
|
+
def initialize(chat_id:, raw_response:)
|
|
9
|
+
@chat_id = chat_id
|
|
10
|
+
@raw_response = raw_response
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def success?
|
|
14
|
+
!@raw_response.key?("unable_to_fulfill_request_error")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def status
|
|
18
|
+
if success?
|
|
19
|
+
"success"
|
|
20
|
+
else
|
|
21
|
+
"error"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def data
|
|
26
|
+
raise "Cannot call data on failed response. Use error_message instead." unless success?
|
|
27
|
+
|
|
28
|
+
raw_response
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def error_message
|
|
32
|
+
raise "Cannot call error_message on successful response. Use data instead." if success?
|
|
33
|
+
|
|
34
|
+
raw_response.fetch("unable_to_fulfill_request_error")
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AgentC
|
|
4
|
+
module Agent
|
|
5
|
+
module Chats
|
|
6
|
+
module AnthropicBedrock
|
|
7
|
+
def self.create(project:, run_id:, prompts:, agent_store:, ruby_llm_context:)
|
|
8
|
+
ruby_llm_config = ruby_llm_context.config
|
|
9
|
+
|
|
10
|
+
chat = agent_store.chat
|
|
11
|
+
.create!(
|
|
12
|
+
model: agent_store.model.find_or_create_by!(
|
|
13
|
+
model_id: ruby_llm_config.default_model,
|
|
14
|
+
provider: "bedrock"
|
|
15
|
+
) { |m|
|
|
16
|
+
m.name = "Claude Sonnet 4.5"
|
|
17
|
+
m.family = "claude"
|
|
18
|
+
},
|
|
19
|
+
project: project,
|
|
20
|
+
run_id: run_id
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Set the context on the chat record so to_llm can use it
|
|
24
|
+
chat.define_singleton_method(:context) { ruby_llm_context }
|
|
25
|
+
|
|
26
|
+
chat.tap { |chat|
|
|
27
|
+
if prompts.any?
|
|
28
|
+
# WARN -- RubyLLM: Anthropic's Claude implementation only supports
|
|
29
|
+
# a single system message. Multiple system messages will be
|
|
30
|
+
# combined into one.
|
|
31
|
+
shared_prompt = prompts.join("\n---\n")
|
|
32
|
+
chat.messages.create!(
|
|
33
|
+
role: :system,
|
|
34
|
+
content_raw: [
|
|
35
|
+
{
|
|
36
|
+
"type" => "text",
|
|
37
|
+
"text" => shared_prompt,
|
|
38
|
+
"cache_control" => { "type" => "ephemeral" }
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AgentC
|
|
4
|
+
class Batch
|
|
5
|
+
def initialize(
|
|
6
|
+
store:,
|
|
7
|
+
workspace: nil,
|
|
8
|
+
repo: nil,
|
|
9
|
+
session:,
|
|
10
|
+
record_type:,
|
|
11
|
+
pipeline:,
|
|
12
|
+
git: ->(dir) { Utils::Git.new(dir) }
|
|
13
|
+
)
|
|
14
|
+
# for context
|
|
15
|
+
@store_config = store
|
|
16
|
+
@workspace_config = workspace
|
|
17
|
+
@repo_config = repo
|
|
18
|
+
@session_config = session
|
|
19
|
+
|
|
20
|
+
# for Batch
|
|
21
|
+
@record_type = record_type
|
|
22
|
+
@pipeline_class = pipeline
|
|
23
|
+
@git = git
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def call(&)
|
|
27
|
+
processor.call(&)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def add_task(record)
|
|
31
|
+
processor.add_task(record, @record_type)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def abort!
|
|
35
|
+
processor.abort!
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def report
|
|
39
|
+
out = StringIO.new
|
|
40
|
+
|
|
41
|
+
tasks = store.task.all
|
|
42
|
+
succeeded_count = tasks.count { |task| task.done? }
|
|
43
|
+
pending_count = tasks.count { |task| task.pending? }
|
|
44
|
+
failed_count = tasks.count { |task| task.failed? }
|
|
45
|
+
|
|
46
|
+
out.puts "Succeeded: #{succeeded_count}"
|
|
47
|
+
out.puts "Pending: #{pending_count}"
|
|
48
|
+
out.puts "Failed: #{failed_count}"
|
|
49
|
+
|
|
50
|
+
cost_data = session.cost
|
|
51
|
+
out.puts "Run cost: $#{'%.2f' % cost_data.run}"
|
|
52
|
+
out.puts "Project total cost: $#{'%.2f' % cost_data.project}"
|
|
53
|
+
|
|
54
|
+
if failed_count > 0
|
|
55
|
+
out.puts "\nFirst #{[failed_count, 3].min} failed task(s):"
|
|
56
|
+
tasks.select { |task| task.failed? }.first(3).each do |task|
|
|
57
|
+
out.puts "- #{task.error_message}"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
out.string
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def store
|
|
65
|
+
context.store
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def workspaces
|
|
69
|
+
context.workspaces
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def session
|
|
73
|
+
context.session
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def processor
|
|
79
|
+
@processor ||= Processor.new(
|
|
80
|
+
context:,
|
|
81
|
+
handlers: {
|
|
82
|
+
@record_type => ->(task) {
|
|
83
|
+
@pipeline_class.call(
|
|
84
|
+
session:,
|
|
85
|
+
task:,
|
|
86
|
+
git: @git
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def context
|
|
94
|
+
@context ||= Context.new(
|
|
95
|
+
store: @store_config,
|
|
96
|
+
session: @session_config,
|
|
97
|
+
workspace: @workspace_config,
|
|
98
|
+
repo: @repo_config,
|
|
99
|
+
)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AgentC
|
|
4
|
+
module Configs
|
|
5
|
+
class Repo
|
|
6
|
+
attr_reader(
|
|
7
|
+
:dir,
|
|
8
|
+
:initial_revision,
|
|
9
|
+
:working_subdir,
|
|
10
|
+
:worktrees_root_dir,
|
|
11
|
+
:worktree_branch_prefix,
|
|
12
|
+
:worktree_envs,
|
|
13
|
+
:logger,
|
|
14
|
+
)
|
|
15
|
+
def initialize(
|
|
16
|
+
dir:,
|
|
17
|
+
initial_revision:,
|
|
18
|
+
working_subdir: "",
|
|
19
|
+
worktrees_root_dir:,
|
|
20
|
+
worktree_branch_prefix:,
|
|
21
|
+
worktree_envs:,
|
|
22
|
+
logger:
|
|
23
|
+
)
|
|
24
|
+
@dir = dir
|
|
25
|
+
@initial_revision = initial_revision
|
|
26
|
+
@working_subdir = working_subdir
|
|
27
|
+
@worktrees_root_dir = worktrees_root_dir
|
|
28
|
+
@worktree_branch_prefix = worktree_branch_prefix
|
|
29
|
+
@worktree_envs = worktree_envs
|
|
30
|
+
@logger = logger
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def workspaces(store)
|
|
34
|
+
git = Utils::Git.new(dir)
|
|
35
|
+
logger.info("Checking worktrees")
|
|
36
|
+
|
|
37
|
+
worktree_configs.map { |spec|
|
|
38
|
+
worktree_dir = spec.fetch(:dir)
|
|
39
|
+
|
|
40
|
+
if store.workspace.where(dir: spec.fetch(:workspace_dir)).exists?
|
|
41
|
+
logger.info("worktree record exists at #{worktree_dir}, not creating/resetting worktree")
|
|
42
|
+
else
|
|
43
|
+
logger.info("creating/resetting worktree at: #{worktree_dir}")
|
|
44
|
+
git.create_worktree(
|
|
45
|
+
worktree_dir: worktree_dir,
|
|
46
|
+
branch: spec.fetch(:branch),
|
|
47
|
+
revision: initial_revision,
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
store
|
|
52
|
+
.workspace
|
|
53
|
+
.ensure_created!(
|
|
54
|
+
dir: spec.fetch(:workspace_dir),
|
|
55
|
+
env: spec.fetch(:env)
|
|
56
|
+
)
|
|
57
|
+
}.tap { logger.info("done checking worktrees")}
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def create_worktrees(store)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def workspace_configs
|
|
66
|
+
worktree_configs.map { _1.slice(:env, :dir) }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def worktree_configs
|
|
72
|
+
@envs_with_paths ||= (
|
|
73
|
+
worktree_envs
|
|
74
|
+
.each_with_index
|
|
75
|
+
.map { |env, i|
|
|
76
|
+
branch = "#{worktree_branch_prefix}-#{i}"
|
|
77
|
+
dir = File.join(worktrees_root_dir, branch)
|
|
78
|
+
|
|
79
|
+
{
|
|
80
|
+
env:,
|
|
81
|
+
dir: ,
|
|
82
|
+
branch:,
|
|
83
|
+
workspace_dir: File.join(dir, working_subdir)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AgentC
|
|
4
|
+
class Context
|
|
5
|
+
attr_reader :config
|
|
6
|
+
def initialize(store:, session:, repo: nil, workspace: nil)
|
|
7
|
+
raise ArgumentError.new("must pass workspace or repo") unless workspace || repo
|
|
8
|
+
|
|
9
|
+
@store_config = store
|
|
10
|
+
@session_config = session
|
|
11
|
+
@workspace_config = workspace
|
|
12
|
+
@repo_config = repo
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def store
|
|
16
|
+
@store ||= (
|
|
17
|
+
if @store_config.is_a?(Hash)
|
|
18
|
+
@store_config.fetch(:class).new(**@store_config.fetch(:config))
|
|
19
|
+
else
|
|
20
|
+
@store_config
|
|
21
|
+
end
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def workspace
|
|
26
|
+
raise "Multiple workspaces configured" unless workspaces.count == 1
|
|
27
|
+
|
|
28
|
+
workspaces.first
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def workspaces
|
|
32
|
+
@workspaces ||= (
|
|
33
|
+
if @workspace_config.is_a?(Hash)
|
|
34
|
+
[store.workspace.ensure_created!(**@workspace_config)]
|
|
35
|
+
elsif @repo_config
|
|
36
|
+
# Note: This method provision the worktrees if they don't exist
|
|
37
|
+
Configs::Repo.new(logger: session.logger, **@repo_config).workspaces(store)
|
|
38
|
+
elsif @workspace_config.is_a?(Array)
|
|
39
|
+
@workspace_config
|
|
40
|
+
else
|
|
41
|
+
[@workspace_config]
|
|
42
|
+
end
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def session
|
|
47
|
+
@session ||= (
|
|
48
|
+
if @session_config.is_a?(Hash)
|
|
49
|
+
Session.new(**@session_config)
|
|
50
|
+
else
|
|
51
|
+
@session_config
|
|
52
|
+
end
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AgentC
|
|
4
|
+
module Costs
|
|
5
|
+
class Data
|
|
6
|
+
attr_reader :project, :run
|
|
7
|
+
|
|
8
|
+
def initialize(project:, run:)
|
|
9
|
+
@project = project
|
|
10
|
+
@run = run
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.calculate(agent_store:, project:, run_id: nil)
|
|
14
|
+
calculator = Report::Calculator.new
|
|
15
|
+
|
|
16
|
+
# Calculate project-level cost
|
|
17
|
+
project_messages = agent_store.message
|
|
18
|
+
.joins(:chat)
|
|
19
|
+
.where(chats: { project: project })
|
|
20
|
+
.includes(:model, :chat)
|
|
21
|
+
project_stats = calculator.calculate(project_messages)
|
|
22
|
+
project_cost = project_stats[:total_cost]
|
|
23
|
+
|
|
24
|
+
# Calculate run-level cost
|
|
25
|
+
run_cost = 0.0
|
|
26
|
+
if run_id
|
|
27
|
+
run_messages = agent_store.message
|
|
28
|
+
.joins(:chat)
|
|
29
|
+
.where(chats: { project: project, run_id: run_id })
|
|
30
|
+
.includes(:model, :chat)
|
|
31
|
+
run_stats = calculator.calculate(run_messages)
|
|
32
|
+
run_cost = run_stats[:total_cost]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
new(project: project_cost, run: run_cost)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|