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.
- checksums.yaml +4 -4
- data/README.md +29 -935
- data/app/assets/builds/raif_admin.css +5 -1
- data/app/assets/images/raif-logo-white.svg +8 -0
- data/app/assets/stylesheets/raif_admin.scss +4 -0
- data/app/models/raif/conversation.rb +4 -0
- data/app/models/raif/conversation_entry.rb +0 -1
- data/app/models/raif/llms/open_router.rb +44 -3
- data/app/views/layouts/raif/admin.html.erb +3 -1
- data/lib/generators/raif/install/templates/initializer.rb +3 -3
- data/lib/generators/raif/model_tool/model_tool_generator.rb +3 -0
- data/lib/generators/raif/model_tool/templates/model_tool.rb.tt +2 -0
- data/lib/generators/raif/model_tool/templates/model_tool_invocation_partial.html.erb.tt +10 -0
- data/lib/raif/configuration.rb +4 -4
- data/lib/raif/engine.rb +2 -1
- data/lib/raif/version.rb +1 -1
- metadata +3 -1
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+

|
2
2
|
|
3
3
|
[](https://badge.fury.io/rb/raif)
|
4
4
|
[](https://github.com/cultivate-labs/raif/actions/workflows/ci.yml)
|
@@ -6,978 +6,72 @@
|
|
6
6
|
[](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](
|
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](
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
- [
|
22
|
-
|
23
|
-
- [
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
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
|
-

|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-

|
793
|
-

|
794
|
-
|
795
|
-
### Tasks
|
796
|
-

|
797
|
-
|
798
|
-
### Conversations
|
799
|
-

|
800
|
-

|
801
|
-
|
802
|
-
### Agents
|
803
|
-

|
804
|
-

|
805
|
-
|
806
|
-
### Model Tool Invocations
|
807
|
-

|
808
|
-

|
809
|
-
|
810
|
-
### Stats
|
811
|
-

|
57
|
+
[Web Admin](https://docs.raif.ai/learn_more/web_admin)
|
812
58
|
|
813
59
|
# Customization
|
814
60
|
|
815
|
-
|
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
|
-
|
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
|
-
|
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
|
-

|
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.
|