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.
Files changed (206) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +8 -7
  3. data/app/assets/builds/raif.css +4 -1
  4. data/app/assets/builds/raif_admin.css +52 -2
  5. data/app/assets/builds/raif_admin_sprockets.js +2709 -0
  6. data/app/assets/javascript/raif/admin/copy_to_clipboard_controller.js +132 -0
  7. data/app/assets/javascript/raif/admin/cost_estimate_controller.js +80 -0
  8. data/app/assets/javascript/raif/admin/judge_config_controller.js +23 -0
  9. data/app/assets/javascript/raif/admin/select_all_checkboxes_controller.js +33 -0
  10. data/app/assets/javascript/raif/admin/sortable_table_controller.js +51 -0
  11. data/app/assets/javascript/raif/admin/table_search_controller.js +15 -0
  12. data/app/assets/javascript/raif/admin/tom_select_controller.js +33 -0
  13. data/app/assets/javascript/raif/controllers/conversations_controller.js +1 -1
  14. data/app/assets/javascript/raif_admin.js +23 -0
  15. data/app/assets/javascript/raif_admin_sprockets.js +24 -0
  16. data/app/assets/stylesheets/raif/admin/conversation.scss +16 -0
  17. data/app/assets/stylesheets/raif/conversations.scss +3 -0
  18. data/app/assets/stylesheets/raif.scss +2 -1
  19. data/app/assets/stylesheets/raif_admin.scss +50 -1
  20. data/app/controllers/raif/admin/agents_controller.rb +27 -1
  21. data/app/controllers/raif/admin/application_controller.rb +16 -0
  22. data/app/controllers/raif/admin/configs_controller.rb +95 -0
  23. data/app/controllers/raif/admin/llms_controller.rb +27 -0
  24. data/app/controllers/raif/admin/model_completions_controller.rb +24 -1
  25. data/app/controllers/raif/admin/model_tool_invocations_controller.rb +7 -1
  26. data/app/controllers/raif/admin/prompt_studio/agents_controller.rb +25 -0
  27. data/app/controllers/raif/admin/prompt_studio/base_controller.rb +32 -0
  28. data/app/controllers/raif/admin/prompt_studio/batch_runs_controller.rb +102 -0
  29. data/app/controllers/raif/admin/prompt_studio/conversations_controller.rb +25 -0
  30. data/app/controllers/raif/admin/prompt_studio/tasks_controller.rb +64 -0
  31. data/app/controllers/raif/admin/stats/model_tool_invocations_controller.rb +21 -0
  32. data/app/controllers/raif/admin/stats/tasks_controller.rb +15 -6
  33. data/app/controllers/raif/admin/stats_controller.rb +32 -3
  34. data/app/controllers/raif/admin/tasks_controller.rb +5 -0
  35. data/app/controllers/raif/conversation_entries_controller.rb +1 -0
  36. data/app/controllers/raif/conversations_controller.rb +10 -2
  37. data/app/helpers/raif/application_helper.rb +40 -0
  38. data/app/jobs/raif/conversation_entry_job.rb +8 -6
  39. data/app/jobs/raif/prompt_studio_batch_run_item_job.rb +11 -0
  40. data/app/jobs/raif/prompt_studio_batch_run_job.rb +15 -0
  41. data/app/jobs/raif/prompt_studio_task_run_job.rb +36 -0
  42. data/app/models/raif/admin/task_stat.rb +7 -0
  43. data/app/models/raif/agent.rb +98 -6
  44. data/app/models/raif/agents/native_tool_calling_agent.rb +179 -52
  45. data/app/models/raif/application_record.rb +18 -0
  46. data/app/models/raif/concerns/agent_inference_stats.rb +35 -0
  47. data/app/models/raif/concerns/has_prompt_templates.rb +88 -0
  48. data/app/models/raif/concerns/has_runtime_duration.rb +41 -0
  49. data/app/models/raif/concerns/json_schema_definition.rb +54 -6
  50. data/app/models/raif/concerns/llm_prompt_caching.rb +20 -0
  51. data/app/models/raif/concerns/llms/anthropic/message_formatting.rb +34 -0
  52. data/app/models/raif/concerns/llms/anthropic/response_tool_calls.rb +24 -0
  53. data/app/models/raif/concerns/llms/anthropic/tool_formatting.rb +8 -0
  54. data/app/models/raif/concerns/llms/bedrock/message_formatting.rb +43 -0
  55. data/app/models/raif/concerns/llms/bedrock/response_tool_calls.rb +26 -0
  56. data/app/models/raif/concerns/llms/bedrock/tool_formatting.rb +8 -0
  57. data/app/models/raif/concerns/llms/google/message_formatting.rb +112 -0
  58. data/app/models/raif/concerns/llms/google/response_tool_calls.rb +32 -0
  59. data/app/models/raif/concerns/llms/google/tool_formatting.rb +76 -0
  60. data/app/models/raif/concerns/llms/message_formatting.rb +41 -5
  61. data/app/models/raif/concerns/llms/open_ai/json_schema_validation.rb +3 -3
  62. data/app/models/raif/concerns/llms/open_ai_completions/message_formatting.rb +22 -0
  63. data/app/models/raif/concerns/llms/open_ai_completions/response_tool_calls.rb +22 -0
  64. data/app/models/raif/concerns/llms/open_ai_completions/tool_formatting.rb +8 -0
  65. data/app/models/raif/concerns/llms/open_ai_responses/message_formatting.rb +17 -0
  66. data/app/models/raif/concerns/llms/open_ai_responses/response_tool_calls.rb +26 -0
  67. data/app/models/raif/concerns/llms/open_ai_responses/tool_formatting.rb +8 -0
  68. data/app/models/raif/concerns/provider_managed_tool_calls.rb +162 -0
  69. data/app/models/raif/concerns/run_with.rb +127 -0
  70. data/app/models/raif/conversation.rb +112 -8
  71. data/app/models/raif/conversation_entry.rb +38 -4
  72. data/app/models/raif/embedding_model.rb +2 -1
  73. data/app/models/raif/embedding_models/bedrock.rb +10 -1
  74. data/app/models/raif/embedding_models/google.rb +37 -0
  75. data/app/models/raif/embedding_models/open_ai.rb +1 -1
  76. data/app/models/raif/evals/llm_judge.rb +70 -0
  77. data/{lib → app/models}/raif/evals/llm_judges/binary.rb +41 -3
  78. data/{lib → app/models}/raif/evals/llm_judges/comparative.rb +41 -3
  79. data/{lib → app/models}/raif/evals/llm_judges/scored.rb +39 -1
  80. data/{lib → app/models}/raif/evals/llm_judges/summarization.rb +40 -2
  81. data/app/models/raif/llm.rb +104 -4
  82. data/app/models/raif/llms/anthropic.rb +32 -22
  83. data/app/models/raif/llms/bedrock.rb +64 -24
  84. data/app/models/raif/llms/google.rb +166 -0
  85. data/app/models/raif/llms/open_ai_base.rb +23 -5
  86. data/app/models/raif/llms/open_ai_completions.rb +14 -12
  87. data/app/models/raif/llms/open_ai_responses.rb +14 -17
  88. data/app/models/raif/llms/open_router.rb +16 -15
  89. data/app/models/raif/model_completion.rb +103 -1
  90. data/app/models/raif/model_tool.rb +55 -5
  91. data/app/models/raif/model_tool_invocation.rb +68 -6
  92. data/app/models/raif/model_tools/agent_final_answer.rb +2 -7
  93. data/app/models/raif/model_tools/provider_managed/code_execution.rb +4 -0
  94. data/app/models/raif/model_tools/provider_managed/image_generation.rb +4 -0
  95. data/app/models/raif/model_tools/provider_managed/web_search.rb +4 -0
  96. data/app/models/raif/prompt_studio_batch_run.rb +155 -0
  97. data/app/models/raif/prompt_studio_batch_run_item.rb +220 -0
  98. data/app/models/raif/streaming_responses/bedrock.rb +60 -1
  99. data/app/models/raif/streaming_responses/google.rb +71 -0
  100. data/app/models/raif/task.rb +85 -18
  101. data/app/models/raif/user_tool_invocation.rb +19 -0
  102. data/app/views/layouts/raif/admin.html.erb +43 -2
  103. data/app/views/raif/admin/agents/_agent.html.erb +9 -0
  104. data/app/views/raif/admin/agents/_conversation_message.html.erb +28 -6
  105. data/app/views/raif/admin/agents/index.html.erb +50 -0
  106. data/app/views/raif/admin/agents/show.html.erb +50 -1
  107. data/app/views/raif/admin/configs/show.html.erb +117 -0
  108. data/app/views/raif/admin/conversations/_conversation_entry.html.erb +29 -34
  109. data/app/views/raif/admin/conversations/show.html.erb +2 -0
  110. data/app/views/raif/admin/llms/index.html.erb +110 -0
  111. data/app/views/raif/admin/model_completions/_model_completion.html.erb +10 -5
  112. data/app/views/raif/admin/model_completions/index.html.erb +40 -1
  113. data/app/views/raif/admin/model_completions/show.html.erb +256 -84
  114. data/app/views/raif/admin/model_tool_invocations/index.html.erb +22 -1
  115. data/app/views/raif/admin/model_tool_invocations/show.html.erb +18 -0
  116. data/app/views/raif/admin/model_tools/_list.html.erb +16 -0
  117. data/app/views/raif/admin/model_tools/_model_tool.html.erb +36 -0
  118. data/app/views/raif/admin/prompt_studio/agents/index.html.erb +56 -0
  119. data/app/views/raif/admin/prompt_studio/agents/show.html.erb +57 -0
  120. data/app/views/raif/admin/prompt_studio/batch_runs/_batch_run_item.html.erb +54 -0
  121. data/app/views/raif/admin/prompt_studio/batch_runs/_judge_config_fields.html.erb +76 -0
  122. data/app/views/raif/admin/prompt_studio/batch_runs/_judge_detail_modal.html.erb +27 -0
  123. data/app/views/raif/admin/prompt_studio/batch_runs/_modal.html.erb +35 -0
  124. data/app/views/raif/admin/prompt_studio/batch_runs/_progress.html.erb +78 -0
  125. data/app/views/raif/admin/prompt_studio/batch_runs/show.html.erb +49 -0
  126. data/app/views/raif/admin/prompt_studio/conversations/index.html.erb +48 -0
  127. data/app/views/raif/admin/prompt_studio/conversations/show.html.erb +36 -0
  128. data/app/views/raif/admin/prompt_studio/shared/_nav_tabs.html.erb +17 -0
  129. data/app/views/raif/admin/prompt_studio/shared/_prompt_comparison.html.erb +87 -0
  130. data/app/views/raif/admin/prompt_studio/shared/_type_filter.html.erb +54 -0
  131. data/app/views/raif/admin/prompt_studio/tasks/_task_result.html.erb +145 -0
  132. data/app/views/raif/admin/prompt_studio/tasks/_task_row.html.erb +12 -0
  133. data/app/views/raif/admin/prompt_studio/tasks/_task_type_filter.html.erb +58 -0
  134. data/app/views/raif/admin/prompt_studio/tasks/_tasks_table.html.erb +22 -0
  135. data/app/views/raif/admin/prompt_studio/tasks/index.html.erb +35 -0
  136. data/app/views/raif/admin/prompt_studio/tasks/show.html.erb +19 -0
  137. data/app/views/raif/admin/stats/_stats_tile.html.erb +34 -0
  138. data/app/views/raif/admin/stats/index.html.erb +71 -88
  139. data/app/views/raif/admin/stats/model_tool_invocations/index.html.erb +43 -0
  140. data/app/views/raif/admin/stats/tasks/index.html.erb +20 -6
  141. data/app/views/raif/admin/tasks/_task.html.erb +1 -0
  142. data/app/views/raif/admin/tasks/index.html.erb +23 -6
  143. data/app/views/raif/admin/tasks/show.html.erb +56 -3
  144. data/app/views/raif/conversation_entries/_form.html.erb +3 -0
  145. data/app/views/raif/conversation_entries/_message.html.erb +10 -6
  146. data/app/views/raif/conversations/_conversation.html.erb +10 -0
  147. data/app/views/raif/conversations/_entry_processed.turbo_stream.erb +12 -0
  148. data/app/views/raif/conversations/index.html.erb +23 -0
  149. data/config/importmap.rb +8 -0
  150. data/config/locales/admin.en.yml +161 -1
  151. data/config/locales/en.yml +67 -4
  152. data/config/routes.rb +10 -0
  153. data/db/migrate/20250904194456_add_generating_entry_response_to_raif_conversations.rb +7 -0
  154. data/db/migrate/20250911125234_add_source_to_raif_tasks.rb +7 -0
  155. data/db/migrate/20251020005853_add_source_to_raif_agents.rb +7 -0
  156. data/db/migrate/20251020011346_rename_task_run_args_to_run_with.rb +7 -0
  157. data/db/migrate/20251020011405_add_run_with_to_raif_agents.rb +13 -0
  158. data/db/migrate/20251024160119_add_llm_messages_max_length_to_raif_conversations.rb +14 -0
  159. data/db/migrate/20251124185033_add_provider_tool_call_id_to_raif_model_tool_invocations.rb +7 -0
  160. data/db/migrate/20251128202941_add_tool_choice_to_raif_model_completions.rb +7 -0
  161. data/db/migrate/20260118144846_add_source_to_raif_conversations.rb +7 -0
  162. data/db/migrate/20260119000000_add_failure_tracking_to_raif_model_completions.rb +10 -0
  163. data/db/migrate/20260119000001_add_completed_at_to_raif_model_completions.rb +8 -0
  164. data/db/migrate/20260119000002_add_started_at_to_raif_model_completions.rb +8 -0
  165. data/db/migrate/20260307000000_add_prompt_studio_run_to_raif_tasks.rb +7 -0
  166. data/db/migrate/20260308000000_create_raif_prompt_studio_batch_runs.rb +27 -0
  167. data/db/migrate/20260308000001_create_raif_prompt_studio_batch_run_items.rb +24 -0
  168. data/db/migrate/20260407000000_add_cache_token_columns_to_raif_model_completions.rb +8 -0
  169. data/lib/generators/raif/agent/agent_generator.rb +18 -0
  170. data/lib/generators/raif/agent/templates/agent.rb.tt +7 -5
  171. data/lib/generators/raif/agent/templates/application_agent.rb.tt +1 -1
  172. data/lib/generators/raif/agent/templates/system_prompt.erb.tt +3 -0
  173. data/lib/generators/raif/conversation/conversation_generator.rb +19 -1
  174. data/lib/generators/raif/conversation/templates/conversation.rb.tt +6 -0
  175. data/lib/generators/raif/conversation/templates/system_prompt.erb.tt +4 -0
  176. data/lib/generators/raif/install/templates/initializer.rb +117 -8
  177. data/lib/generators/raif/task/task_generator.rb +18 -0
  178. data/lib/generators/raif/task/templates/prompt.erb.tt +4 -0
  179. data/lib/generators/raif/task/templates/task.rb.tt +10 -9
  180. data/lib/raif/configuration.rb +47 -2
  181. data/lib/raif/embedding_model_registry.rb +8 -0
  182. data/lib/raif/engine.rb +24 -1
  183. data/lib/raif/errors/blank_response_error.rb +8 -0
  184. data/lib/raif/errors/instance_dependent_schema_error.rb +8 -0
  185. data/lib/raif/errors/prompt_template_error.rb +15 -0
  186. data/lib/raif/errors/streaming_error.rb +6 -3
  187. data/lib/raif/errors.rb +3 -0
  188. data/lib/raif/evals/run.rb +1 -0
  189. data/lib/raif/evals.rb +0 -6
  190. data/lib/raif/json_schema_builder.rb +14 -0
  191. data/lib/raif/llm_registry.rb +433 -42
  192. data/lib/raif/messages.rb +180 -0
  193. data/lib/raif/prompt_studio_comparison_builder.rb +138 -0
  194. data/lib/raif/token_estimator.rb +28 -0
  195. data/lib/raif/version.rb +1 -1
  196. data/lib/raif.rb +11 -0
  197. data/lib/tasks/annotate_rb.rake +10 -0
  198. data/spec/support/rspec_helpers.rb +15 -9
  199. data/spec/support/test_task.rb +9 -0
  200. data/spec/support/test_template_task.rb +41 -0
  201. metadata +108 -15
  202. data/app/models/raif/agents/re_act_agent.rb +0 -127
  203. data/app/models/raif/agents/re_act_step.rb +0 -32
  204. data/app/models/raif/concerns/task_run_args.rb +0 -62
  205. data/lib/raif/evals/llm_judge.rb +0 -32
  206. /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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Raif
4
- VERSION = "1.3.0"
4
+ VERSION = "1.5.0"
5
5
  end
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
@@ -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