raif 1.0.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 (121) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +678 -0
  4. data/Rakefile +20 -0
  5. data/app/assets/builds/raif.css +74 -0
  6. data/app/assets/builds/raif_admin.css +266 -0
  7. data/app/assets/config/raif_manifest.js +1 -0
  8. data/app/assets/javascript/raif/controllers/conversations_controller.js +11 -0
  9. data/app/assets/javascript/raif/stream_actions/raif_scroll_to_bottom.js +12 -0
  10. data/app/assets/javascript/raif.js +10 -0
  11. data/app/assets/stylesheets/raif/admin/conversation.scss +64 -0
  12. data/app/assets/stylesheets/raif/loader.scss +85 -0
  13. data/app/assets/stylesheets/raif.scss +1 -0
  14. data/app/assets/stylesheets/raif_admin.scss +299 -0
  15. data/app/controllers/raif/admin/agents_controller.rb +17 -0
  16. data/app/controllers/raif/admin/application_controller.rb +20 -0
  17. data/app/controllers/raif/admin/conversations_controller.rb +17 -0
  18. data/app/controllers/raif/admin/model_completions_controller.rb +17 -0
  19. data/app/controllers/raif/admin/model_tool_invocations_controller.rb +17 -0
  20. data/app/controllers/raif/admin/tasks_controller.rb +23 -0
  21. data/app/controllers/raif/application_controller.rb +20 -0
  22. data/app/controllers/raif/conversation_entries_controller.rb +60 -0
  23. data/app/controllers/raif/conversations_controller.rb +58 -0
  24. data/app/helpers/raif/application_helper.rb +7 -0
  25. data/app/helpers/raif/shared/conversations_helper.rb +13 -0
  26. data/app/jobs/raif/application_job.rb +8 -0
  27. data/app/jobs/raif/conversation_entry_job.rb +30 -0
  28. data/app/models/raif/agent.rb +133 -0
  29. data/app/models/raif/agents/native_tool_calling_agent.rb +127 -0
  30. data/app/models/raif/agents/re_act_agent.rb +121 -0
  31. data/app/models/raif/agents/re_act_step.rb +33 -0
  32. data/app/models/raif/application_record.rb +14 -0
  33. data/app/models/raif/concerns/boolean_timestamp.rb +69 -0
  34. data/app/models/raif/concerns/has_available_model_tools.rb +13 -0
  35. data/app/models/raif/concerns/has_llm.rb +19 -0
  36. data/app/models/raif/concerns/has_requested_language.rb +20 -0
  37. data/app/models/raif/concerns/invokes_model_tools.rb +13 -0
  38. data/app/models/raif/concerns/llm_response_parsing.rb +44 -0
  39. data/app/models/raif/conversation.rb +67 -0
  40. data/app/models/raif/conversation_entry.rb +85 -0
  41. data/app/models/raif/llm.rb +88 -0
  42. data/app/models/raif/llms/anthropic.rb +120 -0
  43. data/app/models/raif/llms/bedrock_claude.rb +134 -0
  44. data/app/models/raif/llms/open_ai.rb +259 -0
  45. data/app/models/raif/model_completion.rb +28 -0
  46. data/app/models/raif/model_tool.rb +69 -0
  47. data/app/models/raif/model_tool_invocation.rb +43 -0
  48. data/app/models/raif/model_tools/agent_final_answer.rb +46 -0
  49. data/app/models/raif/model_tools/fetch_url.rb +57 -0
  50. data/app/models/raif/model_tools/wikipedia_search.rb +78 -0
  51. data/app/models/raif/task.rb +137 -0
  52. data/app/models/raif/user_tool_invocation.rb +29 -0
  53. data/app/views/layouts/raif/admin.html.erb +98 -0
  54. data/app/views/raif/admin/agents/_agent.html.erb +18 -0
  55. data/app/views/raif/admin/agents/_conversation_message.html.erb +15 -0
  56. data/app/views/raif/admin/agents/index.html.erb +33 -0
  57. data/app/views/raif/admin/agents/show.html.erb +131 -0
  58. data/app/views/raif/admin/conversations/_conversation.html.erb +7 -0
  59. data/app/views/raif/admin/conversations/_conversation_entry.html.erb +34 -0
  60. data/app/views/raif/admin/conversations/index.html.erb +32 -0
  61. data/app/views/raif/admin/conversations/show.html.erb +56 -0
  62. data/app/views/raif/admin/model_completions/_model_completion.html.erb +9 -0
  63. data/app/views/raif/admin/model_completions/index.html.erb +34 -0
  64. data/app/views/raif/admin/model_completions/show.html.erb +117 -0
  65. data/app/views/raif/admin/model_tool_invocations/_model_tool_invocation.html.erb +16 -0
  66. data/app/views/raif/admin/model_tool_invocations/index.html.erb +33 -0
  67. data/app/views/raif/admin/model_tool_invocations/show.html.erb +66 -0
  68. data/app/views/raif/admin/tasks/_task.html.erb +19 -0
  69. data/app/views/raif/admin/tasks/index.html.erb +49 -0
  70. data/app/views/raif/admin/tasks/show.html.erb +176 -0
  71. data/app/views/raif/conversation_entries/_conversation_entry.html.erb +26 -0
  72. data/app/views/raif/conversation_entries/_form.html.erb +25 -0
  73. data/app/views/raif/conversation_entries/_form_with_available_tools.html.erb +4 -0
  74. data/app/views/raif/conversation_entries/_form_with_user_tool_invocation.html.erb +18 -0
  75. data/app/views/raif/conversation_entries/_message.html.erb +17 -0
  76. data/app/views/raif/conversation_entries/_model_response_avatar.html.erb +1 -0
  77. data/app/views/raif/conversation_entries/_user_avatar.html.erb +1 -0
  78. data/app/views/raif/conversation_entries/create.turbo_stream.erb +11 -0
  79. data/app/views/raif/conversation_entries/new.turbo_stream.erb +6 -0
  80. data/app/views/raif/conversations/_available_user_tools.html.erb +11 -0
  81. data/app/views/raif/conversations/_full_conversation.html.erb +15 -0
  82. data/app/views/raif/conversations/show.html.erb +1 -0
  83. data/config/i18n-tasks.yml +181 -0
  84. data/config/importmap.rb +6 -0
  85. data/config/initializers/pagy.rb +14 -0
  86. data/config/locales/admin.en.yml +91 -0
  87. data/config/locales/en.yml +50 -0
  88. data/config/routes.rb +22 -0
  89. data/db/migrate/20250224234252_create_raif_tables.rb +114 -0
  90. data/lib/generators/raif/agent/agent_generator.rb +22 -0
  91. data/lib/generators/raif/agent/templates/agent.rb.tt +28 -0
  92. data/lib/generators/raif/conversation/conversation_generator.rb +27 -0
  93. data/lib/generators/raif/conversation/templates/conversation.rb.tt +37 -0
  94. data/lib/generators/raif/install/install_generator.rb +31 -0
  95. data/lib/generators/raif/install/templates/initializer.rb +81 -0
  96. data/lib/generators/raif/model_tool/model_tool_generator.rb +27 -0
  97. data/lib/generators/raif/model_tool/templates/model_tool.rb.tt +74 -0
  98. data/lib/generators/raif/task/task_generator.rb +28 -0
  99. data/lib/generators/raif/task/templates/application_task.rb.tt +7 -0
  100. data/lib/generators/raif/task/templates/task.rb.tt +52 -0
  101. data/lib/generators/raif/views_generator.rb +22 -0
  102. data/lib/raif/configuration.rb +82 -0
  103. data/lib/raif/default_llms.rb +37 -0
  104. data/lib/raif/engine.rb +86 -0
  105. data/lib/raif/errors/action_not_authorized_error.rb +8 -0
  106. data/lib/raif/errors/anthropic/api_error.rb +10 -0
  107. data/lib/raif/errors/invalid_config_error.rb +8 -0
  108. data/lib/raif/errors/invalid_conversation_type_error.rb +8 -0
  109. data/lib/raif/errors/invalid_user_tool_type_error.rb +8 -0
  110. data/lib/raif/errors/open_ai/api_error.rb +10 -0
  111. data/lib/raif/errors/open_ai/json_schema_error.rb +10 -0
  112. data/lib/raif/errors.rb +9 -0
  113. data/lib/raif/languages.rb +33 -0
  114. data/lib/raif/rspec.rb +7 -0
  115. data/lib/raif/utils/html_to_markdown_converter.rb +7 -0
  116. data/lib/raif/utils/readable_content_extractor.rb +61 -0
  117. data/lib/raif/utils.rb +6 -0
  118. data/lib/raif/version.rb +5 -0
  119. data/lib/raif.rb +65 -0
  120. data/lib/tasks/raif_tasks.rake +6 -0
  121. metadata +294 -0
data/README.md ADDED
@@ -0,0 +1,678 @@
1
+ # Raif
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/raif.svg)](https://badge.fury.io/rb/raif)
4
+ [![Build Status](https://github.com/cultivatelabs/raif/actions/workflows/ci.yml/badge.svg)](https://github.com/cultivate-labs/raif/actions/workflows/ci.yml)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+ [![Documentation](https://img.shields.io/badge/docs-YARD-blue.svg)](https://cultivatelabs.github.io/raif/)
7
+
8
+
9
+ Raif (Ruby AI Framework) is a Rails engine that helps you add AI-powered features to your Rails apps, such as [tasks](#tasks), [conversations](#conversations), and [agents](#agents). It supports for multiple LLM providers including [OpenAI](#openai), [Anthropic Claude](#anthropic-claude), and [AWS Bedrock](#aws-bedrock).
10
+
11
+ Raif is built by [Cultivate Labs](https://www.cultivatelabs.com) and is used to power [ARC](https://www.arcanalysis.ai), an AI-powered research & analysis platform.
12
+
13
+ ## Table of Contents
14
+ - [Setup](#setup)
15
+ - [OpenAI](#openai)
16
+ - [Anthropic Claude](#anthropic-claude)
17
+ - [AWS Bedrock (Claude)](#aws-bedrock-claude)
18
+ - [Chatting with the LLM](#chatting-with-the-llm)
19
+ - [Key Raif Concepts](#key-raif-concepts)
20
+ - [Tasks](#tasks)
21
+ - [Conversations](#conversations)
22
+ - [Conversation Types](#conversation-types)
23
+ - [Agents](#agents)
24
+ - [Model Tools](#model-tools)
25
+ - [Web Admin](#web-admin)
26
+ - [Customization](#customization)
27
+ - [Controllers](#controllers)
28
+ - [Models](#models)
29
+ - [Views](#views)
30
+ - [System Prompts](#system-prompts)
31
+ - [Testing](#testing)
32
+ - [Demo App](#demo-app)
33
+ - [License](#license)
34
+
35
+ # Setup
36
+
37
+ Add this line to your application's Gemfile:
38
+
39
+ ```ruby
40
+ gem "raif"
41
+ ```
42
+
43
+ And then execute:
44
+ ```bash
45
+ bundle install
46
+ ```
47
+
48
+ Run the install generator:
49
+ ```bash
50
+ rails generate raif:install
51
+ ```
52
+
53
+ This will:
54
+ - Create a configuration file at `config/initializers/raif.rb`
55
+ - Copy Raif's database migrations to your application
56
+ - Mount Raif's engine at `/raif` in your application's `config/routes.rb` file
57
+
58
+ Run the migrations. Raif is compatible with both PostgreSQL and MySQL databases.
59
+ ```bash
60
+ rails db:migrate
61
+ ```
62
+
63
+ If you plan to use the [conversations](#conversations) feature or Raif's [web admin](#web-admin), configure authentication and authorization for Raif's controllers in `config/initializers/raif.rb`:
64
+
65
+ ```ruby
66
+ Raif.configure do |config|
67
+ # Configure who can access non-admin controllers
68
+ # For example, to allow all logged in users:
69
+ config.authorize_controller_action = ->{ current_user.present? }
70
+
71
+ # Configure who can access admin controllers
72
+ # For example, to allow users with admin privileges:
73
+ config.authorize_admin_controller_action = ->{ current_user&.admin? }
74
+ end
75
+ ```
76
+
77
+ Configure your LLM providers. You'll need at least one of:
78
+
79
+ ## OpenAI
80
+ ```ruby
81
+ Raif.configure do |config|
82
+ config.open_ai_models_enabled = true
83
+ config.open_ai_api_key = ENV["OPENAI_API_KEY"]
84
+ config.default_llm_model_key = "open_ai_gpt_4o"
85
+ end
86
+ ```
87
+
88
+ Currently supported OpenAI models:
89
+ - `open_ai_gpt_4o_mini`
90
+ - `open_ai_gpt_4o`
91
+ - `open_ai_gpt_3_5_turbo`
92
+
93
+ ## Anthropic Claude
94
+ ```ruby
95
+ Raif.configure do |config|
96
+ config.anthropic_models_enabled = true
97
+ config.anthropic_api_key = ENV["ANTHROPIC_API_KEY"]
98
+ config.default_llm_model_key = "anthropic_claude_3_5_sonnet"
99
+ end
100
+ ```
101
+
102
+ Currently supported Anthropic models:
103
+ - `anthropic_claude_3_7_sonnet`
104
+ - `anthropic_claude_3_5_sonnet`
105
+ - `anthropic_claude_3_5_haiku`
106
+ - `anthropic_claude_3_opus`
107
+
108
+ ## AWS Bedrock (Claude)
109
+ ```ruby
110
+ Raif.configure do |config|
111
+ config.anthropic_bedrock_models_enabled = true
112
+ config.aws_bedrock_region = "us-east-1"
113
+ config.default_llm_model_key = "bedrock_claude_3_5_sonnet"
114
+ end
115
+ ```
116
+
117
+ Currently supported Bedrock models:
118
+ - `bedrock_claude_3_5_sonnet`
119
+ - `bedrock_claude_3_7_sonnet`
120
+ - `bedrock_claude_3_5_haiku`
121
+ - `bedrock_claude_3_opus`
122
+
123
+ Note: Raif utilizes the [AWS Bedrock gem](https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/BedrockRuntime/Client.html) and AWS credentials should be configured via the AWS SDK (environment variables, IAM role, etc.)
124
+
125
+ # Chatting with the LLM
126
+
127
+ When using Raif, it's often useful to use one of the [higher level abstractions](#key-raif-concepts) in your application. But when needed, you can utilize `Raif::Llm` to chat with the model directly. All calls to the LLM will create and return a `Raif::ModelCompletion` record, providing you a log of all interactions with the LLM which can be viewed in the [web admin](#web-admin).
128
+
129
+ Call `Raif::Llm#chat` with either a `message` string or `messages` array.:
130
+ ```ruby
131
+ llm = Raif.llm(:open_ai_gpt_4o) # will return a Raif::Llm instance
132
+ model_completion = llm.chat(message: "Hello")
133
+ puts model_completion.raw_response
134
+ # => "Hello! How can I assist you today?"
135
+ ```
136
+
137
+ The `Raif::ModelCompletion` class will handle parsing the response for you, should you ask for a different response format (which can be one of `:html`, `:text`, or `:json`). You can also provide a `system_prompt` to the `chat` method:
138
+ ```ruby
139
+ llm = Raif.llm(:open_ai_gpt_4o)
140
+ messages = [
141
+ { role: "user", content: "Hello" },
142
+ { role: "assistant", content: "Hello! How can I assist you today?" },
143
+ { role: "user", content: "Can you you tell me a joke?" },
144
+ ]
145
+
146
+ system_prompt = "You are a helpful assistant who specializes in telling jokes. Your response should be a properly formatted JSON object containing a single `joke` key. Do not include any other text in your response outside the JSON object."
147
+
148
+ model_completion = llm.chat(messages: messages, response_format: :json, system_prompt: system_prompt)
149
+ puts model_completion.raw_response
150
+ # => `​`​`json
151
+ # => {
152
+ # => "joke": "Why don't skeletons fight each other? They don't have the guts."
153
+ # => }
154
+ # => `​`​`
155
+
156
+ puts model_completion.parsed_response # will strip backticks, parse the JSON, and give you a Ruby hash
157
+ # => {"joke" => "Why don't skeletons fight each other? They don't have the guts."}
158
+ ```
159
+
160
+ # Key Raif Concepts
161
+
162
+ ## Tasks
163
+ If you have a single-shot task that you want an LLM to do in your application, you should create a `Raif::Task` subclass (see the end of this section for an example of using the task generator), where you'll define the prompt and response format for the task and call via `Raif::Task.run`. For example, say you have a `Document` model in your app and want to have a summarization task for the LLM:
164
+
165
+ ```bash
166
+ rails generate raif:task DocumentSummarization --response-format html
167
+ ```
168
+
169
+ This will create a new task in `app/models/raif/tasks/document_summarization.rb`:
170
+
171
+ ```ruby
172
+ class Raif::Tasks::DocumentSummarization < Raif::ApplicationTask
173
+ llm_response_format :html # options are :html, :text, :json
174
+
175
+ # Any attr_accessor you define can be included as an argument when calling `run`.
176
+ # E.g. Raif::Tasks::DocumentSummarization.run(document: document, creator: user)
177
+ attr_accessor :document
178
+
179
+ def build_system_prompt
180
+ sp = "You are an assistant with expertise in summarizing detailed articles into clear and concise language."
181
+ sp += system_prompt_language_preference if requested_language_key.present?
182
+ sp
183
+ end
184
+
185
+ def build_prompt
186
+ <<~PROMPT
187
+ Consider the following information:
188
+
189
+ Title: #{document.title}
190
+ Text:
191
+ ```
192
+ #{document.content}
193
+ ```
194
+
195
+ Your task is to read the provided article and associated information, and summarize the article concisely and clearly in approximately 1 paragraph. Your summary should include all of the key points, views, and arguments of the text, and should only include facts referenced in the text directly. Do not add any inferences, speculations, or analysis of your own, and do not exaggerate or overstate facts. If you quote directly from the article, include quotation marks.
196
+
197
+ Format your response using basic HTML tags.
198
+
199
+ If the text does not appear to represent the title, please return the text "#{summarization_failure_text}" and nothing else.
200
+ PROMPT
201
+ end
202
+
203
+ end
204
+ ```
205
+
206
+ And then run the task (typically via a background job):
207
+ ```
208
+ document = Document.first # assumes your app defines a Document model
209
+ user = User.first # assumes your app defines a User model
210
+ task = Raif::Tasks::DocumentSummarization.run(document: document, creator: user)
211
+ summary = task.parsed_response
212
+ ```
213
+
214
+ ### JSON Response Format Tasks
215
+
216
+ If you want to use a JSON response format for your task, you can do so by setting the `llm_response_format` to `:json` in your task subclass. If you're using OpenAI, this will set the response to use [JSON mode](https://platform.openai.com/docs/guides/structured-outputs?api-mode=chat#json-mode). You can also define a JSON schema, which will then trigger utilization of OpenAI's [structured outputs](https://platform.openai.com/docs/guides/structured-outputs?api-mode=chat#structured-outputs) feature. If you're using Claude, it will create a tool for Claude to use to generate a JSON response.
217
+
218
+ ```bash
219
+ rails generate raif:task WebSearchQueryGeneration --response-format json
220
+ ```
221
+
222
+ This will create a new task in `app/models/raif/tasks/web_search_query_generation.rb`:
223
+
224
+ ```ruby
225
+ module Raif
226
+ module Tasks
227
+ class WebSearchQueryGeneration < Raif::ApplicationTask
228
+ llm_response_format :json
229
+
230
+ attr_accessor :topic
231
+
232
+ def self.json_response_schema
233
+ {
234
+ type: "object",
235
+ additionalProperties: false,
236
+ required: ["queries"],
237
+ properties: {
238
+ queries: {
239
+ type: "array",
240
+ items: {
241
+ type: "string"
242
+ }
243
+ }
244
+ }
245
+ }
246
+ end
247
+
248
+ def build_prompt
249
+ <<~PROMPT
250
+ Generate a list of 3 search queries that I can use to find information about the following topic:
251
+ #{topic}
252
+
253
+ Format your response as JSON.
254
+ PROMPT
255
+ end
256
+ end
257
+ end
258
+ end
259
+
260
+ ```
261
+
262
+ ### Task Language Preference
263
+ You can also pass in a `requested_language_key` to the `run` method. When this is provided, Raif will add a line to the system prompt requesting that the LLM respond in the specified language:
264
+ ```
265
+ task = Raif::Tasks::DocumentSummarization.run(document: document, creator: user, requested_language_key: "es")
266
+ ```
267
+
268
+ Would produce a system prompt that looks like this:
269
+ ```
270
+ You are an assistant with expertise in summarizing detailed articles into clear and concise language.
271
+ You're collaborating with teammate who speaks Spanish. Please respond in Spanish.
272
+ ```
273
+
274
+ ## Conversations
275
+
276
+ Raif provides `Raif::Conversation` and `Raif::ConversationEntry` models that you can use to provide an LLM-powered chat interface. It also provides controllers and views for the conversation interface.
277
+
278
+ This feature utilizes Turbo Streams, Stimulus controllers, and ActiveJob, so your application must have those set up first.
279
+
280
+ To use it in your application, first set up the css and javascript in your application. In the `<head>` section of your layout file:
281
+ ```erb
282
+ <%= stylesheet_link_tag "raif" %>
283
+ ```
284
+
285
+ In an app using import maps, add the following to your `application.js` file:
286
+ ```js
287
+ import "raif"
288
+ ```
289
+
290
+ In a controller serving the conversation view:
291
+ ```ruby
292
+ class ExampleConversationController < ApplicationController
293
+ def show
294
+ @conversation = Raif::Conversation.where(creator: current_user).order(created_at: :desc).first
295
+
296
+ if @conversation.nil?
297
+ @conversation = Raif::Conversation.new(creator: current_user)
298
+ @conversation.save!
299
+ end
300
+ end
301
+ end
302
+ ```
303
+
304
+ And then in the view where you'd like to display the conversation interface:
305
+ ```erb
306
+ <%= raif_conversation(@conversation) %>
307
+ ```
308
+
309
+ If your app already includes Bootstrap styles, this will render a conversation interface that looks something like:
310
+
311
+ ![Conversation Interface](./screenshots/conversation-interface.png)
312
+
313
+ If your app does not include Bootstrap, you can [override the views](#views) to update styles.
314
+
315
+ ### Conversation Types
316
+
317
+ If your application has a specific type of conversation that you use frequently, you can create a custom conversation type by running the generator. For example, say you are implementing a customer support chatbot in your application and want to have a custom conversation type for doing this with the LLM:
318
+ ```bash
319
+ rails generate raif:conversation CustomerSupport
320
+ ```
321
+
322
+ This will create a new conversation type in `app/models/raif/conversations/customer_support.rb`.
323
+
324
+ You can then customize the system prompt, initial message, and available [model tools](#model-tools) for that conversation type:
325
+
326
+ ```ruby
327
+ class Raif::Conversations::CustomerSupport < Raif::Conversation
328
+ before_create -> {
329
+ self.available_model_tools = [
330
+ "Raif::ModelTools::SearchKnowledgeBase",
331
+ "Raif::ModelTools::FileSupportTicket"
332
+ ]
333
+ }
334
+
335
+ def system_prompt_intro
336
+ <<~PROMPT
337
+ You are a helpful assistant who specializes in customer support. You're working with a customer who is experiencing an issue with your product.
338
+ PROMPT
339
+ end
340
+
341
+ def initial_chat_message
342
+ I18n.t("#{self.class.name.underscore.gsub("/", ".")}.initial_chat_message")
343
+ end
344
+ end
345
+ ```
346
+
347
+
348
+ ## Agents
349
+
350
+ Raif also provides `Raif::Agents::ReActAgent`, which implements a ReAct-style agent loop using [tool calls](#model-tools):
351
+
352
+ ```ruby
353
+ # Create a new agent
354
+ agent = Raif::Agents::ReActAgent.new(
355
+ task: "Research the history of the Eiffel Tower",
356
+ available_model_tools: [Raif::ModelTools::WikipediaSearch, Raif::ModelTools::FetchUrl],
357
+ creator: current_user
358
+ )
359
+
360
+ # Run the agent and get the final answer
361
+ final_answer = agent.run!
362
+
363
+ # Or run the agent and monitor its progress
364
+ agent.run! do |conversation_history_entry|
365
+ Turbo::StreamsChannel.broadcast_append_to(
366
+ :my_agent_channel,
367
+ target: "agent-progress",
368
+ partial: "my_partial_displaying_agent_progress",
369
+ locals: { agent: agent, conversation_history_entry: conversation_history_entry }
370
+ )
371
+ end
372
+ ```
373
+
374
+ On each step of the agent loop, an entry will be added to the `Raif::Agent#conversation_history` and, if you pass a block to the `run!` method, the block will be called with the `conversation_history_entry` as an argument. You can use this to monitor and display the agent's progress in real-time.
375
+
376
+ The conversation_history_entry will be a hash with "role" and "content" keys:
377
+ ```ruby
378
+ {
379
+ "role" => "assistant",
380
+ "content" => "a message here"
381
+ }
382
+ ```
383
+
384
+ ### Creating Custom Agents
385
+
386
+ You can create custom agents using the generator:
387
+ ```bash
388
+ rails generate raif:agent WikipediaResearchAgent
389
+ ```
390
+
391
+ This will create a new agent in `app/models/raif/agents/wikipedia_research_agent.rb`:
392
+
393
+ ```ruby
394
+ module Raif
395
+ module Agents
396
+ class WikipediaResearchAgent < Raif::Agent
397
+ # If you want to always include a certain set of model tools with this agent type,
398
+ # uncomment this callback to populate the available_model_tools attribute with your desired model tools.
399
+ # before_create -> {
400
+ # self.available_model_tools ||= [
401
+ # Raif::ModelTools::WikipediaSearchTool,
402
+ # Raif::ModelTools::FetchUrlTool
403
+ # ]
404
+ # }
405
+
406
+ # Enter your agent's system prompt here. Alternatively, you can change your agent's superclass
407
+ # to an existing agent types (like Raif::Agents::ReActAgent) to utilize an existing system prompt.
408
+ def build_system_prompt
409
+ # TODO: Implement your system prompt here
410
+ end
411
+
412
+ # Each iteration of the agent loop will generate a new Raif::ModelCompletion record and
413
+ # then call this method with it as an argument.
414
+ def process_iteration_model_completion(model_completion)
415
+ # TODO: Implement your iteration processing here
416
+ end
417
+ end
418
+ end
419
+ end
420
+
421
+ ```
422
+
423
+ ## Model Tools
424
+
425
+ Raif provides a `Raif::ModelTool` base class that you can use to create custom tools for your agents and conversations. [`Raif::ModelTools::WikipediaSearch`](https://github.com/CultivateLabs/raif/blob/main/app/models/raif/model_tools/wikipedia_search.rb) and [`Raif::ModelTools::FetchUrl`](https://github.com/CultivateLabs/raif/blob/main/app/models/raif/model_tools/fetch_url.rb) tools are included as examples.
426
+
427
+ You can create your own model tools to provide to the LLM using the generator:
428
+ ```bash
429
+ rails generate raif:model_tool GoogleSearch
430
+ ```
431
+
432
+ This will create a new model tool in `app/models/raif/model_tools/google_search.rb`:
433
+
434
+ ```ruby
435
+ class Raif::ModelTools::GoogleSearch < Raif::ModelTool
436
+ # For example tool implementations, see:
437
+ # Wikipedia Search Tool: https://github.com/CultivateLabs/raif/blob/main/app/models/raif/model_tools/wikipedia_search_tool.rb
438
+ # Fetch URL Tool: https://github.com/CultivateLabs/raif/blob/main/app/models/raif/model_tools/fetch_url_tool.rb
439
+
440
+ # An example of how the LLM should invoke your tool. This should return a hash with name and arguments keys.
441
+ # `to_json` will be called on it and provided to the LLM as an example of how to invoke your tool.
442
+ def self.example_model_invocation
443
+ {
444
+ "name": tool_name,
445
+ "arguments": { "query": "example query here" }
446
+ }
447
+ end
448
+
449
+ # Define your tool's argument schema here. It should be a valid JSON schema.
450
+ # When the model invokes your tool, the arguments it provides will be validated
451
+ # against this schema using JSON::Validator from the json-schema gem.
452
+ def self.tool_arguments_schema
453
+ # For example:
454
+ # {
455
+ # type: "object",
456
+ # additionalProperties: false,
457
+ # required: ["query"],
458
+ # properties: {
459
+ # query: {
460
+ # type: "string",
461
+ # description: "The query to search for"
462
+ # }
463
+ # }
464
+ # }
465
+ # Would expect the model to invoke your tool with an arguments JSON object like:
466
+ # { "query" : "some query here" }
467
+ end
468
+
469
+ def self.tool_description
470
+ "Description of your tool that will be provided to the LLM so it knows when to invoke it"
471
+ end
472
+
473
+ # When your tool is invoked by the LLM in a Raif::Agent loop,
474
+ # the results of the tool invocation are provided back to the LLM as an observation.
475
+ # This method should return whatever you want provided to the LLM.
476
+ # For example, if you were implementing a GoogleSearch tool, this might return a JSON
477
+ # object containing search results for the query.
478
+ def self.observation_for_invocation(tool_invocation)
479
+ return "No results found" unless tool_invocation.result.present?
480
+
481
+ JSON.pretty_generate(tool_invocation.result)
482
+ end
483
+
484
+ # When the LLM invokes your tool, this method will be called with a `Raif::ModelToolInvocation` record as an argument.
485
+ # It should handle the actual execution of the tool.
486
+ # For example, if you are implementing a GoogleSearch tool, this method should run the actual search
487
+ # and store the results in the tool_invocation's result JSON column.
488
+ def self.process_invocation(tool_invocation)
489
+ # Extract arguments from tool_invocation.tool_arguments
490
+ # query = tool_invocation.tool_arguments["query"]
491
+ #
492
+ # Process the invocation and perform the desired action
493
+ # ...
494
+ #
495
+ # Store the results in the tool_invocation
496
+ # tool_invocation.update!(
497
+ # result: {
498
+ # # Your result data structure
499
+ # }
500
+ # )
501
+ #
502
+ # Return the result
503
+ # tool_invocation.result
504
+ end
505
+
506
+ end
507
+ ```
508
+
509
+ # Web Admin
510
+
511
+ Raif includes a web admin interface for viewing all interactions with the LLM. Assuming you have the engine mounted at `/raif`, you can access the admin interface at `/raif/admin`.
512
+
513
+ The admin interface contains sections for:
514
+ - Model Completions
515
+ - Tasks
516
+ - Conversations
517
+ - Agents
518
+ - Model Tool Invocations
519
+
520
+
521
+ ### Model Completions
522
+ ![Model Completions Index](./screenshots/admin-model-completions-index.png)
523
+ ![Model Completion Detail](./screenshots/admin-model-completion-show.png)
524
+
525
+ ### Tasks
526
+ ![Tasks Index](./screenshots/admin-tasks-index.png)
527
+
528
+ ### Conversations
529
+ ![Conversations Index](./screenshots/admin-conversations-index.png)
530
+ ![Conversation Detail](./screenshots/admin-conversation-show.png)
531
+
532
+ ### Agents
533
+ ![Agents Index](./screenshots/admin-agents-index.png)
534
+ ![Agents Detail](./screenshots/admin-agents-show.png)
535
+
536
+ ### Model Tool Invocations
537
+ ![Model Tool Invocations Index](./screenshots/admin-model-tool-invocations-index.png)
538
+ ![Model Tool Invocation Detail](./screenshots/admin-model-tool-invocation-show.png)
539
+
540
+ # Customization
541
+
542
+ ## Controllers
543
+
544
+ You can override Raif's controllers by creating your own that inherit from Raif's base controllers:
545
+
546
+ ```ruby
547
+ class ConversationsController < Raif::ConversationsController
548
+ # Your customizations here
549
+ end
550
+
551
+ class ConversationEntriesController < Raif::ConversationEntriesController
552
+ # Your customizations here
553
+ end
554
+ ```
555
+
556
+ Then update the configuration:
557
+ ```ruby
558
+ Raif.configure do |config|
559
+ config.conversations_controller = "ConversationsController"
560
+ config.conversation_entries_controller = "ConversationEntriesController"
561
+ end
562
+ ```
563
+
564
+ ## Models
565
+
566
+ By default, Raif models inherit from `ApplicationRecord`. You can change this:
567
+
568
+ ```ruby
569
+ Raif.configure do |config|
570
+ config.model_superclass = "CustomRecord"
571
+ end
572
+ ```
573
+
574
+ ## Views
575
+
576
+ You can customize Raif's views by copying them to your application and modifying them. To copy the conversation-related views, run:
577
+
578
+ ```bash
579
+ rails generate raif:views
580
+ ```
581
+
582
+ This will copy all conversation and conversation entry views to your application in:
583
+ - `app/views/raif/conversations/`
584
+ - `app/views/raif/conversation_entries/`
585
+
586
+ These views will automatically override Raif's default views. You can customize them to match your application's look and feel while maintaining the same functionality.
587
+
588
+ ## System Prompts
589
+
590
+ If you don't want to override the system prompt entirely in your task/conversation subclasses, you can customize the intro portion of the system prompts for conversations and tasks:
591
+
592
+ ```ruby
593
+ Raif.configure do |config|
594
+ config.conversation_system_prompt_intro = "You are a helpful assistant who specializes in customer support."
595
+ config.task_system_prompt_intro = "You are a helpful assistant who specializes in data analysis."
596
+ end
597
+ ```
598
+
599
+ # Testing
600
+
601
+ Raif includes RSpec helpers and FactoryBot factories to help with testing in your application.
602
+
603
+ To use the helpers, add the following to your `rails_helper.rb`:
604
+
605
+ ```ruby
606
+ require "raif/rspec"
607
+
608
+ RSpec.configure do |config|
609
+ config.include Raif::RspecHelpers
610
+ end
611
+ ```
612
+
613
+ You can then use the helpers to stub LLM calls:
614
+
615
+ ```ruby
616
+ it "stubs a document summarization task" do
617
+ # the messages argument is the array of messages sent to the LLM. It will look something like:
618
+ # [{"role" => "user", "content" => "The prompt from the Raif::Tasks::DocumentSummarization task" }]
619
+ # The model_completion argument is the Raif::ModelCompletion record that was created for this task.
620
+ stub_raif_task(Raif::Tasks::DocumentSummarization) do |messages, model_completion|
621
+ "Stub out the response from the LLM"
622
+ end
623
+
624
+ user = FactoryBot.create(:user) # assumes you have a User model & factory
625
+ document = FactoryBot.create(:document) # assumes you have a Document model & factory
626
+ task = Raif::Tasks::DocumentSummarization.run(document: document, creator: user)
627
+
628
+ expect(task.raw_response).to eq("Stub out the response from the LLM")
629
+ end
630
+
631
+ it "stubs a conversation" do
632
+ user = FactoryBot.create(:user) # assumes you have a User model & factory
633
+ conversation = FactoryBot.create(:raif_test_conversation, creator: user)
634
+ conversation_entry = FactoryBot.create(:raif_conversation_entry, raif_conversation: conversation, creator: user)
635
+
636
+ stub_raif_conversation(conversation) do |messages, model_completion|
637
+ "Hello"
638
+ end
639
+
640
+ conversation_entry.process_entry!
641
+ expect(conversation_entry.reload).to be_completed
642
+ expect(conversation_entry.model_response_message).to eq("Hello")
643
+ end
644
+
645
+ it "stubs an agent" do
646
+ i = 0
647
+ stub_raif_agent(agent) do |messages, model_completion|
648
+ i += 1
649
+ if i == 1
650
+ "<thought>I need to search.</thought>\n<action>{\"tool\": \"wikipedia_search\", \"arguments\": {\"query\": \"capital of France\"}}</action>"
651
+ else
652
+ "<thought>Now I know.</thought>\n<answer>Paris</answer>"
653
+ end
654
+ end
655
+ end
656
+ ```
657
+
658
+ Raif also provides FactoryBot factories for its models. You can use them to create Raif models for testing. If you're using `factory_bot_rails`, they will be added automatically to `config.factory_bot.definition_file_paths`. The available factories can be found [here](https://github.com/CultivateLabs/raif/tree/main/spec/factories/shared).
659
+
660
+ # Demo App
661
+
662
+ Raif includes a [demo app](https://github.com/CultivateLabs/raif_demo) that you can use to see the engine in action. Assuming you have Ruby 3.4.2 and Postgres installed, you can run the demo app with:
663
+
664
+ ```bash
665
+ git clone git@github.com:CultivateLabs/raif_demo.git
666
+ cd raif_demo
667
+ bundle install
668
+ bin/rails db:create db:prepare
669
+ OPENAI_API_KEY=your-openai-api-key-here bin/rails s
670
+ ```
671
+
672
+ You can then access the app at [http://localhost:3000](http://localhost:3000).
673
+
674
+ ![Demo App Screenshot](./screenshots/demo-app.png)
675
+
676
+ # License
677
+
678
+ The gem is available as open source under the terms of the MIT License.
data/Rakefile ADDED
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+
5
+ APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
6
+ load "rails/tasks/engine.rake"
7
+
8
+ load "rails/tasks/statistics.rake"
9
+
10
+ require "bundler/gem_tasks"
11
+
12
+ begin
13
+ require "yard"
14
+ YARD::Rake::YardocTask.new do |t|
15
+ t.files = ["lib/**/*.rb", "app/**/*.rb", "-", "README.md"]
16
+ t.options = ["--output-dir=doc"]
17
+ end
18
+ rescue LoadError
19
+ # YARD not available
20
+ end