raif 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +200 -41
- data/app/assets/stylesheets/raif/admin/stats.scss +12 -0
- data/app/controllers/raif/admin/application_controller.rb +14 -0
- data/app/controllers/raif/admin/stats/tasks_controller.rb +25 -0
- data/app/controllers/raif/admin/stats_controller.rb +19 -0
- data/app/controllers/raif/admin/tasks_controller.rb +18 -2
- data/app/controllers/raif/conversations_controller.rb +5 -1
- data/app/models/raif/agent.rb +11 -9
- data/app/models/raif/agents/native_tool_calling_agent.rb +11 -1
- data/app/models/raif/agents/re_act_agent.rb +6 -0
- data/app/models/raif/concerns/has_available_model_tools.rb +1 -1
- data/app/models/raif/concerns/json_schema_definition.rb +28 -0
- data/app/models/raif/concerns/llm_response_parsing.rb +23 -1
- data/app/models/raif/concerns/llm_temperature.rb +17 -0
- data/app/models/raif/concerns/llms/anthropic/message_formatting.rb +51 -0
- data/app/models/raif/concerns/llms/bedrock_claude/message_formatting.rb +70 -0
- data/app/models/raif/concerns/llms/message_formatting.rb +41 -0
- data/app/models/raif/concerns/llms/open_ai/message_formatting.rb +41 -0
- data/app/models/raif/conversation.rb +11 -3
- data/app/models/raif/conversation_entry.rb +22 -6
- data/app/models/raif/embedding_model.rb +22 -0
- data/app/models/raif/embedding_models/bedrock_titan.rb +34 -0
- data/app/models/raif/embedding_models/open_ai.rb +40 -0
- data/app/models/raif/llm.rb +39 -6
- data/app/models/raif/llms/anthropic.rb +23 -28
- data/app/models/raif/llms/bedrock_claude.rb +33 -19
- data/app/models/raif/llms/open_ai.rb +14 -17
- data/app/models/raif/llms/open_router.rb +93 -0
- data/app/models/raif/model_completion.rb +21 -2
- data/app/models/raif/model_file_input.rb +113 -0
- data/app/models/raif/model_image_input.rb +4 -0
- data/app/models/raif/model_tool.rb +77 -51
- data/app/models/raif/model_tool_invocation.rb +8 -6
- data/app/models/raif/model_tools/agent_final_answer.rb +18 -27
- data/app/models/raif/model_tools/fetch_url.rb +27 -36
- data/app/models/raif/model_tools/wikipedia_search.rb +46 -55
- data/app/models/raif/task.rb +71 -16
- data/app/views/layouts/raif/admin.html.erb +10 -0
- data/app/views/raif/admin/agents/show.html.erb +3 -1
- data/app/views/raif/admin/conversations/_conversation.html.erb +1 -1
- data/app/views/raif/admin/conversations/show.html.erb +3 -1
- data/app/views/raif/admin/model_completions/_model_completion.html.erb +1 -0
- data/app/views/raif/admin/model_completions/index.html.erb +1 -0
- data/app/views/raif/admin/model_completions/show.html.erb +30 -3
- data/app/views/raif/admin/stats/index.html.erb +128 -0
- data/app/views/raif/admin/stats/tasks/index.html.erb +45 -0
- data/app/views/raif/admin/tasks/_task.html.erb +5 -4
- data/app/views/raif/admin/tasks/index.html.erb +20 -2
- data/app/views/raif/admin/tasks/show.html.erb +3 -1
- data/app/views/raif/conversation_entries/_conversation_entry.html.erb +18 -14
- data/app/views/raif/conversation_entries/_form.html.erb +1 -1
- data/app/views/raif/conversation_entries/_form_with_available_tools.html.erb +4 -4
- data/app/views/raif/conversation_entries/_message.html.erb +10 -3
- data/config/locales/admin.en.yml +14 -0
- data/config/locales/en.yml +25 -3
- data/config/routes.rb +6 -0
- data/db/migrate/20250421202149_add_response_format_to_raif_conversations.rb +7 -0
- data/db/migrate/20250424200755_add_cost_columns_to_raif_model_completions.rb +14 -0
- data/db/migrate/20250424232946_add_created_at_indexes.rb +11 -0
- data/db/migrate/20250502155330_add_status_indexes_to_raif_tasks.rb +14 -0
- data/db/migrate/20250507155314_add_retry_count_to_raif_model_completions.rb +7 -0
- data/lib/generators/raif/agent/agent_generator.rb +22 -12
- data/lib/generators/raif/agent/templates/agent.rb.tt +3 -3
- data/lib/generators/raif/agent/templates/application_agent.rb.tt +7 -0
- data/lib/generators/raif/conversation/conversation_generator.rb +10 -0
- data/lib/generators/raif/conversation/templates/application_conversation.rb.tt +7 -0
- data/lib/generators/raif/conversation/templates/conversation.rb.tt +13 -11
- data/lib/generators/raif/install/templates/initializer.rb +50 -6
- data/lib/generators/raif/model_tool/model_tool_generator.rb +0 -5
- data/lib/generators/raif/model_tool/templates/model_tool.rb.tt +69 -56
- data/lib/generators/raif/task/templates/task.rb.tt +34 -23
- data/lib/raif/configuration.rb +40 -3
- data/lib/raif/embedding_model_registry.rb +83 -0
- data/lib/raif/engine.rb +34 -1
- data/lib/raif/errors/{open_ai/api_error.rb → invalid_model_file_input_error.rb} +1 -3
- data/lib/raif/errors/{anthropic/api_error.rb → invalid_model_image_input_error.rb} +1 -3
- data/lib/raif/errors/unsupported_feature_error.rb +8 -0
- data/lib/raif/errors.rb +3 -2
- data/lib/raif/json_schema_builder.rb +104 -0
- data/lib/raif/llm_registry.rb +205 -0
- data/lib/raif/version.rb +1 -1
- data/lib/raif.rb +5 -32
- data/lib/tasks/raif_tasks.rake +9 -4
- metadata +32 -19
- data/lib/raif/default_llms.rb +0 -37
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1af0a7a003990a40716f3d07ec87838e5fb485147bb87a9b35ae1fe23f642501
|
4
|
+
data.tar.gz: b82bba67ed23a6e47e1bd02f5c64f9448c7374017c097a012ccdae2e3795be34
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d9dd7273eeccb284d7ee720fe586dc1e86b6e3355da378a719310cbe3209c508a57a3ee1cfcaa60762cd2860f76960b4f89aa39d7f5eafa90eb97b78adab6123
|
7
|
+
data.tar.gz: 26013bb1beb60367d878b451163594c50e36ef046b8ae0864a4728c4d0ae862e79ec22ab5ccb6c0acd88991b9f227b3af4b31daedc76e90755e9d87bce45f25c
|
data/README.md
CHANGED
@@ -6,7 +6,7 @@
|
|
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](#tasks), [conversations](#conversations), and [agents](#agents). It supports for multiple LLM providers including [OpenAI](#openai), [Anthropic Claude](#anthropic-claude),
|
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).
|
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
|
|
@@ -15,6 +15,7 @@ Raif is built by [Cultivate Labs](https://www.cultivatelabs.com) and is used to
|
|
15
15
|
- [OpenAI](#openai)
|
16
16
|
- [Anthropic Claude](#anthropic-claude)
|
17
17
|
- [AWS Bedrock (Claude)](#aws-bedrock-claude)
|
18
|
+
- [OpenRouter](#openrouter)
|
18
19
|
- [Chatting with the LLM](#chatting-with-the-llm)
|
19
20
|
- [Key Raif Concepts](#key-raif-concepts)
|
20
21
|
- [Tasks](#tasks)
|
@@ -22,12 +23,16 @@ Raif is built by [Cultivate Labs](https://www.cultivatelabs.com) and is used to
|
|
22
23
|
- [Conversation Types](#conversation-types)
|
23
24
|
- [Agents](#agents)
|
24
25
|
- [Model Tools](#model-tools)
|
26
|
+
- [Images/Files/PDF's](#imagesfilespdfs)
|
27
|
+
- [Images/Files/PDF's in Tasks](#imagesfilespdfs-in-tasks)
|
28
|
+
- [Embedding Models](#embedding-models)
|
25
29
|
- [Web Admin](#web-admin)
|
26
30
|
- [Customization](#customization)
|
27
31
|
- [Controllers](#controllers)
|
28
32
|
- [Models](#models)
|
29
33
|
- [Views](#views)
|
30
34
|
- [System Prompts](#system-prompts)
|
35
|
+
- [Adding LLM Models](#adding-llm-models)
|
31
36
|
- [Testing](#testing)
|
32
37
|
- [Demo App](#demo-app)
|
33
38
|
- [License](#license)
|
@@ -122,6 +127,28 @@ Currently supported Bedrock models:
|
|
122
127
|
|
123
128
|
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
129
|
|
130
|
+
## OpenRouter
|
131
|
+
[OpenRouter](https://openrouter.ai/) is a unified API that provides access to multiple AI models from different providers including Anthropic, Meta, Google, and more.
|
132
|
+
|
133
|
+
```ruby
|
134
|
+
Raif.configure do |config|
|
135
|
+
config.open_router_models_enabled = true
|
136
|
+
config.open_router_api_key = ENV["OPENROUTER_API_KEY"]
|
137
|
+
config.open_router_app_name = "Your App Name" # Optional
|
138
|
+
config.open_router_site_url = "https://yourdomain.com" # Optional
|
139
|
+
config.default_llm_model_key = "open_router_claude_3_7_sonnet"
|
140
|
+
end
|
141
|
+
```
|
142
|
+
|
143
|
+
Currently included OpenRouter models:
|
144
|
+
- `open_router_claude_3_7_sonnet`
|
145
|
+
- `open_router_llama_3_3_70b_instruct`
|
146
|
+
- `open_router_llama_3_1_8b_instruct`
|
147
|
+
- `open_router_gemini_2_0_flash`
|
148
|
+
- `open_router_deepseek_chat_v3`
|
149
|
+
|
150
|
+
See [Adding LLM Models](#adding-llm-models) for more information on adding new OpenRouter models.
|
151
|
+
|
125
152
|
# Chatting with the LLM
|
126
153
|
|
127
154
|
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).
|
@@ -160,7 +187,7 @@ puts model_completion.parsed_response # will strip backticks, parse the JSON, an
|
|
160
187
|
# Key Raif Concepts
|
161
188
|
|
162
189
|
## 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
|
190
|
+
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:
|
164
191
|
|
165
192
|
```bash
|
166
193
|
rails generate raif:task DocumentSummarization --response-format html
|
@@ -171,7 +198,10 @@ This will create a new task in `app/models/raif/tasks/document_summarization.rb`
|
|
171
198
|
```ruby
|
172
199
|
class Raif::Tasks::DocumentSummarization < Raif::ApplicationTask
|
173
200
|
llm_response_format :html # options are :html, :text, :json
|
174
|
-
|
201
|
+
llm_temperature 0.8 # optional, defaults to 0.7
|
202
|
+
llm_response_allowed_tags %w[p b i div strong] # optional, defaults to Rails::HTML5::SafeListSanitizer.allowed_tags
|
203
|
+
llm_response_allowed_attributes %w[style] # optional, defaults to Rails::HTML5::SafeListSanitizer.allowed_attributes
|
204
|
+
|
175
205
|
# Any attr_accessor you define can be included as an argument when calling `run`.
|
176
206
|
# E.g. Raif::Tasks::DocumentSummarization.run(document: document, creator: user)
|
177
207
|
attr_accessor :document
|
@@ -229,20 +259,10 @@ module Raif
|
|
229
259
|
|
230
260
|
attr_accessor :topic
|
231
261
|
|
232
|
-
|
233
|
-
|
234
|
-
type: "
|
235
|
-
|
236
|
-
required: ["queries"],
|
237
|
-
properties: {
|
238
|
-
queries: {
|
239
|
-
type: "array",
|
240
|
-
items: {
|
241
|
-
type: "string"
|
242
|
-
}
|
243
|
-
}
|
244
|
-
}
|
245
|
-
}
|
262
|
+
json_response_schema do
|
263
|
+
array :queries do
|
264
|
+
items type: "string"
|
265
|
+
end
|
246
266
|
end
|
247
267
|
|
248
268
|
def build_prompt
|
@@ -271,6 +291,8 @@ You are an assistant with expertise in summarizing detailed articles into clear
|
|
271
291
|
You're collaborating with teammate who speaks Spanish. Please respond in Spanish.
|
272
292
|
```
|
273
293
|
|
294
|
+
The current list of valid language keys can be found [here](https://github.com/CultivateLabs/raif/blob/main/lib/raif/languages.rb).
|
295
|
+
|
274
296
|
## Conversations
|
275
297
|
|
276
298
|
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.
|
@@ -434,39 +456,31 @@ This will create a new model tool in `app/models/raif/model_tools/google_search.
|
|
434
456
|
```ruby
|
435
457
|
class Raif::ModelTools::GoogleSearch < Raif::ModelTool
|
436
458
|
# For example tool implementations, see:
|
437
|
-
# Wikipedia Search Tool: https://github.com/CultivateLabs/raif/blob/main/app/models/raif/model_tools/
|
438
|
-
# Fetch URL Tool: https://github.com/CultivateLabs/raif/blob/main/app/models/raif/model_tools/
|
459
|
+
# Wikipedia Search Tool: https://github.com/CultivateLabs/raif/blob/main/app/models/raif/model_tools/wikipedia_search.rb
|
460
|
+
# Fetch URL Tool: https://github.com/CultivateLabs/raif/blob/main/app/models/raif/model_tools/fetch_url.rb
|
461
|
+
|
462
|
+
# Define the schema for the arguments that the LLM should use when invoking your tool.
|
463
|
+
# It should be a valid JSON schema. When the model invokes your tool,
|
464
|
+
# the arguments it provides will be validated against this schema using JSON::Validator from the json-schema gem.
|
465
|
+
#
|
466
|
+
# All attributes will be required and additionalProperties will be set to false.
|
467
|
+
#
|
468
|
+
# This schema would expect the model to invoke your tool with an arguments JSON object like:
|
469
|
+
# { "query" : "some query here" }
|
470
|
+
tool_arguments_schema do
|
471
|
+
string :query, description: "The query to search for"
|
472
|
+
end
|
439
473
|
|
440
474
|
# An example of how the LLM should invoke your tool. This should return a hash with name and arguments keys.
|
441
475
|
# `to_json` will be called on it and provided to the LLM as an example of how to invoke your tool.
|
442
|
-
|
476
|
+
example_model_invocation do
|
443
477
|
{
|
444
478
|
"name": tool_name,
|
445
479
|
"arguments": { "query": "example query here" }
|
446
480
|
}
|
447
481
|
end
|
448
482
|
|
449
|
-
|
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
|
483
|
+
tool_description do
|
470
484
|
"Description of your tool that will be provided to the LLM so it knows when to invoke it"
|
471
485
|
end
|
472
486
|
|
@@ -506,6 +520,125 @@ class Raif::ModelTools::GoogleSearch < Raif::ModelTool
|
|
506
520
|
end
|
507
521
|
```
|
508
522
|
|
523
|
+
## Images/Files/PDF's
|
524
|
+
|
525
|
+
Raif supports images, files, and PDF's in the messages sent to the LLM.
|
526
|
+
|
527
|
+
To include an image, file/PDF in a message, you can use the `Raif::ModelImageInput` and `Raif::ModelFileInput`.
|
528
|
+
|
529
|
+
To include an image:
|
530
|
+
```ruby
|
531
|
+
# From a local file
|
532
|
+
image = Raif::ModelImageInput.new(input: "path/to/image.png")
|
533
|
+
|
534
|
+
# From a URL
|
535
|
+
image = Raif::ModelImageInput.new(url: "https://example.com/image.png")
|
536
|
+
|
537
|
+
# From an ActiveStorage attachment (assumes you have a User model with an avatar attachment)
|
538
|
+
image = Raif::ModelImageInput.new(input: user.avatar)
|
539
|
+
|
540
|
+
# Then chat with the LLM
|
541
|
+
llm = Raif.llm(:open_ai_gpt_4o)
|
542
|
+
model_completion = llm.chat(messages: [
|
543
|
+
{ role: "user", content: ["What's in this image?", image]}
|
544
|
+
])
|
545
|
+
```
|
546
|
+
|
547
|
+
To include a file/PDF:
|
548
|
+
```ruby
|
549
|
+
# From a local file
|
550
|
+
file = Raif::ModelFileInput.new(input: "path/to/file.pdf")
|
551
|
+
|
552
|
+
# From a URL
|
553
|
+
file = Raif::ModelFileInput.new(url: "https://example.com/file.pdf")
|
554
|
+
|
555
|
+
# From an ActiveStorage attachment (assumes you have a Document model with a pdf attachment)
|
556
|
+
file = Raif::ModelFileInput.new(input: document.pdf)
|
557
|
+
|
558
|
+
# Then chat with the LLM
|
559
|
+
llm = Raif.llm(:open_ai_gpt_4o)
|
560
|
+
model_completion = llm.chat(messages: [
|
561
|
+
{ role: "user", content: ["What's in this file?", file]}
|
562
|
+
])
|
563
|
+
```
|
564
|
+
|
565
|
+
### Images/Files/PDF's in Tasks
|
566
|
+
|
567
|
+
You can include images and files/PDF's when running a `Raif::Task`:
|
568
|
+
|
569
|
+
To include a file/PDF:
|
570
|
+
```ruby
|
571
|
+
file = Raif::ModelFileInput.new(input: "path/to/file.pdf")
|
572
|
+
|
573
|
+
# Assumes you've created a PdfContentExtraction task
|
574
|
+
task = Raif::Tasks::PdfContentExtraction.run(
|
575
|
+
creator: current_user,
|
576
|
+
files: [file]
|
577
|
+
)
|
578
|
+
```
|
579
|
+
|
580
|
+
To include an image:
|
581
|
+
```ruby
|
582
|
+
image = Raif::ModelImageInput.new(input: "path/to/image.png")
|
583
|
+
|
584
|
+
# Assumes you've created a ImageDescriptionGeneration task
|
585
|
+
task = Raif::Tasks::ImageDescriptionGeneration.run(
|
586
|
+
creator: current_user,
|
587
|
+
images: [image]
|
588
|
+
)
|
589
|
+
```
|
590
|
+
|
591
|
+
|
592
|
+
# Embedding Models
|
593
|
+
|
594
|
+
Raif supports generation of vector embeddings. You can enable and configure embedding models in your Raif configuration:
|
595
|
+
|
596
|
+
```ruby
|
597
|
+
Raif.configure do |config|
|
598
|
+
config.open_ai_embedding_models_enabled = true
|
599
|
+
config.aws_bedrock_titan_embedding_models_enabled = true
|
600
|
+
|
601
|
+
config.default_embedding_model_key = "open_ai_text_embedding_3_small"
|
602
|
+
end
|
603
|
+
```
|
604
|
+
|
605
|
+
## Supported Embedding Models
|
606
|
+
|
607
|
+
Raif currently supports the following embedding models:
|
608
|
+
|
609
|
+
### OpenAI
|
610
|
+
- `open_ai_text_embedding_3_small`
|
611
|
+
- `open_ai_text_embed ding_3_large`
|
612
|
+
- `open_ai_text_embedding_ada_002`
|
613
|
+
|
614
|
+
### AWS Bedrock
|
615
|
+
- `bedrock_titan_embed_text_v2`
|
616
|
+
|
617
|
+
## Creating Embeddings
|
618
|
+
|
619
|
+
By default, Raif will used `Raif.config.default_embedding_model_key` to create embeddings. To create an embedding for a piece of text:
|
620
|
+
|
621
|
+
```ruby
|
622
|
+
# Generate an embedding for a piece of text
|
623
|
+
embedding = Raif.generate_embedding!("Your text here")
|
624
|
+
|
625
|
+
# Generate an embedding for a piece of text with a specific number of dimensions
|
626
|
+
embedding = Raif.generate_embedding!("Your text here", dimensions: 1024)
|
627
|
+
|
628
|
+
# If you're using an OpenAI embedding model, you can pass an array of strings to embed multiple texts at once
|
629
|
+
embeddings = Raif.generate_embedding!([
|
630
|
+
"Your text here",
|
631
|
+
"Your other text here"
|
632
|
+
])
|
633
|
+
```
|
634
|
+
|
635
|
+
Or to generate embeddings for a piece of text with a specific model:
|
636
|
+
|
637
|
+
```ruby
|
638
|
+
model = Raif.embedding_model(:open_ai_text_embedding_3_small)
|
639
|
+
embedding = model.generate_embedding!("Your text here")
|
640
|
+
```
|
641
|
+
|
509
642
|
# Web Admin
|
510
643
|
|
511
644
|
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`.
|
@@ -593,6 +726,32 @@ If you don't want to override the system prompt entirely in your task/conversati
|
|
593
726
|
Raif.configure do |config|
|
594
727
|
config.conversation_system_prompt_intro = "You are a helpful assistant who specializes in customer support."
|
595
728
|
config.task_system_prompt_intro = "You are a helpful assistant who specializes in data analysis."
|
729
|
+
# or with a lambda
|
730
|
+
config.task_system_prompt_intro = ->(task) { "You are a helpful assistant who specializes in #{task.name}." }
|
731
|
+
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')}." }
|
732
|
+
end
|
733
|
+
```
|
734
|
+
|
735
|
+
## Adding LLM Models
|
736
|
+
|
737
|
+
You can easily add new LLM models to Raif:
|
738
|
+
|
739
|
+
```ruby
|
740
|
+
# Register the model in Raif's LLM registry
|
741
|
+
Raif.register_llm(Raif::Llms::OpenRouter, {
|
742
|
+
key: :open_router_gemini_flash_1_5_8b, # a unique key for the model
|
743
|
+
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
|
744
|
+
input_token_cost: 0.038 / 1_000_000, # the cost per input token
|
745
|
+
output_token_cost: 0.15 / 1_000_000, # the cost per output token
|
746
|
+
})
|
747
|
+
|
748
|
+
# Then use the model
|
749
|
+
llm = Raif.llm(:open_router_gemini_flash_1_5_8b)
|
750
|
+
llm.chat(message: "Hello, world!")
|
751
|
+
|
752
|
+
# Or set it as the default LLM model in your initializer
|
753
|
+
Raif.configure do |config|
|
754
|
+
config.default_llm_model_key = "open_router_gemini_flash_1_5_8b"
|
596
755
|
end
|
597
756
|
```
|
598
757
|
|
@@ -15,6 +15,20 @@ module Raif
|
|
15
15
|
end
|
16
16
|
end
|
17
17
|
|
18
|
+
def get_time_range(period)
|
19
|
+
case period
|
20
|
+
when "day"
|
21
|
+
24.hours.ago..Time.current
|
22
|
+
when "week"
|
23
|
+
1.week.ago..Time.current
|
24
|
+
when "month"
|
25
|
+
1.month.ago..Time.current
|
26
|
+
when "all"
|
27
|
+
Time.at(0)..Time.current
|
28
|
+
else
|
29
|
+
24.hours.ago..Time.current
|
30
|
+
end
|
31
|
+
end
|
18
32
|
end
|
19
33
|
end
|
20
34
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Raif
|
4
|
+
module Admin
|
5
|
+
module Stats
|
6
|
+
class TasksController < Raif::Admin::ApplicationController
|
7
|
+
def index
|
8
|
+
@selected_period = params[:period] || "day"
|
9
|
+
@time_range = get_time_range(@selected_period)
|
10
|
+
|
11
|
+
@task_count = Raif::Task.where(created_at: @time_range).count
|
12
|
+
|
13
|
+
# Get task counts by type
|
14
|
+
@task_counts_by_type = Raif::Task.where(created_at: @time_range).group(:type).count
|
15
|
+
|
16
|
+
# Get costs by task type
|
17
|
+
@task_costs_by_type = Raif::Task.joins(:raif_model_completion)
|
18
|
+
.where(created_at: @time_range)
|
19
|
+
.group(:type)
|
20
|
+
.sum("raif_model_completions.total_cost")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Raif
|
4
|
+
module Admin
|
5
|
+
class StatsController < Raif::Admin::ApplicationController
|
6
|
+
def index
|
7
|
+
@selected_period = params[:period] || "day"
|
8
|
+
@time_range = get_time_range(@selected_period)
|
9
|
+
|
10
|
+
@model_completion_count = Raif::ModelCompletion.where(created_at: @time_range).count
|
11
|
+
@model_completion_total_cost = Raif::ModelCompletion.where(created_at: @time_range).sum(:total_cost)
|
12
|
+
@task_count = Raif::Task.where(created_at: @time_range).count
|
13
|
+
@conversation_count = Raif::Conversation.where(created_at: @time_range).count
|
14
|
+
@conversation_entry_count = Raif::ConversationEntry.where(created_at: @time_range).count
|
15
|
+
@agent_count = Raif::Agent.where(created_at: @time_range).count
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -7,10 +7,26 @@ module Raif
|
|
7
7
|
|
8
8
|
def index
|
9
9
|
@task_types = Raif::Task.distinct.pluck(:type)
|
10
|
-
@
|
10
|
+
@selected_type = params[:task_types].present? ? params[:task_types] : "all"
|
11
|
+
|
12
|
+
@task_statuses = [:all, :completed, :failed, :in_progress, :pending]
|
13
|
+
@selected_statuses = params[:task_statuses].present? ? params[:task_statuses].to_sym : :all
|
11
14
|
|
12
15
|
tasks = Raif::Task.order(created_at: :desc)
|
13
|
-
tasks = tasks.where(type: @
|
16
|
+
tasks = tasks.where(type: @selected_type) if @selected_type.present? && @selected_type != "all"
|
17
|
+
|
18
|
+
if @selected_statuses.present? && @selected_statuses != :all
|
19
|
+
case @selected_statuses
|
20
|
+
when :completed
|
21
|
+
tasks = tasks.completed
|
22
|
+
when :failed
|
23
|
+
tasks = tasks.failed
|
24
|
+
when :in_progress
|
25
|
+
tasks = tasks.in_progress
|
26
|
+
when :pending
|
27
|
+
tasks = tasks.pending
|
28
|
+
end
|
29
|
+
end
|
14
30
|
|
15
31
|
@pagy, @tasks = pagy(tasks)
|
16
32
|
end
|
@@ -34,7 +34,11 @@ private
|
|
34
34
|
end
|
35
35
|
|
36
36
|
def validate_conversation_type
|
37
|
-
|
37
|
+
unless Raif.config.conversation_types.include?(conversation_type_param)
|
38
|
+
logger.error("Invalid Raif conversation type - not in Raif.config.conversation_types: #{conversation_type_param}")
|
39
|
+
logger.debug("\n\n\e[33m!!! Make sure to add the conversation type in Raif.config.conversation_types\e[0m\n")
|
40
|
+
head :bad_request
|
41
|
+
end
|
38
42
|
end
|
39
43
|
|
40
44
|
def raif_conversation_type
|
data/app/models/raif/agent.rb
CHANGED
@@ -22,14 +22,12 @@ module Raif
|
|
22
22
|
validates :task, presence: true
|
23
23
|
validates :system_prompt, presence: true
|
24
24
|
validates :max_iterations, presence: true, numericality: { greater_than: 0 }
|
25
|
-
validates :available_model_tools, length: {
|
26
|
-
minimum: 1,
|
27
|
-
message: ->(_object, _data) {
|
28
|
-
I18n.t("raif.agents.errors.available_model_tools.too_short")
|
29
|
-
}
|
30
|
-
}
|
31
25
|
|
32
|
-
before_validation -> {
|
26
|
+
before_validation -> {
|
27
|
+
populate_default_model_tools
|
28
|
+
self.system_prompt ||= build_system_prompt
|
29
|
+
},
|
30
|
+
on: :create
|
33
31
|
|
34
32
|
attr_accessor :on_conversation_history_entry
|
35
33
|
|
@@ -59,7 +57,6 @@ module Raif
|
|
59
57
|
def run!(&block)
|
60
58
|
self.on_conversation_history_entry = block_given? ? block : nil
|
61
59
|
self.started_at = Time.current
|
62
|
-
self.available_model_tools += ["Raif::ModelTools::AgentFinalAnswer"] unless available_model_tools.include?("Raif::ModelTools::AgentFinalAnswer")
|
63
60
|
save!
|
64
61
|
|
65
62
|
logger.debug <<~DEBUG
|
@@ -111,8 +108,12 @@ module Raif
|
|
111
108
|
|
112
109
|
private
|
113
110
|
|
111
|
+
def populate_default_model_tools
|
112
|
+
# no-op by default. Can be overridden by subclasses to add default model tools
|
113
|
+
end
|
114
|
+
|
114
115
|
def process_iteration_model_completion(model_completion)
|
115
|
-
raise NotImplementedError, "#{self.class.name} must implement
|
116
|
+
raise NotImplementedError, "#{self.class.name} must implement process_iteration_model_completion"
|
116
117
|
end
|
117
118
|
|
118
119
|
def native_model_tools
|
@@ -122,6 +123,7 @@ module Raif
|
|
122
123
|
def add_conversation_history_entry(entry)
|
123
124
|
entry_stringified = entry.stringify_keys
|
124
125
|
conversation_history << entry_stringified
|
126
|
+
save!
|
125
127
|
on_conversation_history_entry.call(entry_stringified) if on_conversation_history_entry.present?
|
126
128
|
end
|
127
129
|
|
@@ -4,6 +4,16 @@ module Raif
|
|
4
4
|
module Agents
|
5
5
|
class NativeToolCallingAgent < Raif::Agent
|
6
6
|
validate :ensure_llm_supports_native_tool_use
|
7
|
+
validates :available_model_tools, length: {
|
8
|
+
minimum: 2,
|
9
|
+
message: ->(_object, _data) {
|
10
|
+
I18n.t("raif.agents.native_tool_calling_agent.errors.available_model_tools.too_short")
|
11
|
+
}
|
12
|
+
}
|
13
|
+
|
14
|
+
before_validation -> {
|
15
|
+
available_model_tools << "Raif::ModelTools::AgentFinalAnswer" unless available_model_tools.include?("Raif::ModelTools::AgentFinalAnswer")
|
16
|
+
}
|
7
17
|
|
8
18
|
def build_system_prompt
|
9
19
|
<<~PROMPT.strip
|
@@ -49,7 +59,7 @@ module Raif
|
|
49
59
|
if model_completion.response_tool_calls.blank?
|
50
60
|
add_conversation_history_entry({
|
51
61
|
role: "assistant",
|
52
|
-
content: "<observation>Error: No tool call found. I need make a tool call at each step. Available tools: #{available_model_tools_map.keys.join(", ")}</observation>" # rubocop:disable Layout/LineLength
|
62
|
+
content: "<observation>Error: No tool call found. I need to make a tool call at each step. Available tools: #{available_model_tools_map.keys.join(", ")}</observation>" # rubocop:disable Layout/LineLength
|
53
63
|
})
|
54
64
|
return
|
55
65
|
end
|
@@ -3,6 +3,12 @@
|
|
3
3
|
module Raif
|
4
4
|
module Agents
|
5
5
|
class ReActAgent < Raif::Agent
|
6
|
+
validates :available_model_tools, length: {
|
7
|
+
minimum: 1,
|
8
|
+
message: ->(_object, _data) {
|
9
|
+
I18n.t("raif.agents.re_act_agent.errors.available_model_tools.too_short")
|
10
|
+
}
|
11
|
+
}
|
6
12
|
|
7
13
|
def build_system_prompt
|
8
14
|
<<~PROMPT.strip
|
@@ -4,7 +4,7 @@ module Raif::Concerns::HasAvailableModelTools
|
|
4
4
|
extend ActiveSupport::Concern
|
5
5
|
|
6
6
|
def available_model_tools_map
|
7
|
-
|
7
|
+
available_model_tools&.map do |tool_name|
|
8
8
|
tool_klass = tool_name.is_a?(String) ? tool_name.constantize : tool_name
|
9
9
|
[tool_klass.tool_name, tool_klass]
|
10
10
|
end.to_h
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Raif
|
4
|
+
module Concerns
|
5
|
+
module JsonSchemaDefinition
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
class_methods do
|
9
|
+
def json_schema_definition(schema_name, &block)
|
10
|
+
raise ArgumentError, "A block must be provided to define the JSON schema" unless block_given?
|
11
|
+
|
12
|
+
@schemas ||= {}
|
13
|
+
@schemas[schema_name] = Raif::JsonSchemaBuilder.new
|
14
|
+
@schemas[schema_name].instance_eval(&block)
|
15
|
+
@schemas[schema_name]
|
16
|
+
end
|
17
|
+
|
18
|
+
def schema_defined?(schema_name)
|
19
|
+
@schemas&.dig(schema_name).present?
|
20
|
+
end
|
21
|
+
|
22
|
+
def schema_for(schema_name)
|
23
|
+
@schemas[schema_name].to_schema
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -9,6 +9,25 @@ module Raif::Concerns::LlmResponseParsing
|
|
9
9
|
enum :response_format, Raif::Llm.valid_response_formats, prefix: true
|
10
10
|
|
11
11
|
validates :response_format, presence: true, inclusion: { in: response_formats.keys }
|
12
|
+
|
13
|
+
class_attribute :allowed_tags
|
14
|
+
class_attribute :allowed_attributes
|
15
|
+
end
|
16
|
+
|
17
|
+
class_methods do
|
18
|
+
def llm_response_format(format)
|
19
|
+
raise ArgumentError, "response_format must be one of: #{response_formats.keys.join(", ")}" unless response_formats.keys.include?(format.to_s)
|
20
|
+
|
21
|
+
after_initialize -> { self.response_format = format }, if: :new_record?
|
22
|
+
end
|
23
|
+
|
24
|
+
def llm_response_allowed_tags(tags)
|
25
|
+
self.allowed_tags = tags
|
26
|
+
end
|
27
|
+
|
28
|
+
def llm_response_allowed_attributes(attributes)
|
29
|
+
self.allowed_attributes = attributes
|
30
|
+
end
|
12
31
|
end
|
13
32
|
|
14
33
|
# Parses the response from the LLM into a structured format, based on the response_format.
|
@@ -39,6 +58,9 @@ module Raif::Concerns::LlmResponseParsing
|
|
39
58
|
end
|
40
59
|
end
|
41
60
|
|
42
|
-
|
61
|
+
allowed_tags = self.class.allowed_tags || Rails::HTML5::SafeListSanitizer.allowed_tags
|
62
|
+
allowed_attributes = self.class.allowed_attributes || Rails::HTML5::SafeListSanitizer.allowed_attributes
|
63
|
+
|
64
|
+
ActionController::Base.helpers.sanitize(fragment.to_html, tags: allowed_tags, attributes: allowed_attributes).strip
|
43
65
|
end
|
44
66
|
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Raif::Concerns::LlmTemperature
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
class_attribute :temperature, instance_writer: false
|
8
|
+
end
|
9
|
+
|
10
|
+
class_methods do
|
11
|
+
def llm_temperature(temperature)
|
12
|
+
raise ArgumentError, "temperature must be a number between 0 and 1" unless temperature.is_a?(Numeric) && temperature.between?(0, 1)
|
13
|
+
|
14
|
+
self.temperature = temperature
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|