raif 1.2.1.pre → 1.2.2

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.
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Raif
1
+ ![Raif Logo](docs/assets/images/raif-logo-400.png)
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/raif.svg)](https://badge.fury.io/rb/raif)
4
4
  [![Build Status](https://github.com/cultivatelabs/raif/actions/workflows/ci.yml/badge.svg)](https://github.com/cultivate-labs/raif/actions/workflows/ci.yml)
@@ -6,978 +6,72 @@
6
6
  [![Documentation](https://img.shields.io/badge/docs-YARD-blue.svg)](https://cultivatelabs.github.io/raif/)
7
7
 
8
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), [AWS Bedrock](#aws-bedrock), and [OpenRouter](#openrouter).
9
+ Raif (Ruby AI Framework) is a Rails engine that helps you add AI-powered features to your Rails apps, such as [tasks](https://docs.raif.ai/key_raif_concepts/tasks), [conversations](https://docs.raif.ai/key_raif_concepts/conversations), and [agents](https://docs.raif.ai/key_raif_concepts/agents). It supports for multiple LLM providers including [OpenAI](https://docs.raif.ai/getting_started/setup#openai), [Anthropic Claude](https://docs.raif.ai/getting_started/setup#anthropic), [AWS Bedrock](https://docs.raif.ai/getting_started/setup#aws-bedrock), and [OpenRouter](https://docs.raif.ai/getting_started/setup#openrouter).
10
10
 
11
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
12
 
13
13
  ## Table of Contents
14
- - [Setup](#setup)
15
- - [OpenAI](#openai)
16
- - [OpenAI Completions API](#openai-completions-api)
17
- - [OpenAI Responses API](#openai-responses-api)
18
- - [Anthropic Claude](#anthropic-claude)
19
- - [AWS Bedrock (Claude)](#aws-bedrock-claude)
20
- - [OpenRouter](#openrouter)
21
- - [Chatting with the LLM](#chatting-with-the-llm)
22
- - [Streaming Responses](#streaming-responses)
23
- - [Key Raif Concepts](#key-raif-concepts)
24
- - [Tasks](#tasks)
25
- - [Conversations](#conversations)
26
- - [Real-time Streaming Responses](#real-time-streaming-responses)
27
- - [Conversation Types](#conversation-types)
28
- - [Agents](#agents)
29
- - [Model Tools](#model-tools)
30
- - [Provider-Managed Tools](#provider-managed-tools)
31
- - [Images/Files/PDF's](#imagesfilespdfs)
32
- - [Images/Files/PDF's in Tasks](#imagesfilespdfs-in-tasks)
33
- - [Embedding Models](#embedding-models)
34
- - [Web Admin](#web-admin)
35
- - [Customization](#customization)
36
- - [Controllers](#controllers)
37
- - [Models](#models)
38
- - [Views](#views)
39
- - [System Prompts](#system-prompts)
40
- - [Adding LLM Models](#adding-llm-models)
41
- - [Testing](#testing)
42
- - [Demo App](#demo-app)
43
- - [Contributing](#contributing)
44
- - [License](#license)
14
+ - [Setup](https://docs.raif.ai/getting_started/setup)
15
+ - [Chatting with the LLM](https://docs.raif.ai/getting_started/chatting_with_the_llm)
16
+ - [Tasks](https://docs.raif.ai/key_raif_concepts/tasks)
17
+ - [Conversations](https://docs.raif.ai/key_raif_concepts/conversations)
18
+ - [Agents](https://docs.raif.ai/key_raif_concepts/agents)
19
+ - [Model Tools](https://docs.raif.ai/key_raif_concepts/model_tools)
20
+ - [Images/Files/PDF's](https://docs.raif.ai/learn_more/images_files_pdfs)
21
+ - [Embedding Models](https://docs.raif.ai/learn_more/embedding_models)
22
+ - [Web Admin](https://docs.raif.ai/learn_more/web_admin)
23
+ - [Customization](https://docs.raif.ai/learn_more/customization)
24
+ - [Testing](https://docs.raif.ai/learn_more/testing)
25
+ - [Demo App](https://docs.raif.ai/learn_more/demo_app)
26
+ - [Contributing](https://docs.raif.ai/contributing)
27
+ - [License](https://docs.raif.ai/license)
45
28
 
46
29
  # Setup
47
30
 
48
- Add this line to your application's Gemfile:
49
-
50
- ```ruby
51
- gem "raif"
52
- ```
53
-
54
- And then execute:
55
- ```bash
56
- bundle install
57
- ```
58
-
59
- Run the install generator:
60
- ```bash
61
- rails generate raif:install
62
- ```
63
-
64
- This will:
65
- - Create a configuration file at `config/initializers/raif.rb`
66
- - Copy Raif's database migrations to your application
67
- - Mount Raif's engine at `/raif` in your application's `config/routes.rb` file
68
-
69
- You must configure at least one API key for your LLM provider ([OpenAI](#openai), [Anthropic Claude](#anthropic-claude), [AWS Bedrock](#aws-bedrock-claude), [OpenRouter](#openrouter)). By default, the initializer will load them from environment variables (e.g. `ENV["OPENAI_API_KEY"]`, `ENV["ANTHROPIC_API_KEY"]`, `ENV["OPENROUTER_API_KEY"]`). Alternatively, you can set them directly in `config/initializers/raif.rb`.
70
-
71
- Run the migrations. Raif is compatible with both PostgreSQL and MySQL databases.
72
- ```bash
73
- rails db:migrate
74
- ```
75
-
76
- 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`:
77
-
78
- ```ruby
79
- Raif.configure do |config|
80
- # Configure who can access non-admin controllers
81
- # For example, to allow all logged in users:
82
- config.authorize_controller_action = ->{ current_user.present? }
83
-
84
- # Configure who can access admin controllers
85
- # For example, to allow users with admin privileges:
86
- config.authorize_admin_controller_action = ->{ current_user&.admin? }
87
- end
88
- ```
89
-
90
- Configure your LLM providers. You'll need at least one of:
91
-
92
- ## OpenAI
93
-
94
- Raif supports both OpenAI's [Completions API](https://platform.openai.com/docs/api-reference/chat) and the newer [Responses API](https://platform.openai.com/docs/api-reference/responses), which provides access to provider-managed tools like web search, code execution, and image generation.
95
-
96
- ### OpenAI Completions API
97
- ```ruby
98
- Raif.configure do |config|
99
- config.open_ai_models_enabled = true
100
- config.open_ai_api_key = ENV["OPENAI_API_KEY"]
101
- config.default_llm_model_key = "open_ai_gpt_4o"
102
- end
103
- ```
104
-
105
- Currently supported OpenAI Completions API models:
106
- - `open_ai_gpt_4o_mini`
107
- - `open_ai_gpt_4o`
108
- - `open_ai_gpt_3_5_turbo`
109
- - `open_ai_gpt_4_1`
110
- - `open_ai_gpt_4_1_mini`
111
- - `open_ai_gpt_4_1_nano`
112
- - `open_ai_o1`
113
- - `open_ai_o1_mini`
114
- - `open_ai_o3`
115
- - `open_ai_o3_mini`
116
- - `open_ai_o4_mini`
117
-
118
- ### OpenAI Responses API
119
- ```ruby
120
- Raif.configure do |config|
121
- config.open_ai_models_enabled = true
122
- config.open_ai_api_key = ENV["OPENAI_API_KEY"]
123
- config.default_llm_model_key = "open_ai_responses_gpt_4o"
124
- end
125
- ```
126
-
127
- Currently supported OpenAI Responses API models:
128
- - `open_ai_responses_gpt_4o_mini`
129
- - `open_ai_responses_gpt_4o`
130
- - `open_ai_responses_gpt_3_5_turbo`
131
- - `open_ai_responses_gpt_4_1`
132
- - `open_ai_responses_gpt_4_1_mini`
133
- - `open_ai_responses_gpt_4_1_nano`
134
- - `open_ai_responses_o1`
135
- - `open_ai_responses_o1_mini`
136
- - `open_ai_responses_o1_pro`
137
- - `open_ai_responses_o3`
138
- - `open_ai_responses_o3_mini`
139
- - `open_ai_responses_o3_pro`
140
- - `open_ai_responses_o4_mini`
141
-
142
- The Responses API provides access to [provider-managed tools](#provider-managed-tools), including web search, code execution, and image generation.
143
-
144
- ## Anthropic Claude
145
- ```ruby
146
- Raif.configure do |config|
147
- config.anthropic_models_enabled = true
148
- config.anthropic_api_key = ENV["ANTHROPIC_API_KEY"]
149
- config.default_llm_model_key = "anthropic_claude_3_5_sonnet"
150
- end
151
- ```
152
-
153
- Currently supported Anthropic models:
154
- - `anthropic_claude_3_7_sonnet`
155
- - `anthropic_claude_3_5_sonnet`
156
- - `anthropic_claude_3_5_haiku`
157
- - `anthropic_claude_3_opus`
158
-
159
- The Anthropic adapter provides access to [provider-managed tools](#provider-managed-tools) for web search and code execution.
160
-
161
- ## AWS Bedrock (Claude)
162
- ```ruby
163
- Raif.configure do |config|
164
- config.bedrock_models_enabled = true
165
- config.aws_bedrock_region = "us-east-1"
166
- config.default_llm_model_key = "bedrock_claude_3_5_sonnet"
167
- end
168
- ```
169
-
170
- Currently supported Bedrock models:
171
- - `bedrock_claude_3_5_sonnet`
172
- - `bedrock_claude_3_7_sonnet`
173
- - `bedrock_claude_3_5_haiku`
174
- - `bedrock_claude_3_opus`
175
- - `bedrock_amazon_nova_micro`
176
- - `bedrock_amazon_nova_lite`
177
- - `bedrock_amazon_nova_pro`
178
-
179
- 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.)
180
-
181
- ## OpenRouter
182
- [OpenRouter](https://openrouter.ai/) is a unified API that provides access to multiple AI models from different providers including Anthropic, Meta, Google, and more.
183
-
184
- ```ruby
185
- Raif.configure do |config|
186
- config.open_router_models_enabled = true
187
- config.open_router_api_key = ENV["OPENROUTER_API_KEY"]
188
- config.open_router_app_name = "Your App Name" # Optional
189
- config.open_router_site_url = "https://yourdomain.com" # Optional
190
- config.default_llm_model_key = "open_router_claude_3_7_sonnet"
191
- end
192
- ```
193
-
194
- Currently included OpenRouter models:
195
- - `open_router_claude_3_7_sonnet`
196
- - `open_router_llama_3_3_70b_instruct`
197
- - `open_router_llama_3_1_8b_instruct`
198
- - `open_router_llama_4_maverick`
199
- - `open_router_llama_4_scout`
200
- - `open_router_gemini_2_0_flash`
201
- - `open_router_deepseek_chat_v3`
202
-
203
- See [Adding LLM Models](#adding-llm-models) for more information on adding new OpenRouter models.
31
+ View the [setup guide](https://docs.raif.ai/getting_started/setup).
204
32
 
205
33
  # Chatting with the LLM
206
34
 
207
- 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).
208
-
209
- Call `Raif::Llm#chat` with either a `message` string or `messages` array.:
210
- ```ruby
211
- llm = Raif.llm(:open_ai_gpt_4o) # will return a Raif::Llm instance
212
- model_completion = llm.chat(message: "Hello")
213
- puts model_completion.raw_response
214
- # => "Hello! How can I assist you today?"
215
- ```
216
-
217
- 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:
218
- ```ruby
219
- llm = Raif.llm(:open_ai_gpt_4o)
220
- messages = [
221
- { role: "user", content: "Hello" },
222
- { role: "assistant", content: "Hello! How can I assist you today?" },
223
- { role: "user", content: "Can you you tell me a joke?" },
224
- ]
225
-
226
- 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."
227
-
228
- model_completion = llm.chat(messages: messages, response_format: :json, system_prompt: system_prompt)
229
- puts model_completion.raw_response
230
- # => `​`​`json
231
- # => {
232
- # => "joke": "Why don't skeletons fight each other? They don't have the guts."
233
- # => }
234
- # => `​`​`
235
-
236
- puts model_completion.parsed_response # will strip backticks, parse the JSON, and give you a Ruby hash
237
- # => {"joke" => "Why don't skeletons fight each other? They don't have the guts."}
238
- ```
239
-
240
- ## Streaming Responses
241
-
242
- You can enable streaming for any chat call by passing a block to the `chat` method. When streaming is enabled, the block will be called with partial responses as they're received from the LLM:
243
-
244
- ```ruby
245
- llm = Raif.llm(:open_ai_gpt_4o)
246
- model_completion = llm.chat(message: "Tell me a story") do |model_completion, delta, sse_event|
247
- # This block is called multiple times as the response streams in.
248
- # You could broadcast these updates via Turbo Streams, WebSockets, etc.
249
- Turbo::StreamsChannel.broadcast_replace_to(
250
- :my_channel,
251
- target: "chat-response",
252
- partial: "my_partial_displaying_chat_response",
253
- locals: { model_completion: model_completion, delta: delta, sse_event: sse_event }
254
- )
255
- end
256
-
257
- # The final complete response is available in the model_completion
258
- puts model_completion.raw_response
259
- ```
260
-
261
- You can configure the streaming update frequency by adjusting the chunk size threshold in your Raif configuration:
262
-
263
- ```ruby
264
- Raif.configure do |config|
265
- # Control how often the model completion is updated & the block is called when streaming.
266
- # Lower values = more frequent updates but more database writes.
267
- # Higher values = less frequent updates but fewer database writes.
268
- config.streaming_update_chunk_size_threshold = 50 # default is 25
269
- end
270
- ```
35
+ View the [chatting with the LLM docs](https://docs.raif.ai/getting_started/chatting_with_the_llm).
271
36
 
272
37
  # Key Raif Concepts
273
38
 
274
- ## Tasks
275
- If you have a single-shot task that you want an LLM to do in your application, you should create a `Raif::Task` subclass, 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:
276
-
277
- ```bash
278
- rails generate raif:task DocumentSummarization --response-format html
279
- ```
280
-
281
- This will create a new task in `app/models/raif/tasks/document_summarization.rb`:
282
-
283
- ```ruby
284
- class Raif::Tasks::DocumentSummarization < Raif::ApplicationTask
285
- llm_response_format :html # options are :html, :text, :json
286
- llm_temperature 0.8 # optional, defaults to 0.7
287
- llm_response_allowed_tags %w[p b i div strong] # optional, defaults to Rails::HTML5::SafeListSanitizer.allowed_tags
288
- llm_response_allowed_attributes %w[style] # optional, defaults to Rails::HTML5::SafeListSanitizer.allowed_attributes
289
-
290
- # Any attr_accessor you define can be included as an argument when calling `run`.
291
- # E.g. Raif::Tasks::DocumentSummarization.run(document: document, creator: user)
292
- attr_accessor :document
293
-
294
- def build_system_prompt
295
- sp = "You are an assistant with expertise in summarizing detailed articles into clear and concise language."
296
- sp += system_prompt_language_preference if requested_language_key.present?
297
- sp
298
- end
299
-
300
- def build_prompt
301
- <<~PROMPT
302
- Consider the following information:
303
-
304
- Title: #{document.title}
305
- Text:
306
- ```
307
- #{document.content}
308
- ```
309
-
310
- 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.
311
-
312
- Format your response using basic HTML tags.
313
-
314
- If the text does not appear to represent the title, please return the text "#{summarization_failure_text}" and nothing else.
315
- PROMPT
316
- end
317
-
318
- end
319
- ```
320
-
321
- And then run the task (typically via a background job):
322
- ```
323
- document = Document.first # assumes your app defines a Document model
324
- user = User.first # assumes your app defines a User model
325
- task = Raif::Tasks::DocumentSummarization.run(document: document, creator: user)
326
- summary = task.parsed_response
327
- ```
328
-
329
- ### JSON Response Format Tasks
330
-
331
- 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.
332
-
333
- ```bash
334
- rails generate raif:task WebSearchQueryGeneration --response-format json
335
- ```
336
-
337
- This will create a new task in `app/models/raif/tasks/web_search_query_generation.rb`:
338
-
339
- ```ruby
340
- module Raif
341
- module Tasks
342
- class WebSearchQueryGeneration < Raif::ApplicationTask
343
- llm_response_format :json
344
-
345
- attr_accessor :topic
346
-
347
- json_response_schema do
348
- array :queries do
349
- items type: "string"
350
- end
351
- end
352
-
353
- def build_prompt
354
- <<~PROMPT
355
- Generate a list of 3 search queries that I can use to find information about the following topic:
356
- #{topic}
357
-
358
- Format your response as JSON.
359
- PROMPT
360
- end
361
- end
362
- end
363
- end
364
-
365
- ```
366
-
367
- ### Task Language Preference
368
- 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:
369
- ```
370
- task = Raif::Tasks::DocumentSummarization.run(document: document, creator: user, requested_language_key: "es")
371
- ```
372
-
373
- Would produce a system prompt that looks like this:
374
- ```
375
- You are an assistant with expertise in summarizing detailed articles into clear and concise language.
376
- You're collaborating with teammate who speaks Spanish. Please respond in Spanish.
377
- ```
378
-
379
- The current list of valid language keys can be found [here](https://github.com/CultivateLabs/raif/blob/main/lib/raif/languages.rb).
380
-
381
- ## Conversations
382
-
383
- 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.
384
-
385
- This feature utilizes Turbo Streams, Stimulus controllers, and ActiveJob, so your application must have those set up first.
386
-
387
- To use it in your application, first set up the css and javascript in your application. In the `<head>` section of your layout file:
388
- ```erb
389
- <%= stylesheet_link_tag "raif" %>
390
- ```
391
-
392
- In an app using import maps, add the following to your `application.js` file:
393
- ```js
394
- import "raif"
395
- ```
396
-
397
- In a controller serving the conversation view:
398
- ```ruby
399
- class ExampleConversationController < ApplicationController
400
- def show
401
- @conversation = Raif::Conversation.where(creator: current_user).order(created_at: :desc).first
402
-
403
- if @conversation.nil?
404
- @conversation = Raif::Conversation.new(creator: current_user)
405
- @conversation.save!
406
- end
407
- end
408
- end
409
- ```
410
-
411
- And then in the view where you'd like to display the conversation interface:
412
- ```erb
413
- <%= raif_conversation(@conversation) %>
414
- ```
415
-
416
- If your app already includes Bootstrap styles, this will render a conversation interface that looks something like:
417
-
418
- ![Conversation Interface](./screenshots/conversation-interface.png)
419
-
420
- If your app does not include Bootstrap, you can [override the views](#views) to update styles.
421
-
422
- ### Real-time Streaming Responses
423
-
424
- Raif conversations have built-in support for streaming responses, where the LLM's response is displayed progressively as it's being generated. Each time a conversation entry is updated during the streaming response, Raif will call `broadcast_replace_to(conversation)` (where `conversation` is the `Raif::Conversation` associated with the conversation entry). When using the `raif_conversation` view helper, it will automatically set up the subscription for you.
425
-
426
- ### Conversation Types
427
-
428
- 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:
429
- ```bash
430
- rails generate raif:conversation CustomerSupport
431
- ```
432
-
433
- This will create a new conversation type in `app/models/raif/conversations/customer_support.rb`.
434
-
435
- You can then customize the system prompt, initial message, and available [model tools](#model-tools) for that conversation type:
436
-
437
- ```ruby
438
- class Raif::Conversations::CustomerSupport < Raif::Conversation
439
- before_create -> {
440
- self.available_model_tools = [
441
- "Raif::ModelTools::SearchKnowledgeBase",
442
- "Raif::ModelTools::FileSupportTicket"
443
- ]
444
- }
445
-
446
- def system_prompt_intro
447
- <<~PROMPT
448
- You are a helpful assistant who specializes in customer support. You're working with a customer who is experiencing an issue with your product.
449
- PROMPT
450
- end
451
-
452
- def initial_chat_message
453
- I18n.t("#{self.class.name.underscore.gsub("/", ".")}.initial_chat_message")
454
- end
455
- end
456
- ```
457
-
458
-
459
- ## Agents
460
-
461
- Raif also provides `Raif::Agents::ReActAgent`, which implements a ReAct-style agent loop using [tool calls](#model-tools):
462
-
463
- ```ruby
464
- # Create a new agent
465
- agent = Raif::Agents::ReActAgent.new(
466
- task: "Research the history of the Eiffel Tower",
467
- available_model_tools: [Raif::ModelTools::WikipediaSearch, Raif::ModelTools::FetchUrl],
468
- creator: current_user
469
- )
470
-
471
- # Run the agent and get the final answer
472
- final_answer = agent.run!
473
-
474
- # Or run the agent and monitor its progress
475
- agent.run! do |conversation_history_entry|
476
- Turbo::StreamsChannel.broadcast_append_to(
477
- :my_agent_channel,
478
- target: "agent-progress",
479
- partial: "my_partial_displaying_agent_progress",
480
- locals: { agent: agent, conversation_history_entry: conversation_history_entry }
481
- )
482
- end
483
- ```
484
-
485
- 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.
486
-
487
- The conversation_history_entry will be a hash with "role" and "content" keys:
488
- ```ruby
489
- {
490
- "role" => "assistant",
491
- "content" => "a message here"
492
- }
493
- ```
494
-
495
- ### Creating Custom Agents
496
-
497
- You can create custom agents using the generator:
498
- ```bash
499
- rails generate raif:agent WikipediaResearchAgent
500
- ```
501
-
502
- This will create a new agent in `app/models/raif/agents/wikipedia_research_agent.rb`:
39
+ [Tasks](https://docs.raif.ai/key_raif_concepts/tasks)
40
+ [Conversations](https://docs.raif.ai/key_raif_concepts/conversations)
41
+ [Agents](https://docs.raif.ai/key_raif_concepts/agents)
42
+ [Model Tools](https://docs.raif.ai/key_raif_concepts/model_tools)
503
43
 
504
- ```ruby
505
- module Raif
506
- module Agents
507
- class WikipediaResearchAgent < Raif::Agent
508
- # If you want to always include a certain set of model tools with this agent type,
509
- # uncomment this callback to populate the available_model_tools attribute with your desired model tools.
510
- # before_create -> {
511
- # self.available_model_tools ||= [
512
- # Raif::ModelTools::WikipediaSearchTool,
513
- # Raif::ModelTools::FetchUrlTool
514
- # ]
515
- # }
516
44
 
517
- # Enter your agent's system prompt here. Alternatively, you can change your agent's superclass
518
- # to an existing agent types (like Raif::Agents::ReActAgent) to utilize an existing system prompt.
519
- def build_system_prompt
520
- # TODO: Implement your system prompt here
521
- end
45
+ # Images/Files/PDF's
522
46
 
523
- # Each iteration of the agent loop will generate a new Raif::ModelCompletion record and
524
- # then call this method with it as an argument.
525
- def process_iteration_model_completion(model_completion)
526
- # TODO: Implement your iteration processing here
527
- end
528
- end
529
- end
530
- end
47
+ [Images/Files/PDF's](https://docs.raif.ai/learn_more/images_files_pdfs)
531
48
 
532
- ```
533
-
534
- ## Model Tools
535
-
536
- 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.
537
-
538
- You can create your own model tools to provide to the LLM using the generator:
539
- ```bash
540
- rails generate raif:model_tool GoogleSearch
541
- ```
542
-
543
- This will create a new model tool in `app/models/raif/model_tools/google_search.rb`:
544
-
545
- ```ruby
546
- class Raif::ModelTools::GoogleSearch < Raif::ModelTool
547
- # For example tool implementations, see:
548
- # Wikipedia Search Tool: https://github.com/CultivateLabs/raif/blob/main/app/models/raif/model_tools/wikipedia_search.rb
549
- # Fetch URL Tool: https://github.com/CultivateLabs/raif/blob/main/app/models/raif/model_tools/fetch_url.rb
550
-
551
- # Define the schema for the arguments that the LLM should use when invoking your tool.
552
- # It should be a valid JSON schema. When the model invokes your tool,
553
- # the arguments it provides will be validated against this schema using JSON::Validator from the json-schema gem.
554
- #
555
- # All attributes will be required and additionalProperties will be set to false.
556
- #
557
- # This schema would expect the model to invoke your tool with an arguments JSON object like:
558
- # { "query" : "some query here" }
559
- tool_arguments_schema do
560
- string :query, description: "The query to search for"
561
- end
562
-
563
- # An example of how the LLM should invoke your tool. This should return a hash with name and arguments keys.
564
- # `to_json` will be called on it and provided to the LLM as an example of how to invoke your tool.
565
- example_model_invocation do
566
- {
567
- "name": tool_name,
568
- "arguments": { "query": "example query here" }
569
- }
570
- end
571
-
572
- tool_description do
573
- "Description of your tool that will be provided to the LLM so it knows when to invoke it"
574
- end
575
-
576
- # When your tool is invoked by the LLM in a Raif::Agent loop,
577
- # the results of the tool invocation are provided back to the LLM as an observation.
578
- # This method should return whatever you want provided to the LLM.
579
- # For example, if you were implementing a GoogleSearch tool, this might return a JSON
580
- # object containing search results for the query.
581
- def self.observation_for_invocation(tool_invocation)
582
- return "No results found" unless tool_invocation.result.present?
583
-
584
- JSON.pretty_generate(tool_invocation.result)
585
- end
586
-
587
- # When the LLM invokes your tool, this method will be called with a `Raif::ModelToolInvocation` record as an argument.
588
- # It should handle the actual execution of the tool.
589
- # For example, if you are implementing a GoogleSearch tool, this method should run the actual search
590
- # and store the results in the tool_invocation's result JSON column.
591
- def self.process_invocation(tool_invocation)
592
- # Extract arguments from tool_invocation.tool_arguments
593
- # query = tool_invocation.tool_arguments["query"]
594
- #
595
- # Process the invocation and perform the desired action
596
- # ...
597
- #
598
- # Store the results in the tool_invocation
599
- # tool_invocation.update!(
600
- # result: {
601
- # # Your result data structure
602
- # }
603
- # )
604
- #
605
- # Return the result
606
- # tool_invocation.result
607
- end
608
-
609
- end
610
- ```
611
-
612
- ### Provider-Managed Tools
613
-
614
- In addition to the ability to create your own model tools, Raif supports provider-managed tools. These are tools that are built into certain LLM providers and run on the provider's infrastructure:
615
-
616
- - **`Raif::ModelTools::ProviderManaged::WebSearch`**: Performs real-time web searches and returns relevant results
617
- - **`Raif::ModelTools::ProviderManaged::CodeExecution`**: Executes code in a secure sandboxed environment (e.g. Python)
618
- - **`Raif::ModelTools::ProviderManaged::ImageGeneration`**: Generates images based on text descriptions
619
-
620
- Current provider-managed tool support:
621
- | Provider | WebSearch | CodeExecution | ImageGeneration |
622
- |----------|-----------|---------------|-----------------|
623
- | OpenAI Responses API | ✅ | ✅ | ✅ |
624
- | OpenAI Completions API | ❌ | ❌ | ❌ |
625
- | Anthropic Claude | ✅ | ✅ | ❌ |
626
- | AWS Bedrock (Claude) | ❌ | ❌ | ❌ |
627
- | OpenRouter | ❌ | ❌ | ❌ |
628
-
629
- To use provider-managed tools, include them in the `available_model_tools` array:
630
-
631
- ```ruby
632
- # In a conversation
633
- conversation = Raif::Conversation.create!(
634
- creator: current_user,
635
- available_model_tools: [
636
- "Raif::ModelTools::ProviderManaged::WebSearch",
637
- "Raif::ModelTools::ProviderManaged::CodeExecution"
638
- ]
639
- )
640
-
641
- # In an agent
642
- agent = Raif::Agents::ReActAgent.new(
643
- task: "Search for recent news about AI and create a summary chart",
644
- available_model_tools: [
645
- "Raif::ModelTools::ProviderManaged::WebSearch",
646
- "Raif::ModelTools::ProviderManaged::CodeExecution"
647
- ],
648
- creator: current_user
649
- )
650
-
651
- # Directly in a chat
652
- llm = Raif.llm(:open_ai_responses_gpt_4_1)
653
- model_completion = llm.chat(
654
- messages: [{ role: "user", content: "What are the latest developments in Ruby on Rails?" }],
655
- available_model_tools: [Raif::ModelTools::ProviderManaged::WebSearch]
656
- )
657
- ```
658
-
659
- ## Sending Images/Files/PDF's to the LLM
660
-
661
- Raif supports images, files, and PDF's in the messages sent to the LLM.
662
-
663
- To include an image, file/PDF in a message, you can use the `Raif::ModelImageInput` and `Raif::ModelFileInput`.
664
-
665
- To include an image:
666
- ```ruby
667
- # From a local file
668
- image = Raif::ModelImageInput.new(input: "path/to/image.png")
669
-
670
- # From a URL
671
- image = Raif::ModelImageInput.new(url: "https://example.com/image.png")
672
-
673
- # From an ActiveStorage attachment (assumes you have a User model with an avatar attachment)
674
- image = Raif::ModelImageInput.new(input: user.avatar)
675
-
676
- # Then chat with the LLM
677
- llm = Raif.llm(:open_ai_gpt_4o)
678
- model_completion = llm.chat(messages: [
679
- { role: "user", content: ["What's in this image?", image]}
680
- ])
681
- ```
682
-
683
- To include a file/PDF:
684
- ```ruby
685
- # From a local file
686
- file = Raif::ModelFileInput.new(input: "path/to/file.pdf")
687
-
688
- # From a URL
689
- file = Raif::ModelFileInput.new(url: "https://example.com/file.pdf")
690
-
691
- # From an ActiveStorage attachment (assumes you have a Document model with a pdf attachment)
692
- file = Raif::ModelFileInput.new(input: document.pdf)
693
-
694
- # Then chat with the LLM
695
- llm = Raif.llm(:open_ai_gpt_4o)
696
- model_completion = llm.chat(messages: [
697
- { role: "user", content: ["What's in this file?", file]}
698
- ])
699
- ```
700
-
701
- ### Images/Files/PDF's in Tasks
702
-
703
- You can include images and files/PDF's when running a `Raif::Task`:
704
-
705
- To include a file/PDF:
706
- ```ruby
707
- file = Raif::ModelFileInput.new(input: "path/to/file.pdf")
708
-
709
- # Assumes you've created a PdfContentExtraction task
710
- task = Raif::Tasks::PdfContentExtraction.run(
711
- creator: current_user,
712
- files: [file]
713
- )
714
- ```
715
-
716
- To include an image:
717
- ```ruby
718
- image = Raif::ModelImageInput.new(input: "path/to/image.png")
719
-
720
- # Assumes you've created a ImageDescriptionGeneration task
721
- task = Raif::Tasks::ImageDescriptionGeneration.run(
722
- creator: current_user,
723
- images: [image]
724
- )
725
- ```
726
49
 
727
50
 
728
51
  # Embedding Models
729
52
 
730
- Raif supports generation of vector embeddings. You can enable and configure embedding models in your Raif configuration:
731
-
732
- ```ruby
733
- Raif.configure do |config|
734
- config.open_ai_embedding_models_enabled = true
735
- config.bedrock_embedding_models_enabled = true
736
-
737
- config.default_embedding_model_key = "open_ai_text_embedding_3_small"
738
- end
739
- ```
740
-
741
- ## Supported Embedding Models
742
-
743
- Raif currently supports the following embedding models:
744
-
745
- ### OpenAI
746
- - `open_ai_text_embedding_3_small`
747
- - `open_ai_text_embed ding_3_large`
748
- - `open_ai_text_embedding_ada_002`
749
-
750
- ### AWS Bedrock
751
- - `bedrock_titan_embed_text_v2`
752
-
753
- ## Creating Embeddings
754
-
755
- By default, Raif will used `Raif.config.default_embedding_model_key` to create embeddings. To create an embedding for a piece of text:
756
-
757
- ```ruby
758
- # Generate an embedding for a piece of text
759
- embedding = Raif.generate_embedding!("Your text here")
760
-
761
- # Generate an embedding for a piece of text with a specific number of dimensions
762
- embedding = Raif.generate_embedding!("Your text here", dimensions: 1024)
763
-
764
- # If you're using an OpenAI embedding model, you can pass an array of strings to embed multiple texts at once
765
- embeddings = Raif.generate_embedding!([
766
- "Your text here",
767
- "Your other text here"
768
- ])
769
- ```
770
-
771
- Or to generate embeddings for a piece of text with a specific model:
772
-
773
- ```ruby
774
- model = Raif.embedding_model(:open_ai_text_embedding_3_small)
775
- embedding = model.generate_embedding!("Your text here")
776
- ```
53
+ [Embedding Models](https://docs.raif.ai/learn_more/embedding_models)
777
54
 
778
55
  # Web Admin
779
56
 
780
- 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`.
781
-
782
- The admin interface contains sections for:
783
- - Model Completions
784
- - Tasks
785
- - Conversations
786
- - Agents
787
- - Model Tool Invocations
788
- - Stats
789
-
790
-
791
- ### Model Completions
792
- ![Model Completions Index](./screenshots/admin-model-completions-index.png)
793
- ![Model Completion Detail](./screenshots/admin-model-completion-show.png)
794
-
795
- ### Tasks
796
- ![Tasks Index](./screenshots/admin-tasks-index.png)
797
-
798
- ### Conversations
799
- ![Conversations Index](./screenshots/admin-conversations-index.png)
800
- ![Conversation Detail](./screenshots/admin-conversation-show.png)
801
-
802
- ### Agents
803
- ![Agents Index](./screenshots/admin-agents-index.png)
804
- ![Agents Detail](./screenshots/admin-agents-show.png)
805
-
806
- ### Model Tool Invocations
807
- ![Model Tool Invocations Index](./screenshots/admin-model-tool-invocations-index.png)
808
- ![Model Tool Invocation Detail](./screenshots/admin-model-tool-invocation-show.png)
809
-
810
- ### Stats
811
- ![Stats](./screenshots/admin-stats.png)
57
+ [Web Admin](https://docs.raif.ai/learn_more/web_admin)
812
58
 
813
59
  # Customization
814
60
 
815
- ## Controllers
816
-
817
- You can override Raif's controllers by creating your own that inherit from Raif's base controllers:
818
-
819
- ```ruby
820
- class ConversationsController < Raif::ConversationsController
821
- # Your customizations here
822
- end
823
-
824
- class ConversationEntriesController < Raif::ConversationEntriesController
825
- # Your customizations here
826
- end
827
- ```
828
-
829
- Then update the configuration:
830
- ```ruby
831
- Raif.configure do |config|
832
- config.conversations_controller = "ConversationsController"
833
- config.conversation_entries_controller = "ConversationEntriesController"
834
- end
835
- ```
836
-
837
- ## Models
838
-
839
- By default, Raif models inherit from `ApplicationRecord`. You can change this:
840
-
841
- ```ruby
842
- Raif.configure do |config|
843
- config.model_superclass = "CustomRecord"
844
- end
845
- ```
846
-
847
- ## Views
848
-
849
- You can customize Raif's views by copying them to your application and modifying them. To copy the conversation-related views, run:
850
-
851
- ```bash
852
- rails generate raif:views
853
- ```
854
-
855
- This will copy all conversation and conversation entry views to your application in:
856
- - `app/views/raif/conversations/`
857
- - `app/views/raif/conversation_entries/`
858
-
859
- 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.
860
-
861
- ## System Prompts
862
-
863
- 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:
864
-
865
- ```ruby
866
- Raif.configure do |config|
867
- config.conversation_system_prompt_intro = "You are a helpful assistant who specializes in customer support."
868
- config.task_system_prompt_intro = "You are a helpful assistant who specializes in data analysis."
869
- # or with a lambda
870
- config.task_system_prompt_intro = ->(task) { "You are a helpful assistant who specializes in #{task.name}." }
871
- config.conversation_system_prompt_intro = ->(conversation) { "You are a helpful assistant talking to #{conversation.creator.email}. Today's date is #{Date.today.strftime('%B %d, %Y')}." }
872
- end
873
- ```
874
-
875
- ## Adding LLM Models
876
-
877
- You can easily add new LLM models to Raif:
878
-
879
- ```ruby
880
- # Register the model in Raif's LLM registry
881
- Raif.register_llm(Raif::Llms::OpenRouter, {
882
- key: :open_router_gemini_flash_1_5_8b, # a unique key for the model
883
- api_name: "google/gemini-flash-1.5-8b", # name of the model to be used in API calls - needs to match the provider's API name
884
- input_token_cost: 0.038 / 1_000_000, # the cost per input token
885
- output_token_cost: 0.15 / 1_000_000, # the cost per output token
886
- })
887
-
888
- # Then use the model
889
- llm = Raif.llm(:open_router_gemini_flash_1_5_8b)
890
- llm.chat(message: "Hello, world!")
891
-
892
- # Or set it as the default LLM model in your initializer
893
- Raif.configure do |config|
894
- config.default_llm_model_key = "open_router_gemini_flash_1_5_8b"
895
- end
896
- ```
61
+ [Customization](https://docs.raif.ai/learn_more/customization)
897
62
 
898
63
  # Testing
899
64
 
900
- Raif includes RSpec helpers and FactoryBot factories to help with testing in your application.
901
-
902
- To use the helpers, add the following to your `rails_helper.rb`:
903
-
904
- ```ruby
905
- require "raif/rspec"
906
-
907
- RSpec.configure do |config|
908
- config.include Raif::RspecHelpers
909
- end
910
- ```
911
-
912
- You can then use the helpers to stub LLM calls:
913
-
914
- ```ruby
915
- it "stubs a document summarization task" do
916
- # the messages argument is the array of messages sent to the LLM. It will look something like:
917
- # [{"role" => "user", "content" => "The prompt from the Raif::Tasks::DocumentSummarization task" }]
918
- # The model_completion argument is the Raif::ModelCompletion record that was created for this task.
919
- stub_raif_task(Raif::Tasks::DocumentSummarization) do |messages, model_completion|
920
- "Stub out the response from the LLM"
921
- end
922
-
923
- user = FactoryBot.create(:user) # assumes you have a User model & factory
924
- document = FactoryBot.create(:document) # assumes you have a Document model & factory
925
- task = Raif::Tasks::DocumentSummarization.run(document: document, creator: user)
926
-
927
- expect(task.raw_response).to eq("Stub out the response from the LLM")
928
- end
929
-
930
- it "stubs a conversation" do
931
- user = FactoryBot.create(:user) # assumes you have a User model & factory
932
- conversation = FactoryBot.create(:raif_test_conversation, creator: user)
933
- conversation_entry = FactoryBot.create(:raif_conversation_entry, raif_conversation: conversation, creator: user)
934
-
935
- stub_raif_conversation(conversation) do |messages, model_completion|
936
- "Hello"
937
- end
938
-
939
- conversation_entry.process_entry!
940
- expect(conversation_entry.reload).to be_completed
941
- expect(conversation_entry.model_response_message).to eq("Hello")
942
- end
943
-
944
- it "stubs an agent" do
945
- i = 0
946
- stub_raif_agent(agent) do |messages, model_completion|
947
- i += 1
948
- if i == 1
949
- "<thought>I need to search.</thought>\n<action>{\"tool\": \"wikipedia_search\", \"arguments\": {\"query\": \"capital of France\"}}</action>"
950
- else
951
- "<thought>Now I know.</thought>\n<answer>Paris</answer>"
952
- end
953
- end
954
- end
955
- ```
956
-
957
- 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).
65
+ [Testing](https://docs.raif.ai/learn_more/testing)
958
66
 
959
67
  # Demo App
960
68
 
961
- 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:
962
-
963
- ```bash
964
- git clone git@github.com:CultivateLabs/raif_demo.git
965
- cd raif_demo
966
- bundle install
967
- bin/rails db:create db:prepare
968
- OPENAI_API_KEY=your-openai-api-key-here bin/rails s
969
- ```
970
-
971
- You can then access the app at [http://localhost:3000](http://localhost:3000).
972
-
973
- ![Demo App Screenshot](./screenshots/demo-app.png)
69
+ [Demo App](https://docs.raif.ai/learn_more/demo_app)
974
70
 
975
71
  # Contributing
976
72
 
977
73
  We welcome contributions to Raif! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
978
74
 
979
- **Important**: All PR's should be made against the `dev` branch.
980
-
981
75
  # License
982
76
 
983
77
  The gem is available as open source under the terms of the MIT License.