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,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raif::Concerns::HasRuntimeDuration
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
def runtime_ended_at
|
|
7
|
+
completed_at || failed_at
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def runtime_duration_seconds
|
|
11
|
+
return if started_at.blank? || runtime_ended_at.blank?
|
|
12
|
+
|
|
13
|
+
duration_in_seconds = runtime_ended_at - started_at
|
|
14
|
+
return if duration_in_seconds.negative?
|
|
15
|
+
|
|
16
|
+
duration_in_seconds
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def runtime_duration
|
|
20
|
+
duration_in_seconds = runtime_duration_seconds
|
|
21
|
+
return "-" if duration_in_seconds.nil?
|
|
22
|
+
|
|
23
|
+
if duration_in_seconds < 1
|
|
24
|
+
"#{(duration_in_seconds * 1000).round}ms"
|
|
25
|
+
elsif duration_in_seconds < 60
|
|
26
|
+
seconds = (duration_in_seconds * 100).round / 100.0
|
|
27
|
+
"#{seconds.to_s.sub(/\.0+\z/, "").sub(/(\.\d*[1-9])0+\z/, "\\1")}s"
|
|
28
|
+
else
|
|
29
|
+
total_seconds = duration_in_seconds.round
|
|
30
|
+
hours = total_seconds / 3600
|
|
31
|
+
minutes = (total_seconds % 3600) / 60
|
|
32
|
+
seconds = total_seconds % 60
|
|
33
|
+
|
|
34
|
+
parts = []
|
|
35
|
+
parts << "#{hours}h" if hours.positive?
|
|
36
|
+
parts << "#{minutes}m" if minutes.positive? || hours.positive?
|
|
37
|
+
parts << "#{seconds}s"
|
|
38
|
+
parts.join(" ")
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -6,22 +6,70 @@ module Raif
|
|
|
6
6
|
extend ActiveSupport::Concern
|
|
7
7
|
|
|
8
8
|
class_methods do
|
|
9
|
-
def json_schema_definition(schema_name, &block)
|
|
9
|
+
def json_schema_definition(schema_name, dynamic: false, &block)
|
|
10
10
|
raise ArgumentError, "A block must be provided to define the JSON schema" unless block_given?
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
# Check if block expects an instance parameter (arity == 1)
|
|
13
|
+
# arity == 0: no parameters (class-level schema)
|
|
14
|
+
# arity == 1: one parameter (instance-dependent schema)
|
|
15
|
+
if block.arity == 1
|
|
16
|
+
# Store block for instance-dependent schema building
|
|
17
|
+
@schema_blocks ||= {}
|
|
18
|
+
@schema_blocks[schema_name] = block
|
|
19
|
+
elsif dynamic
|
|
20
|
+
# Store block for class-level dynamic schema (re-evaluated each call)
|
|
21
|
+
@dynamic_schema_blocks ||= {}
|
|
22
|
+
@dynamic_schema_blocks[schema_name] = block
|
|
23
|
+
else
|
|
24
|
+
# Build schema immediately for class-level (backward compatible)
|
|
25
|
+
@schemas ||= {}
|
|
26
|
+
@schemas[schema_name] = Raif::JsonSchemaBuilder.new
|
|
27
|
+
@schemas[schema_name].instance_eval(&block)
|
|
28
|
+
end
|
|
16
29
|
end
|
|
17
30
|
|
|
18
31
|
def schema_defined?(schema_name)
|
|
19
|
-
@schemas&.dig(schema_name).present?
|
|
32
|
+
@schemas&.dig(schema_name).present? ||
|
|
33
|
+
@schema_blocks&.dig(schema_name).present? ||
|
|
34
|
+
@dynamic_schema_blocks&.dig(schema_name).present?
|
|
20
35
|
end
|
|
21
36
|
|
|
22
37
|
def schema_for(schema_name)
|
|
38
|
+
# Check if this is an instance-dependent schema
|
|
39
|
+
if @schema_blocks&.dig(schema_name).present?
|
|
40
|
+
raise Raif::Errors::InstanceDependentSchemaError,
|
|
41
|
+
"The schema '#{schema_name}' is instance-dependent and cannot be accessed at the class level. " \
|
|
42
|
+
"Call this method on an instance instead."
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Check if this is a dynamic schema (re-evaluate each call)
|
|
46
|
+
if @dynamic_schema_blocks&.dig(schema_name).present?
|
|
47
|
+
builder = Raif::JsonSchemaBuilder.new
|
|
48
|
+
builder.instance_eval(&@dynamic_schema_blocks[schema_name])
|
|
49
|
+
return builder.to_schema
|
|
50
|
+
end
|
|
51
|
+
|
|
23
52
|
@schemas[schema_name].to_schema
|
|
24
53
|
end
|
|
54
|
+
|
|
55
|
+
def instance_dependent_schema?(schema_name)
|
|
56
|
+
@schema_blocks&.dig(schema_name).present?
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Instance method to build schema with instance context
|
|
61
|
+
def schema_for_instance(schema_name)
|
|
62
|
+
block = self.class.instance_variable_get(:@schema_blocks)&.[](schema_name)
|
|
63
|
+
|
|
64
|
+
if block
|
|
65
|
+
# Build schema with instance context
|
|
66
|
+
builder = Raif::JsonSchemaBuilder.new
|
|
67
|
+
builder.build_with_instance(self, &block)
|
|
68
|
+
builder.to_schema
|
|
69
|
+
elsif self.class.schema_defined?(schema_name)
|
|
70
|
+
# Fall back to class-level schema (handles both static and dynamic)
|
|
71
|
+
self.class.schema_for(schema_name)
|
|
72
|
+
end
|
|
25
73
|
end
|
|
26
74
|
end
|
|
27
75
|
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raif::Concerns::LlmPromptCaching
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
included do
|
|
7
|
+
class_attribute :anthropic_prompt_caching_enabled, instance_writer: false, default: false
|
|
8
|
+
class_attribute :bedrock_prompt_caching_enabled, instance_writer: false, default: false
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class_methods do
|
|
12
|
+
def enable_anthropic_prompt_caching
|
|
13
|
+
self.anthropic_prompt_caching_enabled = true
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def enable_bedrock_prompt_caching
|
|
17
|
+
self.bedrock_prompt_caching_enabled = true
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
module Raif::Concerns::Llms::Anthropic::MessageFormatting
|
|
4
4
|
extend ActiveSupport::Concern
|
|
5
5
|
|
|
6
|
+
def format_messages(messages)
|
|
7
|
+
# Anthropic tool results come back as user-role content blocks, so conversation
|
|
8
|
+
# continuations may need adjacent user messages collapsed after formatting.
|
|
9
|
+
consolidate_consecutive_role_messages(super, content_key: "content")
|
|
10
|
+
end
|
|
11
|
+
|
|
6
12
|
def format_model_image_input_message(image_input)
|
|
7
13
|
if image_input.source_type == :url
|
|
8
14
|
{
|
|
@@ -48,4 +54,32 @@ module Raif::Concerns::Llms::Anthropic::MessageFormatting
|
|
|
48
54
|
raise Raif::Errors::InvalidModelFileInputError, "Invalid model file input source type: #{file_input.source_type}"
|
|
49
55
|
end
|
|
50
56
|
end
|
|
57
|
+
|
|
58
|
+
def format_tool_call_message(tool_call)
|
|
59
|
+
content_array = []
|
|
60
|
+
content_array << format_string_message(tool_call["assistant_message"]) if tool_call["assistant_message"].present?
|
|
61
|
+
|
|
62
|
+
content_array << {
|
|
63
|
+
"type" => "tool_use",
|
|
64
|
+
"id" => tool_call["provider_tool_call_id"],
|
|
65
|
+
"name" => tool_call["name"],
|
|
66
|
+
"input" => tool_call["arguments"]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
{
|
|
70
|
+
"role" => "assistant",
|
|
71
|
+
"content" => content_array
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def format_tool_call_result_message(tool_call_result)
|
|
76
|
+
{
|
|
77
|
+
"role" => "user",
|
|
78
|
+
"content" => [{
|
|
79
|
+
"type" => "tool_result",
|
|
80
|
+
"tool_use_id" => tool_call_result["provider_tool_call_id"],
|
|
81
|
+
"content" => tool_call_result["result"].is_a?(String) ? tool_call_result["result"] : JSON.generate(tool_call_result["result"])
|
|
82
|
+
}]
|
|
83
|
+
}
|
|
84
|
+
end
|
|
51
85
|
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raif::Concerns::Llms::Anthropic::ResponseToolCalls
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
def extract_response_tool_calls(resp)
|
|
7
|
+
return if resp&.dig("content").nil?
|
|
8
|
+
|
|
9
|
+
# Find any tool_use content blocks
|
|
10
|
+
tool_uses = resp&.dig("content")&.select do |content|
|
|
11
|
+
content["type"] == "tool_use"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
return if tool_uses.blank?
|
|
15
|
+
|
|
16
|
+
tool_uses.map do |tool_use|
|
|
17
|
+
{
|
|
18
|
+
"provider_tool_call_id" => tool_use["id"],
|
|
19
|
+
"name" => tool_use["name"],
|
|
20
|
+
"arguments" => tool_use["input"],
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -53,4 +53,12 @@ module Raif::Concerns::Llms::Anthropic::ToolFormatting
|
|
|
53
53
|
"Invalid provider-managed tool: #{tool.name} for #{key}"
|
|
54
54
|
end
|
|
55
55
|
end
|
|
56
|
+
|
|
57
|
+
def build_forced_tool_choice(tool_name)
|
|
58
|
+
{ "type" => "tool", "name" => tool_name, "disable_parallel_tool_use" => true }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def build_required_tool_choice
|
|
62
|
+
{ "type" => "any", "disable_parallel_tool_use" => true }
|
|
63
|
+
end
|
|
56
64
|
end
|
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
module Raif::Concerns::Llms::Bedrock::MessageFormatting
|
|
4
4
|
extend ActiveSupport::Concern
|
|
5
5
|
|
|
6
|
+
def format_messages(messages)
|
|
7
|
+
# Bedrock tool results are represented as user-role content blocks, so a
|
|
8
|
+
# tool_result followed by the next user prompt must be merged into one user
|
|
9
|
+
# message before sending it to the provider.
|
|
10
|
+
consolidate_consecutive_role_messages(super, content_key: "content")
|
|
11
|
+
end
|
|
12
|
+
|
|
6
13
|
def format_string_message(content, role: nil)
|
|
7
14
|
{ "text" => content }
|
|
8
15
|
end
|
|
@@ -67,4 +74,40 @@ module Raif::Concerns::Llms::Bedrock::MessageFormatting
|
|
|
67
74
|
"text/markdown" => "md"
|
|
68
75
|
}[content_type]
|
|
69
76
|
end
|
|
77
|
+
|
|
78
|
+
def format_tool_call_message(tool_call)
|
|
79
|
+
content_array = []
|
|
80
|
+
content_array << format_string_message(tool_call["assistant_message"]) if tool_call["assistant_message"].present?
|
|
81
|
+
|
|
82
|
+
content_array << {
|
|
83
|
+
"tool_use" => {
|
|
84
|
+
"tool_use_id" => tool_call["provider_tool_call_id"],
|
|
85
|
+
"name" => tool_call["name"],
|
|
86
|
+
"input" => tool_call["arguments"]
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
{
|
|
91
|
+
"role" => "assistant",
|
|
92
|
+
"content" => content_array
|
|
93
|
+
}
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def format_tool_call_result_message(tool_call_result)
|
|
97
|
+
tool_result_content = if tool_call_result["result"].is_a?(String)
|
|
98
|
+
{ "text" => tool_call_result["result"] }
|
|
99
|
+
else
|
|
100
|
+
{ "json" => tool_call_result["result"] }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
{
|
|
104
|
+
"role" => "user",
|
|
105
|
+
"content" => [{
|
|
106
|
+
"tool_result" => {
|
|
107
|
+
"tool_use_id" => tool_call_result["provider_tool_call_id"],
|
|
108
|
+
"content" => [tool_result_content]
|
|
109
|
+
}
|
|
110
|
+
}]
|
|
111
|
+
}
|
|
112
|
+
end
|
|
70
113
|
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raif::Concerns::Llms::Bedrock::ResponseToolCalls
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
def extract_response_tool_calls(resp)
|
|
7
|
+
# Get the message from the response object
|
|
8
|
+
message = resp.output.message
|
|
9
|
+
return if message.content.nil?
|
|
10
|
+
|
|
11
|
+
# Find any tool_use blocks in the content array
|
|
12
|
+
tool_uses = message.content.select do |content|
|
|
13
|
+
content.respond_to?(:tool_use) && content.tool_use.present?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
return if tool_uses.blank?
|
|
17
|
+
|
|
18
|
+
tool_uses.map do |content|
|
|
19
|
+
{
|
|
20
|
+
"provider_tool_call_id" => content.tool_use.tool_use_id,
|
|
21
|
+
"name" => content.tool_use.name,
|
|
22
|
+
"arguments" => content.tool_use.input
|
|
23
|
+
}
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -34,4 +34,12 @@ module Raif::Concerns::Llms::Bedrock::ToolFormatting
|
|
|
34
34
|
tools: tools.map{|tool| { tool_spec: tool } }
|
|
35
35
|
}
|
|
36
36
|
end
|
|
37
|
+
|
|
38
|
+
def build_forced_tool_choice(tool_name)
|
|
39
|
+
{ tool: { name: tool_name } }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def build_required_tool_choice
|
|
43
|
+
{ any: {} }
|
|
44
|
+
end
|
|
37
45
|
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raif::Concerns::Llms::Google::MessageFormatting
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
# Google uses a different envelope ("parts") and also represents tool results as
|
|
7
|
+
# user-role messages, so we normalize adjacent same-role messages after formatting.
|
|
8
|
+
def format_messages(messages)
|
|
9
|
+
formatted_messages = messages.map do |message|
|
|
10
|
+
if message.is_a?(Hash) && message["type"] == "tool_call"
|
|
11
|
+
format_tool_call_message(message)
|
|
12
|
+
elsif message.is_a?(Hash) && message["type"] == "tool_call_result"
|
|
13
|
+
format_tool_call_result_message(message)
|
|
14
|
+
else
|
|
15
|
+
role = message["role"] || message[:role]
|
|
16
|
+
# Google uses "model" instead of "assistant"
|
|
17
|
+
google_role = role == "assistant" ? "model" : role
|
|
18
|
+
{
|
|
19
|
+
"role" => google_role,
|
|
20
|
+
"parts" => format_message_content(message["content"] || message[:content], role: role)
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
consolidate_consecutive_role_messages(formatted_messages, content_key: "parts")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def format_string_message(content, role: nil)
|
|
29
|
+
{ "text" => content }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def format_model_image_input_message(image_input)
|
|
33
|
+
if image_input.source_type == :url
|
|
34
|
+
{
|
|
35
|
+
"fileData" => {
|
|
36
|
+
"mimeType" => image_input.content_type,
|
|
37
|
+
"fileUri" => image_input.url
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
elsif image_input.source_type == :file_content
|
|
41
|
+
{
|
|
42
|
+
"inlineData" => {
|
|
43
|
+
"mimeType" => image_input.content_type,
|
|
44
|
+
"data" => image_input.base64_data
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
else
|
|
48
|
+
raise Raif::Errors::InvalidModelImageInputError, "Invalid model image input source type: #{image_input.source_type}"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def format_model_file_input_message(file_input)
|
|
53
|
+
if file_input.source_type == :url
|
|
54
|
+
{
|
|
55
|
+
"fileData" => {
|
|
56
|
+
"mimeType" => file_input.content_type,
|
|
57
|
+
"fileUri" => file_input.url
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
elsif file_input.source_type == :file_content
|
|
61
|
+
{
|
|
62
|
+
"inlineData" => {
|
|
63
|
+
"mimeType" => file_input.content_type,
|
|
64
|
+
"data" => file_input.base64_data
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else
|
|
68
|
+
raise Raif::Errors::InvalidModelFileInputError, "Invalid model file input source type: #{file_input.source_type}"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def format_tool_call_message(tool_call)
|
|
73
|
+
parts = []
|
|
74
|
+
|
|
75
|
+
if tool_call["assistant_message"].present?
|
|
76
|
+
parts << format_string_message(tool_call["assistant_message"])
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
function_call_part = {
|
|
80
|
+
"functionCall" => {
|
|
81
|
+
"name" => tool_call["name"],
|
|
82
|
+
"args" => tool_call["arguments"]
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# Include thoughtSignature if present (required for Gemini 2.5+ thinking models)
|
|
87
|
+
thought_signature = tool_call.dig("provider_metadata", "thought_signature")
|
|
88
|
+
function_call_part["thoughtSignature"] = thought_signature if thought_signature.present?
|
|
89
|
+
|
|
90
|
+
parts << function_call_part
|
|
91
|
+
|
|
92
|
+
{
|
|
93
|
+
"role" => "model",
|
|
94
|
+
"parts" => parts
|
|
95
|
+
}
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def format_tool_call_result_message(tool_call_result)
|
|
99
|
+
result = tool_call_result["result"]
|
|
100
|
+
response_content = result.is_a?(String) ? { "output" => result } : result
|
|
101
|
+
|
|
102
|
+
{
|
|
103
|
+
"role" => "user",
|
|
104
|
+
"parts" => [{
|
|
105
|
+
"functionResponse" => {
|
|
106
|
+
"name" => tool_call_result["name"],
|
|
107
|
+
"response" => response_content
|
|
108
|
+
}
|
|
109
|
+
}]
|
|
110
|
+
}
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raif::Concerns::Llms::Google::ResponseToolCalls
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
def extract_response_tool_calls(resp)
|
|
7
|
+
parts = resp&.dig("candidates", 0, "content", "parts")
|
|
8
|
+
return if parts.blank?
|
|
9
|
+
|
|
10
|
+
# Find any functionCall parts
|
|
11
|
+
function_calls = parts.select { |part| part.key?("functionCall") }
|
|
12
|
+
|
|
13
|
+
return if function_calls.blank?
|
|
14
|
+
|
|
15
|
+
function_calls.map do |part|
|
|
16
|
+
function_call = part["functionCall"]
|
|
17
|
+
tool_call = {
|
|
18
|
+
# Google doesn't provide a unique ID for function calls, so we generate one
|
|
19
|
+
"provider_tool_call_id" => SecureRandom.uuid,
|
|
20
|
+
"name" => function_call["name"],
|
|
21
|
+
"arguments" => function_call["args"]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
# Capture thoughtSignature if present (required for Gemini 2.5+ thinking models)
|
|
25
|
+
if part["thoughtSignature"].present?
|
|
26
|
+
tool_call["provider_metadata"] = { "thought_signature" => part["thoughtSignature"] }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
tool_call
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raif::Concerns::Llms::Google::ToolFormatting
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
def build_tools_parameter(model_completion)
|
|
7
|
+
tools = []
|
|
8
|
+
function_declarations = []
|
|
9
|
+
|
|
10
|
+
# If we support native tool use and have tools available, add them to the request
|
|
11
|
+
if supports_native_tool_use? && model_completion.available_model_tools.any?
|
|
12
|
+
model_completion.available_model_tools_map.each do |_tool_name, tool|
|
|
13
|
+
if tool.provider_managed?
|
|
14
|
+
# Provider-managed tools are added as separate tool entries
|
|
15
|
+
tools << format_provider_managed_tool(tool)
|
|
16
|
+
else
|
|
17
|
+
function_declarations << {
|
|
18
|
+
name: tool.tool_name,
|
|
19
|
+
description: tool.tool_description,
|
|
20
|
+
parameters: sanitize_schema_for_google(tool.tool_arguments_schema)
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Add function declarations if any
|
|
27
|
+
if function_declarations.any?
|
|
28
|
+
tools << { functionDeclarations: function_declarations }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
tools
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def format_provider_managed_tool(tool)
|
|
35
|
+
validate_provider_managed_tool_support!(tool)
|
|
36
|
+
|
|
37
|
+
case tool.name
|
|
38
|
+
when "Raif::ModelTools::ProviderManaged::WebSearch"
|
|
39
|
+
{ google_search: {} }
|
|
40
|
+
when "Raif::ModelTools::ProviderManaged::CodeExecution"
|
|
41
|
+
{ code_execution: {} }
|
|
42
|
+
else
|
|
43
|
+
raise Raif::Errors::UnsupportedFeatureError,
|
|
44
|
+
"Invalid provider-managed tool: #{tool.name} for #{key}"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def build_forced_tool_choice(tool_name)
|
|
49
|
+
{ mode: "ANY", allowedFunctionNames: [tool_name] }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def build_required_tool_choice
|
|
53
|
+
{ mode: "ANY" }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
# Google's API doesn't support additionalProperties in JSON schemas
|
|
59
|
+
# This method recursively removes it from the schema
|
|
60
|
+
def sanitize_schema_for_google(schema)
|
|
61
|
+
return schema unless schema.is_a?(Hash)
|
|
62
|
+
|
|
63
|
+
sanitized = schema.except(:additionalProperties, "additionalProperties")
|
|
64
|
+
|
|
65
|
+
sanitized.transform_values do |value|
|
|
66
|
+
case value
|
|
67
|
+
when Hash
|
|
68
|
+
sanitize_schema_for_google(value)
|
|
69
|
+
when Array
|
|
70
|
+
value.map { |item| sanitize_schema_for_google(item) }
|
|
71
|
+
else
|
|
72
|
+
value
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -5,11 +5,17 @@ module Raif::Concerns::Llms::MessageFormatting
|
|
|
5
5
|
|
|
6
6
|
def format_messages(messages)
|
|
7
7
|
messages.map do |message|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
if message.is_a?(Hash) && message["type"] == "tool_call"
|
|
9
|
+
format_tool_call_message(message)
|
|
10
|
+
elsif message.is_a?(Hash) && message["type"] == "tool_call_result"
|
|
11
|
+
format_tool_call_result_message(message)
|
|
12
|
+
else
|
|
13
|
+
role = message["role"] || message[:role]
|
|
14
|
+
{
|
|
15
|
+
"role" => role,
|
|
16
|
+
"content" => format_message_content(message["content"] || message[:content], role: role)
|
|
17
|
+
}
|
|
18
|
+
end
|
|
13
19
|
end
|
|
14
20
|
end
|
|
15
21
|
|
|
@@ -39,4 +45,34 @@ module Raif::Concerns::Llms::MessageFormatting
|
|
|
39
45
|
{ "type" => "text", "text" => content }
|
|
40
46
|
end
|
|
41
47
|
|
|
48
|
+
def consolidate_consecutive_role_messages(messages, content_key:)
|
|
49
|
+
# Bedrock, Anthropic, and Google all model tool results as normal role-based
|
|
50
|
+
# message content blocks. After formatting, a tool result can therefore be a
|
|
51
|
+
# "user" message immediately followed by the next user turn. Those providers
|
|
52
|
+
# expect alternating roles, so their adapters collapse adjacent same-role blocks.
|
|
53
|
+
return messages if messages.size <= 1
|
|
54
|
+
|
|
55
|
+
messages.each_with_object([]) do |message, consolidated|
|
|
56
|
+
candidate = message.deep_dup
|
|
57
|
+
previous_message = consolidated.last
|
|
58
|
+
|
|
59
|
+
if mergeable_consecutive_role_messages?(previous_message, candidate, content_key:)
|
|
60
|
+
previous_message[content_key] += candidate[content_key]
|
|
61
|
+
else
|
|
62
|
+
consolidated << candidate
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def mergeable_consecutive_role_messages?(previous_message, message, content_key:)
|
|
70
|
+
previous_message.is_a?(Hash) &&
|
|
71
|
+
message.is_a?(Hash) &&
|
|
72
|
+
previous_message["role"].present? &&
|
|
73
|
+
previous_message["role"] == message["role"] &&
|
|
74
|
+
previous_message[content_key].is_a?(Array) &&
|
|
75
|
+
message[content_key].is_a?(Array)
|
|
76
|
+
end
|
|
77
|
+
|
|
42
78
|
end
|
|
@@ -23,7 +23,7 @@ module Raif::Concerns::Llms::OpenAi::JsonSchemaValidation
|
|
|
23
23
|
# Check properties count (max 100 total)
|
|
24
24
|
validate_properties_count(schema, errors)
|
|
25
25
|
|
|
26
|
-
# Check nesting depth (max
|
|
26
|
+
# Check nesting depth (max 10 levels)
|
|
27
27
|
validate_nesting_depth(schema, errors)
|
|
28
28
|
|
|
29
29
|
# Check for unsupported anyOf at root level
|
|
@@ -118,8 +118,8 @@ private
|
|
|
118
118
|
def validate_nesting_depth(schema, errors, depth = 1)
|
|
119
119
|
return unless schema.is_a?(Hash)
|
|
120
120
|
|
|
121
|
-
if depth >
|
|
122
|
-
errors << "Schema exceeds maximum nesting depth of
|
|
121
|
+
if depth > 10
|
|
122
|
+
errors << "Schema exceeds maximum nesting depth of 10 levels"
|
|
123
123
|
return
|
|
124
124
|
end
|
|
125
125
|
|
|
@@ -38,4 +38,26 @@ module Raif::Concerns::Llms::OpenAiCompletions::MessageFormatting
|
|
|
38
38
|
raise Raif::Errors::InvalidModelFileInputError, "Invalid model image input source type: #{file_input.source_type}"
|
|
39
39
|
end
|
|
40
40
|
end
|
|
41
|
+
|
|
42
|
+
def format_tool_call_message(tool_call)
|
|
43
|
+
{
|
|
44
|
+
"role" => "assistant",
|
|
45
|
+
"tool_calls" => [{
|
|
46
|
+
"id" => tool_call["provider_tool_call_id"],
|
|
47
|
+
"type" => "function",
|
|
48
|
+
"function" => {
|
|
49
|
+
"name" => tool_call["name"],
|
|
50
|
+
"arguments" => JSON.generate(tool_call["arguments"])
|
|
51
|
+
}
|
|
52
|
+
}]
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def format_tool_call_result_message(tool_call_result)
|
|
57
|
+
{
|
|
58
|
+
"role" => "tool",
|
|
59
|
+
"tool_call_id" => tool_call_result["provider_tool_call_id"],
|
|
60
|
+
"content" => tool_call_result["result"].is_a?(String) ? tool_call_result["result"] : JSON.generate(tool_call_result["result"])
|
|
61
|
+
}
|
|
62
|
+
end
|
|
41
63
|
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raif::Concerns::Llms::OpenAiCompletions::ResponseToolCalls
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
def extract_response_tool_calls(resp)
|
|
7
|
+
tool_calls = resp&.dig("choices", 0, "message", "tool_calls")
|
|
8
|
+
return if tool_calls.blank?
|
|
9
|
+
|
|
10
|
+
tool_calls.map do |tool_call|
|
|
11
|
+
{
|
|
12
|
+
"provider_tool_call_id" => tool_call["id"],
|
|
13
|
+
"name" => tool_call["function"]["name"],
|
|
14
|
+
"arguments" => begin
|
|
15
|
+
JSON.parse(tool_call["function"]["arguments"])
|
|
16
|
+
rescue JSON::ParserError
|
|
17
|
+
tool_call["function"]["arguments"]
|
|
18
|
+
end
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -23,4 +23,12 @@ module Raif::Concerns::Llms::OpenAiCompletions::ToolFormatting
|
|
|
23
23
|
end
|
|
24
24
|
end
|
|
25
25
|
end
|
|
26
|
+
|
|
27
|
+
def build_forced_tool_choice(tool_name)
|
|
28
|
+
{ "type" => "function", "function" => { "name" => tool_name } }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def build_required_tool_choice
|
|
32
|
+
"required"
|
|
33
|
+
end
|
|
26
34
|
end
|