raif 1.3.0 → 1.5.0
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 +4 -4
- data/README.md +8 -7
- data/app/assets/builds/raif.css +4 -1
- data/app/assets/builds/raif_admin.css +52 -2
- data/app/assets/builds/raif_admin_sprockets.js +2709 -0
- data/app/assets/javascript/raif/admin/copy_to_clipboard_controller.js +132 -0
- data/app/assets/javascript/raif/admin/cost_estimate_controller.js +80 -0
- data/app/assets/javascript/raif/admin/judge_config_controller.js +23 -0
- data/app/assets/javascript/raif/admin/select_all_checkboxes_controller.js +33 -0
- data/app/assets/javascript/raif/admin/sortable_table_controller.js +51 -0
- data/app/assets/javascript/raif/admin/table_search_controller.js +15 -0
- data/app/assets/javascript/raif/admin/tom_select_controller.js +33 -0
- data/app/assets/javascript/raif/controllers/conversations_controller.js +1 -1
- data/app/assets/javascript/raif_admin.js +23 -0
- data/app/assets/javascript/raif_admin_sprockets.js +24 -0
- data/app/assets/stylesheets/raif/admin/conversation.scss +16 -0
- data/app/assets/stylesheets/raif/conversations.scss +3 -0
- data/app/assets/stylesheets/raif.scss +2 -1
- data/app/assets/stylesheets/raif_admin.scss +50 -1
- data/app/controllers/raif/admin/agents_controller.rb +27 -1
- data/app/controllers/raif/admin/application_controller.rb +16 -0
- data/app/controllers/raif/admin/configs_controller.rb +95 -0
- data/app/controllers/raif/admin/llms_controller.rb +27 -0
- data/app/controllers/raif/admin/model_completions_controller.rb +24 -1
- data/app/controllers/raif/admin/model_tool_invocations_controller.rb +7 -1
- data/app/controllers/raif/admin/prompt_studio/agents_controller.rb +25 -0
- data/app/controllers/raif/admin/prompt_studio/base_controller.rb +32 -0
- data/app/controllers/raif/admin/prompt_studio/batch_runs_controller.rb +102 -0
- data/app/controllers/raif/admin/prompt_studio/conversations_controller.rb +25 -0
- data/app/controllers/raif/admin/prompt_studio/tasks_controller.rb +64 -0
- data/app/controllers/raif/admin/stats/model_tool_invocations_controller.rb +21 -0
- data/app/controllers/raif/admin/stats/tasks_controller.rb +15 -6
- data/app/controllers/raif/admin/stats_controller.rb +32 -3
- data/app/controllers/raif/admin/tasks_controller.rb +5 -0
- data/app/controllers/raif/conversation_entries_controller.rb +1 -0
- data/app/controllers/raif/conversations_controller.rb +10 -2
- data/app/helpers/raif/application_helper.rb +40 -0
- data/app/jobs/raif/conversation_entry_job.rb +8 -6
- data/app/jobs/raif/prompt_studio_batch_run_item_job.rb +11 -0
- data/app/jobs/raif/prompt_studio_batch_run_job.rb +15 -0
- data/app/jobs/raif/prompt_studio_task_run_job.rb +36 -0
- data/app/models/raif/admin/task_stat.rb +7 -0
- data/app/models/raif/agent.rb +98 -6
- data/app/models/raif/agents/native_tool_calling_agent.rb +179 -52
- data/app/models/raif/application_record.rb +18 -0
- data/app/models/raif/concerns/agent_inference_stats.rb +35 -0
- data/app/models/raif/concerns/has_prompt_templates.rb +88 -0
- data/app/models/raif/concerns/has_runtime_duration.rb +41 -0
- data/app/models/raif/concerns/json_schema_definition.rb +54 -6
- data/app/models/raif/concerns/llm_prompt_caching.rb +20 -0
- data/app/models/raif/concerns/llms/anthropic/message_formatting.rb +34 -0
- data/app/models/raif/concerns/llms/anthropic/response_tool_calls.rb +24 -0
- data/app/models/raif/concerns/llms/anthropic/tool_formatting.rb +8 -0
- data/app/models/raif/concerns/llms/bedrock/message_formatting.rb +43 -0
- data/app/models/raif/concerns/llms/bedrock/response_tool_calls.rb +26 -0
- data/app/models/raif/concerns/llms/bedrock/tool_formatting.rb +8 -0
- data/app/models/raif/concerns/llms/google/message_formatting.rb +112 -0
- data/app/models/raif/concerns/llms/google/response_tool_calls.rb +32 -0
- data/app/models/raif/concerns/llms/google/tool_formatting.rb +76 -0
- data/app/models/raif/concerns/llms/message_formatting.rb +41 -5
- data/app/models/raif/concerns/llms/open_ai/json_schema_validation.rb +3 -3
- data/app/models/raif/concerns/llms/open_ai_completions/message_formatting.rb +22 -0
- data/app/models/raif/concerns/llms/open_ai_completions/response_tool_calls.rb +22 -0
- data/app/models/raif/concerns/llms/open_ai_completions/tool_formatting.rb +8 -0
- data/app/models/raif/concerns/llms/open_ai_responses/message_formatting.rb +17 -0
- data/app/models/raif/concerns/llms/open_ai_responses/response_tool_calls.rb +26 -0
- data/app/models/raif/concerns/llms/open_ai_responses/tool_formatting.rb +8 -0
- data/app/models/raif/concerns/provider_managed_tool_calls.rb +162 -0
- data/app/models/raif/concerns/run_with.rb +127 -0
- data/app/models/raif/conversation.rb +112 -8
- data/app/models/raif/conversation_entry.rb +38 -4
- data/app/models/raif/embedding_model.rb +2 -1
- data/app/models/raif/embedding_models/bedrock.rb +10 -1
- data/app/models/raif/embedding_models/google.rb +37 -0
- data/app/models/raif/embedding_models/open_ai.rb +1 -1
- data/app/models/raif/evals/llm_judge.rb +70 -0
- data/{lib → app/models}/raif/evals/llm_judges/binary.rb +41 -3
- data/{lib → app/models}/raif/evals/llm_judges/comparative.rb +41 -3
- data/{lib → app/models}/raif/evals/llm_judges/scored.rb +39 -1
- data/{lib → app/models}/raif/evals/llm_judges/summarization.rb +40 -2
- data/app/models/raif/llm.rb +104 -4
- data/app/models/raif/llms/anthropic.rb +32 -22
- data/app/models/raif/llms/bedrock.rb +64 -24
- data/app/models/raif/llms/google.rb +166 -0
- data/app/models/raif/llms/open_ai_base.rb +23 -5
- data/app/models/raif/llms/open_ai_completions.rb +14 -12
- data/app/models/raif/llms/open_ai_responses.rb +14 -17
- data/app/models/raif/llms/open_router.rb +16 -15
- data/app/models/raif/model_completion.rb +103 -1
- data/app/models/raif/model_tool.rb +55 -5
- data/app/models/raif/model_tool_invocation.rb +68 -6
- data/app/models/raif/model_tools/agent_final_answer.rb +2 -7
- data/app/models/raif/model_tools/provider_managed/code_execution.rb +4 -0
- data/app/models/raif/model_tools/provider_managed/image_generation.rb +4 -0
- data/app/models/raif/model_tools/provider_managed/web_search.rb +4 -0
- data/app/models/raif/prompt_studio_batch_run.rb +155 -0
- data/app/models/raif/prompt_studio_batch_run_item.rb +220 -0
- data/app/models/raif/streaming_responses/bedrock.rb +60 -1
- data/app/models/raif/streaming_responses/google.rb +71 -0
- data/app/models/raif/task.rb +85 -18
- data/app/models/raif/user_tool_invocation.rb +19 -0
- data/app/views/layouts/raif/admin.html.erb +43 -2
- data/app/views/raif/admin/agents/_agent.html.erb +9 -0
- data/app/views/raif/admin/agents/_conversation_message.html.erb +28 -6
- data/app/views/raif/admin/agents/index.html.erb +50 -0
- data/app/views/raif/admin/agents/show.html.erb +50 -1
- data/app/views/raif/admin/configs/show.html.erb +117 -0
- data/app/views/raif/admin/conversations/_conversation_entry.html.erb +29 -34
- data/app/views/raif/admin/conversations/show.html.erb +2 -0
- data/app/views/raif/admin/llms/index.html.erb +110 -0
- data/app/views/raif/admin/model_completions/_model_completion.html.erb +10 -5
- data/app/views/raif/admin/model_completions/index.html.erb +40 -1
- data/app/views/raif/admin/model_completions/show.html.erb +256 -84
- data/app/views/raif/admin/model_tool_invocations/index.html.erb +22 -1
- data/app/views/raif/admin/model_tool_invocations/show.html.erb +18 -0
- data/app/views/raif/admin/model_tools/_list.html.erb +16 -0
- data/app/views/raif/admin/model_tools/_model_tool.html.erb +36 -0
- data/app/views/raif/admin/prompt_studio/agents/index.html.erb +56 -0
- data/app/views/raif/admin/prompt_studio/agents/show.html.erb +57 -0
- data/app/views/raif/admin/prompt_studio/batch_runs/_batch_run_item.html.erb +54 -0
- data/app/views/raif/admin/prompt_studio/batch_runs/_judge_config_fields.html.erb +76 -0
- data/app/views/raif/admin/prompt_studio/batch_runs/_judge_detail_modal.html.erb +27 -0
- data/app/views/raif/admin/prompt_studio/batch_runs/_modal.html.erb +35 -0
- data/app/views/raif/admin/prompt_studio/batch_runs/_progress.html.erb +78 -0
- data/app/views/raif/admin/prompt_studio/batch_runs/show.html.erb +49 -0
- data/app/views/raif/admin/prompt_studio/conversations/index.html.erb +48 -0
- data/app/views/raif/admin/prompt_studio/conversations/show.html.erb +36 -0
- data/app/views/raif/admin/prompt_studio/shared/_nav_tabs.html.erb +17 -0
- data/app/views/raif/admin/prompt_studio/shared/_prompt_comparison.html.erb +87 -0
- data/app/views/raif/admin/prompt_studio/shared/_type_filter.html.erb +54 -0
- data/app/views/raif/admin/prompt_studio/tasks/_task_result.html.erb +145 -0
- data/app/views/raif/admin/prompt_studio/tasks/_task_row.html.erb +12 -0
- data/app/views/raif/admin/prompt_studio/tasks/_task_type_filter.html.erb +58 -0
- data/app/views/raif/admin/prompt_studio/tasks/_tasks_table.html.erb +22 -0
- data/app/views/raif/admin/prompt_studio/tasks/index.html.erb +35 -0
- data/app/views/raif/admin/prompt_studio/tasks/show.html.erb +19 -0
- data/app/views/raif/admin/stats/_stats_tile.html.erb +34 -0
- data/app/views/raif/admin/stats/index.html.erb +71 -88
- data/app/views/raif/admin/stats/model_tool_invocations/index.html.erb +43 -0
- data/app/views/raif/admin/stats/tasks/index.html.erb +20 -6
- data/app/views/raif/admin/tasks/_task.html.erb +1 -0
- data/app/views/raif/admin/tasks/index.html.erb +23 -6
- data/app/views/raif/admin/tasks/show.html.erb +56 -3
- data/app/views/raif/conversation_entries/_form.html.erb +3 -0
- data/app/views/raif/conversation_entries/_message.html.erb +10 -6
- data/app/views/raif/conversations/_conversation.html.erb +10 -0
- data/app/views/raif/conversations/_entry_processed.turbo_stream.erb +12 -0
- data/app/views/raif/conversations/index.html.erb +23 -0
- data/config/importmap.rb +8 -0
- data/config/locales/admin.en.yml +161 -1
- data/config/locales/en.yml +67 -4
- data/config/routes.rb +10 -0
- data/db/migrate/20250904194456_add_generating_entry_response_to_raif_conversations.rb +7 -0
- data/db/migrate/20250911125234_add_source_to_raif_tasks.rb +7 -0
- data/db/migrate/20251020005853_add_source_to_raif_agents.rb +7 -0
- data/db/migrate/20251020011346_rename_task_run_args_to_run_with.rb +7 -0
- data/db/migrate/20251020011405_add_run_with_to_raif_agents.rb +13 -0
- data/db/migrate/20251024160119_add_llm_messages_max_length_to_raif_conversations.rb +14 -0
- data/db/migrate/20251124185033_add_provider_tool_call_id_to_raif_model_tool_invocations.rb +7 -0
- data/db/migrate/20251128202941_add_tool_choice_to_raif_model_completions.rb +7 -0
- data/db/migrate/20260118144846_add_source_to_raif_conversations.rb +7 -0
- data/db/migrate/20260119000000_add_failure_tracking_to_raif_model_completions.rb +10 -0
- data/db/migrate/20260119000001_add_completed_at_to_raif_model_completions.rb +8 -0
- data/db/migrate/20260119000002_add_started_at_to_raif_model_completions.rb +8 -0
- data/db/migrate/20260307000000_add_prompt_studio_run_to_raif_tasks.rb +7 -0
- data/db/migrate/20260308000000_create_raif_prompt_studio_batch_runs.rb +27 -0
- data/db/migrate/20260308000001_create_raif_prompt_studio_batch_run_items.rb +24 -0
- data/db/migrate/20260407000000_add_cache_token_columns_to_raif_model_completions.rb +8 -0
- data/lib/generators/raif/agent/agent_generator.rb +18 -0
- data/lib/generators/raif/agent/templates/agent.rb.tt +7 -5
- data/lib/generators/raif/agent/templates/application_agent.rb.tt +1 -1
- data/lib/generators/raif/agent/templates/system_prompt.erb.tt +3 -0
- data/lib/generators/raif/conversation/conversation_generator.rb +19 -1
- data/lib/generators/raif/conversation/templates/conversation.rb.tt +6 -0
- data/lib/generators/raif/conversation/templates/system_prompt.erb.tt +4 -0
- data/lib/generators/raif/install/templates/initializer.rb +117 -8
- data/lib/generators/raif/task/task_generator.rb +18 -0
- data/lib/generators/raif/task/templates/prompt.erb.tt +4 -0
- data/lib/generators/raif/task/templates/task.rb.tt +10 -9
- data/lib/raif/configuration.rb +47 -2
- data/lib/raif/embedding_model_registry.rb +8 -0
- data/lib/raif/engine.rb +24 -1
- data/lib/raif/errors/blank_response_error.rb +8 -0
- data/lib/raif/errors/instance_dependent_schema_error.rb +8 -0
- data/lib/raif/errors/prompt_template_error.rb +15 -0
- data/lib/raif/errors/streaming_error.rb +6 -3
- data/lib/raif/errors.rb +3 -0
- data/lib/raif/evals/run.rb +1 -0
- data/lib/raif/evals.rb +0 -6
- data/lib/raif/json_schema_builder.rb +14 -0
- data/lib/raif/llm_registry.rb +433 -42
- data/lib/raif/messages.rb +180 -0
- data/lib/raif/prompt_studio_comparison_builder.rb +138 -0
- data/lib/raif/token_estimator.rb +28 -0
- data/lib/raif/version.rb +1 -1
- data/lib/raif.rb +11 -0
- data/lib/tasks/annotate_rb.rake +10 -0
- data/spec/support/rspec_helpers.rb +15 -9
- data/spec/support/test_task.rb +9 -0
- data/spec/support/test_template_task.rb +41 -0
- metadata +108 -15
- data/app/models/raif/agents/re_act_agent.rb +0 -127
- data/app/models/raif/agents/re_act_step.rb +0 -32
- data/app/models/raif/concerns/task_run_args.rb +0 -62
- data/lib/raif/evals/llm_judge.rb +0 -32
- /data/{lib → app/models}/raif/evals/scoring_rubric.rb +0 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raif
|
|
4
|
+
class PromptStudioBatchRunJob < ApplicationJob
|
|
5
|
+
|
|
6
|
+
def perform(batch_run:)
|
|
7
|
+
batch_run.update!(started_at: Time.current)
|
|
8
|
+
|
|
9
|
+
batch_run.items.where(status: "pending").find_each do |item|
|
|
10
|
+
Raif::PromptStudioBatchRunItemJob.perform_later(item: item)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raif
|
|
4
|
+
class PromptStudioTaskRunJob < ApplicationJob
|
|
5
|
+
|
|
6
|
+
def perform(task:)
|
|
7
|
+
task.run
|
|
8
|
+
broadcast_task_result(task)
|
|
9
|
+
rescue StandardError => e
|
|
10
|
+
logger.error "Error running prompt studio task: #{e.message}"
|
|
11
|
+
logger.error e.backtrace&.join("\n")
|
|
12
|
+
|
|
13
|
+
task.update(failed_at: Time.current) unless task.failed_at?
|
|
14
|
+
broadcast_task_result(task)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def broadcast_task_result(task)
|
|
20
|
+
comparison = Raif::PromptStudioComparisonBuilder.build(task)
|
|
21
|
+
original_task = task.prompt_studio_run? && task.source.is_a?(Raif::Task) ? task.source : nil
|
|
22
|
+
|
|
23
|
+
html = Raif::Admin::PromptStudio::TasksController.render(
|
|
24
|
+
partial: "raif/admin/prompt_studio/tasks/task_result",
|
|
25
|
+
locals: { task: task, comparison: comparison, original_task: original_task }
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
Turbo::StreamsChannel.broadcast_replace_to(
|
|
29
|
+
task,
|
|
30
|
+
target: ActionView::RecordIdentifier.dom_id(task, :result),
|
|
31
|
+
html: html
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
end
|
|
36
|
+
end
|
data/app/models/raif/agent.rb
CHANGED
|
@@ -1,18 +1,59 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# == Schema Information
|
|
4
|
+
#
|
|
5
|
+
# Table name: raif_agents
|
|
6
|
+
#
|
|
7
|
+
# id :bigint not null, primary key
|
|
8
|
+
# available_model_tools :jsonb not null
|
|
9
|
+
# completed_at :datetime
|
|
10
|
+
# conversation_history :jsonb not null
|
|
11
|
+
# creator_type :string not null
|
|
12
|
+
# failed_at :datetime
|
|
13
|
+
# failure_reason :text
|
|
14
|
+
# final_answer :text
|
|
15
|
+
# iteration_count :integer default(0), not null
|
|
16
|
+
# llm_model_key :string not null
|
|
17
|
+
# max_iterations :integer default(10), not null
|
|
18
|
+
# requested_language_key :string
|
|
19
|
+
# run_with :jsonb
|
|
20
|
+
# source_type :string
|
|
21
|
+
# started_at :datetime
|
|
22
|
+
# system_prompt :text
|
|
23
|
+
# task :text
|
|
24
|
+
# type :string not null
|
|
25
|
+
# created_at :datetime not null
|
|
26
|
+
# updated_at :datetime not null
|
|
27
|
+
# creator_id :bigint not null
|
|
28
|
+
# source_id :bigint
|
|
29
|
+
#
|
|
30
|
+
# Indexes
|
|
31
|
+
#
|
|
32
|
+
# index_raif_agents_on_created_at (created_at)
|
|
33
|
+
# index_raif_agents_on_creator (creator_type,creator_id)
|
|
34
|
+
# index_raif_agents_on_source (source_type,source_id)
|
|
35
|
+
#
|
|
3
36
|
module Raif
|
|
4
37
|
class Agent < ApplicationRecord
|
|
38
|
+
prepend Raif::Concerns::HasPromptTemplates
|
|
39
|
+
|
|
5
40
|
include Raif::Concerns::HasLlm
|
|
6
41
|
include Raif::Concerns::HasRequestedLanguage
|
|
7
42
|
include Raif::Concerns::HasAvailableModelTools
|
|
43
|
+
include Raif::Concerns::HasRuntimeDuration
|
|
8
44
|
include Raif::Concerns::InvokesModelTools
|
|
45
|
+
include Raif::Concerns::AgentInferenceStats
|
|
46
|
+
include Raif::Concerns::LlmPromptCaching
|
|
47
|
+
include Raif::Concerns::RunWith
|
|
9
48
|
|
|
10
49
|
belongs_to :creator, polymorphic: true
|
|
50
|
+
belongs_to :source, polymorphic: true, optional: true
|
|
11
51
|
|
|
12
52
|
has_many :raif_model_completions, as: :source, dependent: :destroy, class_name: "Raif::ModelCompletion"
|
|
13
53
|
|
|
14
54
|
after_initialize -> { self.available_model_tools ||= [] }
|
|
15
55
|
after_initialize -> { self.conversation_history ||= [] }
|
|
56
|
+
after_initialize -> { self.run_with ||= {} }
|
|
16
57
|
|
|
17
58
|
boolean_timestamp :started_at
|
|
18
59
|
boolean_timestamp :completed_at
|
|
@@ -69,16 +110,25 @@ module Raif
|
|
|
69
110
|
Task: #{task}
|
|
70
111
|
DEBUG
|
|
71
112
|
|
|
72
|
-
add_conversation_history_entry(
|
|
113
|
+
add_conversation_history_entry(Raif::Messages::UserMessage.new(content: task).to_h)
|
|
73
114
|
|
|
74
115
|
while iteration_count < max_iterations
|
|
75
116
|
update_columns(iteration_count: iteration_count + 1)
|
|
76
117
|
|
|
118
|
+
# Update the system prompt on each iteration in case it has changed since the last iteration
|
|
119
|
+
self.system_prompt = build_system_prompt
|
|
120
|
+
|
|
121
|
+
# Hook for subclasses to perform actions before the LLM chat (e.g., add warnings)
|
|
122
|
+
before_iteration_llm_chat
|
|
123
|
+
|
|
77
124
|
model_completion = llm.chat(
|
|
78
125
|
messages: conversation_history,
|
|
79
126
|
source: self,
|
|
80
127
|
system_prompt: system_prompt,
|
|
81
|
-
available_model_tools: native_model_tools
|
|
128
|
+
available_model_tools: native_model_tools,
|
|
129
|
+
tool_choice: tool_choice_for_iteration,
|
|
130
|
+
anthropic_prompt_caching_enabled: self.class.anthropic_prompt_caching_enabled,
|
|
131
|
+
bedrock_prompt_caching_enabled: self.class.bedrock_prompt_caching_enabled
|
|
82
132
|
)
|
|
83
133
|
|
|
84
134
|
logger.debug <<~DEBUG
|
|
@@ -93,25 +143,40 @@ module Raif
|
|
|
93
143
|
DEBUG
|
|
94
144
|
|
|
95
145
|
process_iteration_model_completion(model_completion)
|
|
96
|
-
break if final_answer.present?
|
|
146
|
+
break if final_answer.present? || failed?
|
|
97
147
|
end
|
|
98
148
|
|
|
99
|
-
|
|
149
|
+
finalize_run!
|
|
100
150
|
final_answer
|
|
101
151
|
rescue StandardError => e
|
|
102
|
-
self.failed_at
|
|
103
|
-
self.failure_reason
|
|
152
|
+
self.failed_at ||= Time.current
|
|
153
|
+
self.failure_reason ||= e.message
|
|
104
154
|
save!
|
|
105
155
|
|
|
106
156
|
raise
|
|
107
157
|
end
|
|
108
158
|
|
|
159
|
+
def final_iteration?
|
|
160
|
+
iteration_count == max_iterations
|
|
161
|
+
end
|
|
162
|
+
|
|
109
163
|
private
|
|
110
164
|
|
|
111
165
|
def populate_default_model_tools
|
|
112
166
|
# no-op by default. Can be overridden by subclasses to add default model tools
|
|
113
167
|
end
|
|
114
168
|
|
|
169
|
+
def finalize_run!
|
|
170
|
+
validate_successful_completion
|
|
171
|
+
return if failed?
|
|
172
|
+
|
|
173
|
+
completed!
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def validate_successful_completion
|
|
177
|
+
# no-op by default. Can be overridden by subclasses to enforce success criteria.
|
|
178
|
+
end
|
|
179
|
+
|
|
115
180
|
def process_iteration_model_completion(model_completion)
|
|
116
181
|
raise NotImplementedError, "#{self.class.name} must implement process_iteration_model_completion"
|
|
117
182
|
end
|
|
@@ -120,6 +185,27 @@ module Raif
|
|
|
120
185
|
# no-op by default
|
|
121
186
|
end
|
|
122
187
|
|
|
188
|
+
# Hook for subclasses to perform actions before the LLM chat on each iteration
|
|
189
|
+
# Override in subclasses to add warnings, context, etc.
|
|
190
|
+
def before_iteration_llm_chat
|
|
191
|
+
# no-op by default
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Hook for subclasses to specify tool_choice for the current iteration
|
|
195
|
+
# Override in subclasses to force specific tools (e.g., on final iteration)
|
|
196
|
+
# @return [Class, nil] A model tool class (e.g., Raif::ModelTools::AgentFinalAnswer), or nil for default behavior
|
|
197
|
+
def tool_choice_for_iteration
|
|
198
|
+
nil
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Hook for subclasses to require a specific tool on the current iteration.
|
|
202
|
+
# Override to align prompt warnings and provider-level tool_choice.
|
|
203
|
+
# Overrides should be deterministic and side-effect free for a given iteration.
|
|
204
|
+
# @return [Class, nil] A model tool class, or nil if no specific tool is required.
|
|
205
|
+
def required_tool_for_iteration
|
|
206
|
+
nil
|
|
207
|
+
end
|
|
208
|
+
|
|
123
209
|
def add_conversation_history_entry(entry)
|
|
124
210
|
entry_stringified = entry.stringify_keys
|
|
125
211
|
conversation_history << entry_stringified
|
|
@@ -127,6 +213,12 @@ module Raif
|
|
|
127
213
|
on_conversation_history_entry.call(entry_stringified) if on_conversation_history_entry.present?
|
|
128
214
|
end
|
|
129
215
|
|
|
216
|
+
def fail_run!(reason)
|
|
217
|
+
self.failed_at ||= Time.current
|
|
218
|
+
self.failure_reason ||= reason
|
|
219
|
+
save!
|
|
220
|
+
end
|
|
221
|
+
|
|
130
222
|
def build_system_prompt
|
|
131
223
|
raise NotImplementedError, "Subclasses of Raif::Agent must implement build_system_prompt"
|
|
132
224
|
end
|
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# == Schema Information
|
|
4
|
+
#
|
|
5
|
+
# Table name: raif_agents
|
|
6
|
+
#
|
|
7
|
+
# id :bigint not null, primary key
|
|
8
|
+
# available_model_tools :jsonb not null
|
|
9
|
+
# completed_at :datetime
|
|
10
|
+
# conversation_history :jsonb not null
|
|
11
|
+
# creator_type :string not null
|
|
12
|
+
# failed_at :datetime
|
|
13
|
+
# failure_reason :text
|
|
14
|
+
# final_answer :text
|
|
15
|
+
# iteration_count :integer default(0), not null
|
|
16
|
+
# llm_model_key :string not null
|
|
17
|
+
# max_iterations :integer default(10), not null
|
|
18
|
+
# requested_language_key :string
|
|
19
|
+
# run_with :jsonb
|
|
20
|
+
# source_type :string
|
|
21
|
+
# started_at :datetime
|
|
22
|
+
# system_prompt :text
|
|
23
|
+
# task :text
|
|
24
|
+
# type :string not null
|
|
25
|
+
# created_at :datetime not null
|
|
26
|
+
# updated_at :datetime not null
|
|
27
|
+
# creator_id :bigint not null
|
|
28
|
+
# source_id :bigint
|
|
29
|
+
#
|
|
30
|
+
# Indexes
|
|
31
|
+
#
|
|
32
|
+
# index_raif_agents_on_created_at (created_at)
|
|
33
|
+
# index_raif_agents_on_creator (creator_type,creator_id)
|
|
34
|
+
# index_raif_agents_on_source (source_type,source_id)
|
|
35
|
+
#
|
|
3
36
|
module Raif
|
|
4
37
|
module Agents
|
|
5
38
|
class NativeToolCallingAgent < Raif::Agent
|
|
@@ -12,7 +45,10 @@ module Raif
|
|
|
12
45
|
}
|
|
13
46
|
|
|
14
47
|
before_validation -> {
|
|
15
|
-
|
|
48
|
+
# If there is no final answer tool added, add it
|
|
49
|
+
unless available_model_tools_map.key?("agent_final_answer")
|
|
50
|
+
available_model_tools << "Raif::ModelTools::AgentFinalAnswer"
|
|
51
|
+
end
|
|
16
52
|
}
|
|
17
53
|
|
|
18
54
|
def build_system_prompt
|
|
@@ -28,7 +64,7 @@ module Raif
|
|
|
28
64
|
6. Provide a final answer to the user's request.
|
|
29
65
|
|
|
30
66
|
For your final answer:
|
|
31
|
-
-
|
|
67
|
+
- You **MUST** use the agent_final_answer tool/function to provide your final answer.
|
|
32
68
|
- Your answer should be comprehensive and directly address the user's request.
|
|
33
69
|
|
|
34
70
|
Guidelines
|
|
@@ -48,82 +84,173 @@ module Raif
|
|
|
48
84
|
available_model_tools
|
|
49
85
|
end
|
|
50
86
|
|
|
87
|
+
def final_answer_tool
|
|
88
|
+
available_model_tools_map["agent_final_answer"]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def required_tool_for_iteration
|
|
92
|
+
return final_answer_tool if final_iteration?
|
|
93
|
+
|
|
94
|
+
nil
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def before_iteration_llm_chat
|
|
98
|
+
required_tool = current_iteration_required_tool
|
|
99
|
+
return if required_tool.blank?
|
|
100
|
+
|
|
101
|
+
warning_message = Raif::Messages::UserMessage.new(
|
|
102
|
+
content: required_tool_warning_message(required_tool)
|
|
103
|
+
)
|
|
104
|
+
add_conversation_history_entry(warning_message.to_h)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def tool_choice_for_iteration
|
|
108
|
+
return current_iteration_required_tool if current_iteration_required_tool.present?
|
|
109
|
+
return :required if llm.supports_faithful_required_tool_choice?(native_model_tools)
|
|
110
|
+
|
|
111
|
+
log_required_tool_choice_fallback_once!
|
|
112
|
+
nil
|
|
113
|
+
end
|
|
114
|
+
|
|
51
115
|
def process_iteration_model_completion(model_completion)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
role: "assistant",
|
|
55
|
-
content: model_completion.parsed_response
|
|
56
|
-
})
|
|
57
|
-
end
|
|
116
|
+
required_tool = current_iteration_required_tool
|
|
117
|
+
assistant_response_message = model_completion.parsed_response if model_completion.parsed_response.present?
|
|
58
118
|
|
|
119
|
+
# The model made no tool call in this completion. Tell it to make a tool call.
|
|
59
120
|
if model_completion.response_tool_calls.blank?
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
121
|
+
if assistant_response_message.present?
|
|
122
|
+
assistant_message = Raif::Messages::AssistantMessage.new(content: assistant_response_message)
|
|
123
|
+
add_conversation_history_entry(assistant_message.to_h)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
error_content = if required_tool.present?
|
|
127
|
+
"Error: This iteration required the tool '#{required_tool.tool_name}', but the model response contained no tool call. Available tools: #{available_model_tools_map.keys.join(", ")}" # rubocop:disable Layout/LineLength
|
|
128
|
+
else
|
|
129
|
+
"Error: Previous message contained no tool call. Make a tool call at each step. Available tools: #{available_model_tools_map.keys.join(", ")}" # rubocop:disable Layout/LineLength
|
|
130
|
+
end
|
|
131
|
+
handle_iteration_error(error_content, required_tool:)
|
|
132
|
+
|
|
64
133
|
return
|
|
65
134
|
end
|
|
66
135
|
|
|
67
|
-
|
|
136
|
+
# The model returned multiple tool calls. We only allow one per step.
|
|
137
|
+
if model_completion.response_tool_calls.length > 1
|
|
138
|
+
if assistant_response_message.present?
|
|
139
|
+
assistant_message = Raif::Messages::AssistantMessage.new(content: assistant_response_message)
|
|
140
|
+
add_conversation_history_entry(assistant_message.to_h)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
error_content = "Error: Multiple tool calls received. Only one tool call is allowed per step. " \
|
|
144
|
+
"Please call exactly one tool at a time."
|
|
145
|
+
handle_iteration_error(error_content, required_tool:)
|
|
68
146
|
|
|
69
|
-
unless tool_call["name"] && tool_call["arguments"]
|
|
70
|
-
add_conversation_history_entry({
|
|
71
|
-
role: "assistant",
|
|
72
|
-
content: "<observation>Error: Invalid action specified. Please provide a valid action, formatted as a JSON object with 'tool' and 'arguments' keys.</observation>" # rubocop:disable Layout/LineLength
|
|
73
|
-
})
|
|
74
147
|
return
|
|
75
148
|
end
|
|
76
149
|
|
|
150
|
+
tool_call = model_completion.response_tool_calls.first
|
|
151
|
+
|
|
77
152
|
tool_name = tool_call["name"]
|
|
78
153
|
tool_arguments = tool_call["arguments"]
|
|
154
|
+
tool_klass = available_model_tools_map[tool_name]
|
|
79
155
|
|
|
80
|
-
#
|
|
81
|
-
#
|
|
82
|
-
|
|
83
|
-
# content: "<thought>I need to use the #{tool_name} tool to help with this task.</thought>"
|
|
84
|
-
# })
|
|
156
|
+
# Prepare tool arguments before recording to history so the history
|
|
157
|
+
# accurately reflects what was actually invoked
|
|
158
|
+
tool_arguments = tool_klass.prepare_tool_arguments(tool_arguments) if tool_klass.present?
|
|
85
159
|
|
|
86
|
-
#
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
160
|
+
# Add the tool call to history (with prepared arguments if tool is known)
|
|
161
|
+
tool_call_message = Raif::Messages::ToolCall.new(
|
|
162
|
+
provider_tool_call_id: tool_call["provider_tool_call_id"],
|
|
163
|
+
name: tool_call["name"],
|
|
164
|
+
arguments: tool_arguments,
|
|
165
|
+
assistant_message: assistant_response_message,
|
|
166
|
+
provider_metadata: tool_call["provider_metadata"]
|
|
167
|
+
)
|
|
168
|
+
add_conversation_history_entry(tool_call_message.to_h)
|
|
169
|
+
|
|
170
|
+
if required_tool.present? && tool_name != required_tool.tool_name
|
|
171
|
+
error_content = "Error: This iteration required the tool '#{required_tool.tool_name}', but the model called '#{tool_name}' instead."
|
|
172
|
+
handle_iteration_error(error_content, required_tool:)
|
|
90
173
|
return
|
|
91
174
|
end
|
|
92
175
|
|
|
93
|
-
# Add the tool call to conversation history
|
|
94
|
-
add_conversation_history_entry({
|
|
95
|
-
role: "assistant",
|
|
96
|
-
content: "<action>#{JSON.pretty_generate(tool_call)}</action>"
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
# Find the tool class and process it
|
|
100
|
-
tool_klass = available_model_tools_map[tool_name]
|
|
101
|
-
|
|
102
176
|
# The model tried to use a tool that doesn't exist
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
})
|
|
177
|
+
if tool_klass.blank?
|
|
178
|
+
error_content = "Error: Tool '#{tool_name}' is not a valid tool. " \
|
|
179
|
+
"Available tools: #{available_model_tools_map.keys.join(", ")}"
|
|
180
|
+
handle_iteration_error(error_content, required_tool:)
|
|
108
181
|
return
|
|
109
182
|
end
|
|
110
183
|
|
|
184
|
+
# Make sure the tool arguments match the tool's schema
|
|
111
185
|
unless JSON::Validator.validate(tool_klass.tool_arguments_schema, tool_arguments)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
})
|
|
186
|
+
error_content = "Error: Invalid tool arguments for the tool '#{tool_name}'. " \
|
|
187
|
+
"Tool arguments schema: #{tool_klass.tool_arguments_schema.to_json}"
|
|
188
|
+
handle_iteration_error(error_content, required_tool:)
|
|
116
189
|
return
|
|
117
190
|
end
|
|
118
191
|
|
|
119
|
-
# Process the tool and add observation to history
|
|
120
|
-
tool_invocation = tool_klass.invoke_tool(
|
|
121
|
-
|
|
192
|
+
# Process the tool invocation and add observation/result to history
|
|
193
|
+
tool_invocation = tool_klass.invoke_tool(
|
|
194
|
+
provider_tool_call_id: tool_call["provider_tool_call_id"],
|
|
195
|
+
tool_arguments: tool_arguments,
|
|
196
|
+
source: self
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
if tool_name == "agent_final_answer"
|
|
200
|
+
self.final_answer = tool_invocation.result
|
|
201
|
+
else
|
|
202
|
+
add_conversation_history_entry(tool_invocation.as_tool_call_result_message)
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def validate_successful_completion
|
|
207
|
+
return if failed? || final_answer.present?
|
|
208
|
+
|
|
209
|
+
fail_run!("Agent completed without calling agent_final_answer")
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def required_tool_warning_message(required_tool)
|
|
213
|
+
if required_tool == final_answer_tool
|
|
214
|
+
if final_iteration?
|
|
215
|
+
I18n.t("raif.agents.native_tool_calling_agent.final_answer_warning")
|
|
216
|
+
else
|
|
217
|
+
"Warning: This iteration requires the agent_final_answer tool. If you do not use it now, the next iteration will be your final chance."
|
|
218
|
+
end
|
|
219
|
+
else
|
|
220
|
+
"Warning: This iteration requires the #{required_tool.tool_name} tool."
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def current_iteration_required_tool
|
|
225
|
+
if @required_tool_iteration_count != iteration_count
|
|
226
|
+
@required_tool_iteration_count = iteration_count
|
|
227
|
+
@current_iteration_required_tool = required_tool_for_iteration
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
@current_iteration_required_tool
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def handle_iteration_error(error_content, required_tool: nil)
|
|
234
|
+
error_message = Raif::Messages::UserMessage.new(content: error_content)
|
|
235
|
+
add_conversation_history_entry(error_message.to_h)
|
|
236
|
+
|
|
237
|
+
return if required_tool.blank? || retry_iteration_available?
|
|
238
|
+
|
|
239
|
+
fail_run!(error_content)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def retry_iteration_available?
|
|
243
|
+
iteration_count < max_iterations
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def log_required_tool_choice_fallback_once!
|
|
247
|
+
return if @logged_required_tool_choice_fallback
|
|
122
248
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
249
|
+
@logged_required_tool_choice_fallback = true
|
|
250
|
+
Raif.logger.warn(
|
|
251
|
+
"NativeToolCallingAgent is falling back to runtime tool-call validation because #{llm.key} " \
|
|
252
|
+
"cannot faithfully enforce tool_choice: :required for tools: #{available_model_tools_map.keys.join(", ")}"
|
|
253
|
+
)
|
|
127
254
|
end
|
|
128
255
|
|
|
129
256
|
def ensure_llm_supports_native_tool_use
|
|
@@ -8,6 +8,24 @@ class Raif::ApplicationRecord < Raif.config.model_superclass.constantize
|
|
|
8
8
|
scope :newest_first, -> { order(created_at: :desc) }
|
|
9
9
|
scope :oldest_first, -> { order(created_at: :asc) }
|
|
10
10
|
|
|
11
|
+
# Returns a scope that checks if a JSON column is not blank (not null and not empty array)
|
|
12
|
+
# @param column_name [Symbol, String] the name of the JSON column
|
|
13
|
+
# @return [ActiveRecord::Relation]
|
|
14
|
+
def self.where_json_not_blank(column_name)
|
|
15
|
+
quoted_column = connection.quote_column_name(column_name.to_s)
|
|
16
|
+
|
|
17
|
+
case connection.adapter_name.downcase
|
|
18
|
+
when "postgresql"
|
|
19
|
+
where.not(column_name => nil)
|
|
20
|
+
.where("jsonb_array_length(#{quoted_column}) > 0")
|
|
21
|
+
when "mysql2", "trilogy"
|
|
22
|
+
where.not(column_name => nil)
|
|
23
|
+
.where("JSON_LENGTH(#{quoted_column}) > 0")
|
|
24
|
+
else
|
|
25
|
+
raise "Unsupported database: #{connection.adapter_name}"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
11
29
|
def self.table_name_prefix
|
|
12
30
|
"raif_"
|
|
13
31
|
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raif::Concerns::AgentInferenceStats
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
# Returns the total number of prompt tokens across all model completions
|
|
7
|
+
def total_prompt_tokens
|
|
8
|
+
@total_prompt_tokens ||= raif_model_completions.sum(:prompt_tokens)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Returns the total number of completion tokens across all model completions
|
|
12
|
+
def total_completion_tokens
|
|
13
|
+
@total_completion_tokens ||= raif_model_completions.sum(:completion_tokens)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Returns the total number of tokens across all model completions
|
|
17
|
+
def total_tokens_sum
|
|
18
|
+
@total_tokens_sum ||= raif_model_completions.sum(:total_tokens)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Returns the total cost of prompt tokens across all model completions
|
|
22
|
+
def total_prompt_token_cost
|
|
23
|
+
@total_prompt_token_cost ||= raif_model_completions.sum(:prompt_token_cost)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Returns the total cost of output tokens across all model completions
|
|
27
|
+
def total_output_token_cost
|
|
28
|
+
@total_output_token_cost ||= raif_model_completions.sum(:output_token_cost)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Returns the total cost across all model completions
|
|
32
|
+
def total_cost
|
|
33
|
+
@total_cost ||= raif_model_completions.sum(:total_cost)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raif
|
|
4
|
+
module Concerns
|
|
5
|
+
module HasPromptTemplates
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
class TemplateContext < ActionView::Base.with_empty_template_cache
|
|
9
|
+
def initialize(lookup_context, instance)
|
|
10
|
+
super(lookup_context, {}, nil)
|
|
11
|
+
@_instance = instance
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def method_missing(method_name, ...)
|
|
15
|
+
if @_instance.respond_to?(method_name)
|
|
16
|
+
@_instance.public_send(method_name, ...)
|
|
17
|
+
else
|
|
18
|
+
super
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
23
|
+
@_instance.respond_to?(method_name, include_private) || super
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class_methods do
|
|
28
|
+
# Returns the template prefix path derived from the class name.
|
|
29
|
+
# e.g. Raif::Tasks::SummarizeDocument -> "raif/tasks/summarize_document"
|
|
30
|
+
# e.g. Raif::Tasks::Docs::Summarize -> "raif/tasks/docs/summarize"
|
|
31
|
+
def prompt_template_prefix
|
|
32
|
+
name.underscore
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def prompt_template_view_paths
|
|
36
|
+
ActionController::Base.view_paths
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def build_prompt
|
|
41
|
+
if prompt_template_exists?(:prompt)
|
|
42
|
+
render_prompt_template(:prompt)
|
|
43
|
+
else
|
|
44
|
+
super
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def build_system_prompt
|
|
49
|
+
if prompt_template_exists?(:system_prompt)
|
|
50
|
+
render_prompt_template(:system_prompt)
|
|
51
|
+
else
|
|
52
|
+
super
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def prompt_template_name
|
|
59
|
+
self.class.prompt_template_prefix.split("/").last
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def prompt_template_dir
|
|
63
|
+
File.dirname(self.class.prompt_template_prefix)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def prompt_template_exists?(template_type)
|
|
67
|
+
prompt_lookup_context_for(template_type).exists?(prompt_template_name, prompt_template_dir)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def prompt_lookup_context_for(template_type)
|
|
71
|
+
lookup = ActionView::LookupContext.new(self.class.prompt_template_view_paths)
|
|
72
|
+
lookup.formats = [template_type]
|
|
73
|
+
lookup
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def render_prompt_template(template_type)
|
|
77
|
+
lookup = prompt_lookup_context_for(template_type)
|
|
78
|
+
context = TemplateContext.new(lookup, self)
|
|
79
|
+
context.render(template: "#{prompt_template_dir}/#{prompt_template_name}").strip
|
|
80
|
+
rescue ActionView::Template::Error, ActionView::MissingTemplate => e
|
|
81
|
+
raise Raif::Errors::PromptTemplateError.new(
|
|
82
|
+
template_path: "#{self.class.prompt_template_prefix}.#{template_type}.erb",
|
|
83
|
+
original_error: e
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|