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,180 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raif
|
|
4
|
+
# Message types for agent conversation_history and conversation llm_messages.
|
|
5
|
+
#
|
|
6
|
+
# These classes provide a structured API for creating messages that get stored
|
|
7
|
+
# as JSONB and passed to LLM providers. Each class has:
|
|
8
|
+
# - Named parameters for initialization
|
|
9
|
+
# - `to_h` for converting to hash format (for storage/API calls)
|
|
10
|
+
# - `from_h` class method for deserializing from stored hashes
|
|
11
|
+
#
|
|
12
|
+
# @example Creating messages
|
|
13
|
+
# message = Raif::Messages::ToolCall.new(
|
|
14
|
+
# name: "wikipedia_search",
|
|
15
|
+
# arguments: { query: "Ruby" },
|
|
16
|
+
# provider_tool_call_id: "call_123"
|
|
17
|
+
# )
|
|
18
|
+
# conversation_history << message.to_h
|
|
19
|
+
#
|
|
20
|
+
# @example Deserializing stored messages
|
|
21
|
+
# messages = Raif::Messages.from_array(agent.conversation_history)
|
|
22
|
+
# messages.each do |msg|
|
|
23
|
+
# case msg
|
|
24
|
+
# when Raif::Messages::ToolCall
|
|
25
|
+
# puts "Tool: #{msg.name}"
|
|
26
|
+
# when Raif::Messages::UserMessage
|
|
27
|
+
# puts "User: #{msg.content}"
|
|
28
|
+
# end
|
|
29
|
+
# end
|
|
30
|
+
module Messages
|
|
31
|
+
# User role message
|
|
32
|
+
class UserMessage
|
|
33
|
+
attr_reader :content
|
|
34
|
+
|
|
35
|
+
# @param content [String] The user's message content
|
|
36
|
+
def initialize(content:)
|
|
37
|
+
@content = content
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @return [Hash] Hash representation for JSONB storage and LLM APIs
|
|
41
|
+
def to_h
|
|
42
|
+
{ "role" => "user", "content" => content }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Deserialize from a hash
|
|
46
|
+
# @param hash [Hash] A hash with "content" key
|
|
47
|
+
# @return [UserMessage]
|
|
48
|
+
def self.from_h(hash)
|
|
49
|
+
new(content: hash["content"])
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Assistant role message
|
|
54
|
+
class AssistantMessage
|
|
55
|
+
attr_reader :content
|
|
56
|
+
|
|
57
|
+
# @param content [String] The assistant's message content
|
|
58
|
+
def initialize(content:)
|
|
59
|
+
@content = content
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @return [Hash] Hash representation for JSONB storage and LLM APIs
|
|
63
|
+
def to_h
|
|
64
|
+
{ "role" => "assistant", "content" => content }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Deserialize from a hash
|
|
68
|
+
# @param hash [Hash] A hash with "content" key
|
|
69
|
+
# @return [AssistantMessage]
|
|
70
|
+
def self.from_h(hash)
|
|
71
|
+
new(content: hash["content"])
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Tool invocation request from the assistant
|
|
76
|
+
class ToolCall
|
|
77
|
+
attr_reader :provider_tool_call_id, :name, :arguments, :assistant_message, :provider_metadata
|
|
78
|
+
|
|
79
|
+
# @param name [String] The tool name (snake_case)
|
|
80
|
+
# @param arguments [Hash] The arguments passed to the tool
|
|
81
|
+
# @param provider_tool_call_id [String, nil] Provider-assigned ID for the tool call
|
|
82
|
+
# @param assistant_message [String, nil] Optional assistant message accompanying the tool call
|
|
83
|
+
# @param provider_metadata [Hash, nil] Provider-specific metadata (e.g., Google's thoughtSignature)
|
|
84
|
+
def initialize(name:, arguments:, provider_tool_call_id: nil, assistant_message: nil, provider_metadata: nil)
|
|
85
|
+
@provider_tool_call_id = provider_tool_call_id
|
|
86
|
+
@name = name
|
|
87
|
+
@arguments = arguments
|
|
88
|
+
@assistant_message = assistant_message
|
|
89
|
+
@provider_metadata = provider_metadata
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# @return [Hash] Hash representation for JSONB storage and LLM APIs
|
|
93
|
+
def to_h
|
|
94
|
+
{
|
|
95
|
+
"type" => "tool_call",
|
|
96
|
+
"provider_tool_call_id" => provider_tool_call_id,
|
|
97
|
+
"name" => name,
|
|
98
|
+
"arguments" => arguments,
|
|
99
|
+
"assistant_message" => assistant_message,
|
|
100
|
+
"provider_metadata" => provider_metadata
|
|
101
|
+
}.compact
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Deserialize from a hash
|
|
105
|
+
# @param hash [Hash] A hash with tool call fields
|
|
106
|
+
# @return [ToolCall]
|
|
107
|
+
def self.from_h(hash)
|
|
108
|
+
new(
|
|
109
|
+
name: hash["name"],
|
|
110
|
+
arguments: hash["arguments"],
|
|
111
|
+
provider_tool_call_id: hash["provider_tool_call_id"],
|
|
112
|
+
assistant_message: hash["assistant_message"],
|
|
113
|
+
provider_metadata: hash["provider_metadata"]
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Result of a tool invocation
|
|
119
|
+
class ToolCallResult
|
|
120
|
+
attr_reader :provider_tool_call_id, :name, :result
|
|
121
|
+
|
|
122
|
+
# @param result [Hash, String] The result returned by the tool
|
|
123
|
+
# @param provider_tool_call_id [String, nil] Provider-assigned ID matching the tool call
|
|
124
|
+
# @param name [String, nil] The tool name (required by some providers like Google)
|
|
125
|
+
def initialize(result:, provider_tool_call_id: nil, name: nil)
|
|
126
|
+
@provider_tool_call_id = provider_tool_call_id
|
|
127
|
+
@name = name
|
|
128
|
+
@result = result
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# @return [Hash] Hash representation for JSONB storage and LLM APIs
|
|
132
|
+
def to_h
|
|
133
|
+
{
|
|
134
|
+
"type" => "tool_call_result",
|
|
135
|
+
"provider_tool_call_id" => provider_tool_call_id,
|
|
136
|
+
"name" => name,
|
|
137
|
+
"result" => result
|
|
138
|
+
}.compact
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Deserialize from a hash
|
|
142
|
+
# @param hash [Hash] A hash with tool call result fields
|
|
143
|
+
# @return [ToolCallResult]
|
|
144
|
+
def self.from_h(hash)
|
|
145
|
+
new(
|
|
146
|
+
provider_tool_call_id: hash["provider_tool_call_id"],
|
|
147
|
+
name: hash["name"],
|
|
148
|
+
result: hash["result"]
|
|
149
|
+
)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
class << self
|
|
154
|
+
# Deserialize a single message hash into the appropriate message class
|
|
155
|
+
# @param hash [Hash] A message hash with either "role" or "type" key
|
|
156
|
+
# @return [UserMessage, AssistantMessage, ToolCall, ToolCallResult]
|
|
157
|
+
# @raise [ArgumentError] if the hash doesn't match a known message type
|
|
158
|
+
def from_h(hash)
|
|
159
|
+
if hash["type"] == "tool_call"
|
|
160
|
+
ToolCall.from_h(hash)
|
|
161
|
+
elsif hash["type"] == "tool_call_result"
|
|
162
|
+
ToolCallResult.from_h(hash)
|
|
163
|
+
elsif hash["role"] == "user"
|
|
164
|
+
UserMessage.from_h(hash)
|
|
165
|
+
elsif hash["role"] == "assistant"
|
|
166
|
+
AssistantMessage.from_h(hash)
|
|
167
|
+
else
|
|
168
|
+
raise ArgumentError, "Unknown message type: #{hash.inspect}"
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Deserialize an array of message hashes
|
|
173
|
+
# @param messages [Array<Hash>] Array of message hashes
|
|
174
|
+
# @return [Array<UserMessage, AssistantMessage, ToolCall, ToolCallResult>]
|
|
175
|
+
def from_array(messages)
|
|
176
|
+
messages.map { |msg| from_h(msg) }
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raif
|
|
4
|
+
class PromptStudioComparisonBuilder
|
|
5
|
+
# Attempts to rebuild the prompt from current code for a given record.
|
|
6
|
+
# Returns a hash with the rendered prompts and any warnings.
|
|
7
|
+
def self.build(record)
|
|
8
|
+
new(record).build
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(record)
|
|
12
|
+
@record = record
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def build
|
|
16
|
+
{
|
|
17
|
+
original_prompt: original_prompt,
|
|
18
|
+
original_system_prompt: original_system_prompt,
|
|
19
|
+
current_prompt: current_prompt,
|
|
20
|
+
current_system_prompt: current_system_prompt,
|
|
21
|
+
prompt_changed: changed?(original_prompt, current_prompt),
|
|
22
|
+
system_prompt_changed: changed?(original_system_prompt, current_system_prompt),
|
|
23
|
+
has_stale_references: has_stale_references?,
|
|
24
|
+
warnings: warnings,
|
|
25
|
+
original_prompt_tokens: original_prompt_tokens,
|
|
26
|
+
original_prompt_token_cost: original_prompt_token_cost,
|
|
27
|
+
current_prompt_token_estimate: current_prompt_token_estimate,
|
|
28
|
+
current_prompt_cost_estimate: current_prompt_cost_estimate
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def original_prompt
|
|
35
|
+
@original_prompt ||= @record.respond_to?(:prompt) ? @record.prompt : nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def original_system_prompt
|
|
39
|
+
@original_system_prompt ||= @record.system_prompt
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def current_prompt
|
|
43
|
+
return @current_prompt if defined?(@current_prompt)
|
|
44
|
+
|
|
45
|
+
@current_prompt = begin
|
|
46
|
+
@record.build_prompt
|
|
47
|
+
rescue NotImplementedError
|
|
48
|
+
nil
|
|
49
|
+
rescue => e
|
|
50
|
+
warnings << "Error rendering current prompt: #{e.message}"
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def current_system_prompt
|
|
56
|
+
return @current_system_prompt if defined?(@current_system_prompt)
|
|
57
|
+
|
|
58
|
+
@current_system_prompt = begin
|
|
59
|
+
@record.build_system_prompt
|
|
60
|
+
rescue NotImplementedError
|
|
61
|
+
nil
|
|
62
|
+
rescue => e
|
|
63
|
+
warnings << "Error rendering current system prompt: #{e.message}"
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def warnings
|
|
69
|
+
@warnings ||= [].tap do |w|
|
|
70
|
+
w << I18n.t("raif.admin.prompt_studio.common.warning_stale_reference") if has_stale_references?
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def has_stale_references?
|
|
75
|
+
return @has_stale_references if defined?(@has_stale_references)
|
|
76
|
+
|
|
77
|
+
@has_stale_references = detect_stale_references
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def detect_stale_references
|
|
81
|
+
return false unless @record.respond_to?(:run_with) && @record.run_with.present?
|
|
82
|
+
|
|
83
|
+
@record.run_with.each do |_key, value|
|
|
84
|
+
if value.is_a?(String) && value.start_with?("gid://")
|
|
85
|
+
begin
|
|
86
|
+
return true if GlobalID::Locator.locate(value).nil?
|
|
87
|
+
rescue StandardError
|
|
88
|
+
return true
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
false
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def changed?(original, current)
|
|
97
|
+
original.present? && current.present? && original.strip != current.strip
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def original_prompt_tokens
|
|
101
|
+
return unless @record.respond_to?(:raif_model_completion)
|
|
102
|
+
|
|
103
|
+
@record.raif_model_completion&.prompt_tokens
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def original_prompt_token_cost
|
|
107
|
+
return unless @record.respond_to?(:raif_model_completion)
|
|
108
|
+
|
|
109
|
+
@record.raif_model_completion&.prompt_token_cost
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def current_prompt_token_estimate
|
|
113
|
+
return unless prompt_changed? || system_prompt_changed?
|
|
114
|
+
|
|
115
|
+
Raif::TokenEstimator.estimate_tokens(current_system_prompt, current_prompt)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def current_prompt_cost_estimate
|
|
119
|
+
token_estimate = current_prompt_token_estimate
|
|
120
|
+
return unless token_estimate
|
|
121
|
+
|
|
122
|
+
return unless @record.llm_model_key.present?
|
|
123
|
+
|
|
124
|
+
llm_config = Raif.llm_config(@record.llm_model_key.to_sym)
|
|
125
|
+
return unless llm_config&.dig(:input_token_cost)
|
|
126
|
+
|
|
127
|
+
llm_config[:input_token_cost] * token_estimate
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def prompt_changed?
|
|
131
|
+
changed?(original_prompt, current_prompt)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def system_prompt_changed?
|
|
135
|
+
changed?(original_system_prompt, current_system_prompt)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raif
|
|
4
|
+
class TokenEstimator
|
|
5
|
+
def self.available?
|
|
6
|
+
return true if defined?(::Tiktoken)
|
|
7
|
+
|
|
8
|
+
require "tiktoken_ruby"
|
|
9
|
+
!!defined?(::Tiktoken)
|
|
10
|
+
rescue LoadError
|
|
11
|
+
false
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Estimates the total token count for a prompt + system prompt combination.
|
|
15
|
+
# Returns nil if tiktoken_ruby is not installed.
|
|
16
|
+
def self.estimate_tokens(*texts)
|
|
17
|
+
return unless available?
|
|
18
|
+
|
|
19
|
+
encoder = encoder_for_model("gpt-4")
|
|
20
|
+
texts.compact.sum { |text| encoder.encode(text).length }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.encoder_for_model(model)
|
|
24
|
+
@encoders ||= {}
|
|
25
|
+
@encoders[model] ||= ::Tiktoken.encoding_for_model(model)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
data/lib/raif/version.rb
CHANGED
data/lib/raif.rb
CHANGED
|
@@ -10,6 +10,9 @@ require "raif/llm_registry"
|
|
|
10
10
|
require "raif/embedding_model_registry"
|
|
11
11
|
require "raif/json_schema_builder"
|
|
12
12
|
require "raif/migration_checker"
|
|
13
|
+
require "raif/messages"
|
|
14
|
+
require "raif/prompt_studio_comparison_builder"
|
|
15
|
+
require "raif/token_estimator"
|
|
13
16
|
|
|
14
17
|
require "faraday"
|
|
15
18
|
require "event_stream_parser"
|
|
@@ -41,4 +44,12 @@ module Raif
|
|
|
41
44
|
def self.running_evals?
|
|
42
45
|
ENV["RAIF_RUNNING_EVALS"] == "true"
|
|
43
46
|
end
|
|
47
|
+
|
|
48
|
+
def self.default_request_options
|
|
49
|
+
{
|
|
50
|
+
open_timeout: config.request_open_timeout,
|
|
51
|
+
read_timeout: config.request_read_timeout,
|
|
52
|
+
write_timeout: config.request_write_timeout
|
|
53
|
+
}.compact
|
|
54
|
+
end
|
|
44
55
|
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# This rake task was added by annotate_rb gem.
|
|
4
|
+
|
|
5
|
+
# Can set `ANNOTATERB_SKIP_ON_DB_TASKS` to be anything to skip this
|
|
6
|
+
if Rails.env.development? && ENV["ANNOTATERB_SKIP_ON_DB_TASKS"].nil?
|
|
7
|
+
require "annotate_rb"
|
|
8
|
+
|
|
9
|
+
AnnotateRb::Core.load_rake_tasks
|
|
10
|
+
end
|
|
@@ -3,17 +3,23 @@
|
|
|
3
3
|
module Raif
|
|
4
4
|
module RspecHelpers
|
|
5
5
|
|
|
6
|
-
def stubbed_llm(llm_model_key, &block)
|
|
6
|
+
def stubbed_llm(llm_model_key, source_instance, &block)
|
|
7
7
|
test_llm = Raif.llm(llm_model_key.to_sym)
|
|
8
8
|
|
|
9
|
-
allow(test_llm).to receive(:perform_model_completion!) do |model_completion|
|
|
10
|
-
result = block.call(model_completion.messages, model_completion)
|
|
9
|
+
allow(test_llm).to receive(:perform_model_completion!) do |model_completion, &streaming_block|
|
|
10
|
+
result = block.call(model_completion.messages, model_completion, source_instance)
|
|
11
11
|
model_completion.raw_response = result if result.is_a?(String)
|
|
12
12
|
model_completion.completion_tokens = rand(100..2000)
|
|
13
13
|
model_completion.prompt_tokens = rand(100..2000)
|
|
14
14
|
model_completion.total_tokens = model_completion.completion_tokens + model_completion.prompt_tokens
|
|
15
15
|
model_completion.save!
|
|
16
16
|
|
|
17
|
+
if streaming_block && result.is_a?(String)
|
|
18
|
+
result.chars.each_slice(25) do |chunk|
|
|
19
|
+
streaming_block.call(model_completion, chunk.join, nil)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
17
23
|
model_completion
|
|
18
24
|
end
|
|
19
25
|
|
|
@@ -24,10 +30,10 @@ module Raif
|
|
|
24
30
|
allow(Raif.config).to receive(:llm_api_requests_enabled){ true }
|
|
25
31
|
|
|
26
32
|
if task.is_a?(Raif::Task)
|
|
27
|
-
allow(task).to receive(:llm){ stubbed_llm(task.llm_model_key, &block) }
|
|
33
|
+
allow(task).to receive(:llm){ stubbed_llm(task.llm_model_key, task, &block) }
|
|
28
34
|
else
|
|
29
35
|
allow_any_instance_of(task).to receive(:llm) do |task_instance|
|
|
30
|
-
stubbed_llm(task_instance.llm_model_key, &block)
|
|
36
|
+
stubbed_llm(task_instance.llm_model_key, task_instance, &block)
|
|
31
37
|
end
|
|
32
38
|
end
|
|
33
39
|
end
|
|
@@ -36,10 +42,10 @@ module Raif
|
|
|
36
42
|
allow(Raif.config).to receive(:llm_api_requests_enabled){ true }
|
|
37
43
|
|
|
38
44
|
if conversation.is_a?(Raif::Conversation)
|
|
39
|
-
allow(conversation).to receive(:llm){ stubbed_llm(conversation.llm_model_key, &block) }
|
|
45
|
+
allow(conversation).to receive(:llm){ stubbed_llm(conversation.llm_model_key, conversation, &block) }
|
|
40
46
|
else
|
|
41
47
|
allow_any_instance_of(conversation).to receive(:llm) do |conversation_instance|
|
|
42
|
-
stubbed_llm(conversation_instance.llm_model_key, &block)
|
|
48
|
+
stubbed_llm(conversation_instance.llm_model_key, conversation_instance, &block)
|
|
43
49
|
end
|
|
44
50
|
end
|
|
45
51
|
end
|
|
@@ -48,10 +54,10 @@ module Raif
|
|
|
48
54
|
allow(Raif.config).to receive(:llm_api_requests_enabled){ true }
|
|
49
55
|
|
|
50
56
|
if agent.is_a?(Raif::Agent)
|
|
51
|
-
allow(agent).to receive(:llm){ stubbed_llm(agent.llm_model_key, &block) }
|
|
57
|
+
allow(agent).to receive(:llm){ stubbed_llm(agent.llm_model_key, agent, &block) }
|
|
52
58
|
else
|
|
53
59
|
allow_any_instance_of(agent).to receive(:llm) do |agent_instance|
|
|
54
|
-
stubbed_llm(agent_instance.llm_model_key, &block)
|
|
60
|
+
stubbed_llm(agent_instance.llm_model_key, agent_instance, &block)
|
|
55
61
|
end
|
|
56
62
|
end
|
|
57
63
|
end
|
data/spec/support/test_task.rb
CHANGED
|
@@ -30,6 +30,15 @@ class Raif::TestJsonTask < Raif::Task
|
|
|
30
30
|
end
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
+
class Raif::TestCachedTask < Raif::Task
|
|
34
|
+
enable_anthropic_prompt_caching
|
|
35
|
+
enable_bedrock_prompt_caching
|
|
36
|
+
|
|
37
|
+
def build_prompt
|
|
38
|
+
"Tell me a joke"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
33
42
|
class Raif::TestHtmlTask < Raif::Task
|
|
34
43
|
llm_response_format :html
|
|
35
44
|
llm_response_allowed_tags %w[p b i u s a]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Raif::TestTemplateTask < Raif::Task
|
|
4
|
+
run_with :topic
|
|
5
|
+
|
|
6
|
+
after_initialize -> { self.topic ||= "pirates" }
|
|
7
|
+
|
|
8
|
+
def topic_description
|
|
9
|
+
"the topic of #{topic}"
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
class Raif::TestTemplateSystemPromptTask < Raif::Task
|
|
14
|
+
run_with :persona
|
|
15
|
+
|
|
16
|
+
after_initialize -> { self.persona ||= "comedian" }
|
|
17
|
+
|
|
18
|
+
def build_prompt
|
|
19
|
+
"Tell me a joke"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class Raif::TestTemplateConversation < Raif::Conversation
|
|
24
|
+
attr_writer :persona
|
|
25
|
+
|
|
26
|
+
def persona
|
|
27
|
+
@persona || "helpful assistant"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class Raif::TestTemplateWithPartialTask < Raif::Task
|
|
32
|
+
run_with :topic
|
|
33
|
+
|
|
34
|
+
after_initialize -> { self.topic ||= "dogs" }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class Raif::TestTemplateAgent < Raif::Agent
|
|
38
|
+
run_with :agent_role
|
|
39
|
+
|
|
40
|
+
after_initialize -> { self.agent_role ||= "researcher" }
|
|
41
|
+
end
|