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.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +200 -41
  3. data/app/assets/stylesheets/raif/admin/stats.scss +12 -0
  4. data/app/controllers/raif/admin/application_controller.rb +14 -0
  5. data/app/controllers/raif/admin/stats/tasks_controller.rb +25 -0
  6. data/app/controllers/raif/admin/stats_controller.rb +19 -0
  7. data/app/controllers/raif/admin/tasks_controller.rb +18 -2
  8. data/app/controllers/raif/conversations_controller.rb +5 -1
  9. data/app/models/raif/agent.rb +11 -9
  10. data/app/models/raif/agents/native_tool_calling_agent.rb +11 -1
  11. data/app/models/raif/agents/re_act_agent.rb +6 -0
  12. data/app/models/raif/concerns/has_available_model_tools.rb +1 -1
  13. data/app/models/raif/concerns/json_schema_definition.rb +28 -0
  14. data/app/models/raif/concerns/llm_response_parsing.rb +23 -1
  15. data/app/models/raif/concerns/llm_temperature.rb +17 -0
  16. data/app/models/raif/concerns/llms/anthropic/message_formatting.rb +51 -0
  17. data/app/models/raif/concerns/llms/bedrock_claude/message_formatting.rb +70 -0
  18. data/app/models/raif/concerns/llms/message_formatting.rb +41 -0
  19. data/app/models/raif/concerns/llms/open_ai/message_formatting.rb +41 -0
  20. data/app/models/raif/conversation.rb +11 -3
  21. data/app/models/raif/conversation_entry.rb +22 -6
  22. data/app/models/raif/embedding_model.rb +22 -0
  23. data/app/models/raif/embedding_models/bedrock_titan.rb +34 -0
  24. data/app/models/raif/embedding_models/open_ai.rb +40 -0
  25. data/app/models/raif/llm.rb +39 -6
  26. data/app/models/raif/llms/anthropic.rb +23 -28
  27. data/app/models/raif/llms/bedrock_claude.rb +33 -19
  28. data/app/models/raif/llms/open_ai.rb +14 -17
  29. data/app/models/raif/llms/open_router.rb +93 -0
  30. data/app/models/raif/model_completion.rb +21 -2
  31. data/app/models/raif/model_file_input.rb +113 -0
  32. data/app/models/raif/model_image_input.rb +4 -0
  33. data/app/models/raif/model_tool.rb +77 -51
  34. data/app/models/raif/model_tool_invocation.rb +8 -6
  35. data/app/models/raif/model_tools/agent_final_answer.rb +18 -27
  36. data/app/models/raif/model_tools/fetch_url.rb +27 -36
  37. data/app/models/raif/model_tools/wikipedia_search.rb +46 -55
  38. data/app/models/raif/task.rb +71 -16
  39. data/app/views/layouts/raif/admin.html.erb +10 -0
  40. data/app/views/raif/admin/agents/show.html.erb +3 -1
  41. data/app/views/raif/admin/conversations/_conversation.html.erb +1 -1
  42. data/app/views/raif/admin/conversations/show.html.erb +3 -1
  43. data/app/views/raif/admin/model_completions/_model_completion.html.erb +1 -0
  44. data/app/views/raif/admin/model_completions/index.html.erb +1 -0
  45. data/app/views/raif/admin/model_completions/show.html.erb +30 -3
  46. data/app/views/raif/admin/stats/index.html.erb +128 -0
  47. data/app/views/raif/admin/stats/tasks/index.html.erb +45 -0
  48. data/app/views/raif/admin/tasks/_task.html.erb +5 -4
  49. data/app/views/raif/admin/tasks/index.html.erb +20 -2
  50. data/app/views/raif/admin/tasks/show.html.erb +3 -1
  51. data/app/views/raif/conversation_entries/_conversation_entry.html.erb +18 -14
  52. data/app/views/raif/conversation_entries/_form.html.erb +1 -1
  53. data/app/views/raif/conversation_entries/_form_with_available_tools.html.erb +4 -4
  54. data/app/views/raif/conversation_entries/_message.html.erb +10 -3
  55. data/config/locales/admin.en.yml +14 -0
  56. data/config/locales/en.yml +25 -3
  57. data/config/routes.rb +6 -0
  58. data/db/migrate/20250421202149_add_response_format_to_raif_conversations.rb +7 -0
  59. data/db/migrate/20250424200755_add_cost_columns_to_raif_model_completions.rb +14 -0
  60. data/db/migrate/20250424232946_add_created_at_indexes.rb +11 -0
  61. data/db/migrate/20250502155330_add_status_indexes_to_raif_tasks.rb +14 -0
  62. data/db/migrate/20250507155314_add_retry_count_to_raif_model_completions.rb +7 -0
  63. data/lib/generators/raif/agent/agent_generator.rb +22 -12
  64. data/lib/generators/raif/agent/templates/agent.rb.tt +3 -3
  65. data/lib/generators/raif/agent/templates/application_agent.rb.tt +7 -0
  66. data/lib/generators/raif/conversation/conversation_generator.rb +10 -0
  67. data/lib/generators/raif/conversation/templates/application_conversation.rb.tt +7 -0
  68. data/lib/generators/raif/conversation/templates/conversation.rb.tt +13 -11
  69. data/lib/generators/raif/install/templates/initializer.rb +50 -6
  70. data/lib/generators/raif/model_tool/model_tool_generator.rb +0 -5
  71. data/lib/generators/raif/model_tool/templates/model_tool.rb.tt +69 -56
  72. data/lib/generators/raif/task/templates/task.rb.tt +34 -23
  73. data/lib/raif/configuration.rb +40 -3
  74. data/lib/raif/embedding_model_registry.rb +83 -0
  75. data/lib/raif/engine.rb +34 -1
  76. data/lib/raif/errors/{open_ai/api_error.rb → invalid_model_file_input_error.rb} +1 -3
  77. data/lib/raif/errors/{anthropic/api_error.rb → invalid_model_image_input_error.rb} +1 -3
  78. data/lib/raif/errors/unsupported_feature_error.rb +8 -0
  79. data/lib/raif/errors.rb +3 -2
  80. data/lib/raif/json_schema_builder.rb +104 -0
  81. data/lib/raif/llm_registry.rb +205 -0
  82. data/lib/raif/version.rb +1 -1
  83. data/lib/raif.rb +5 -32
  84. data/lib/tasks/raif_tasks.rake +9 -4
  85. metadata +32 -19
  86. data/lib/raif/default_llms.rb +0 -37
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 232a25681c254ff213ce9565506bffc805df041361e145016eee17f0ca3514f3
4
- data.tar.gz: 24c54f8c2d5ea95664a55a57b7ed8c3e22d3040f79f28311eb60083aa2d9f47e
3
+ metadata.gz: 1af0a7a003990a40716f3d07ec87838e5fb485147bb87a9b35ae1fe23f642501
4
+ data.tar.gz: b82bba67ed23a6e47e1bd02f5c64f9448c7374017c097a012ccdae2e3795be34
5
5
  SHA512:
6
- metadata.gz: c07aa30bcc9b040f75876124fec474bf48f13222ccbb053b7f0dba2313dbd459965d37fcd2b305d399dd503495cfadc2ca4f2b5c8e48f604528f6555d46e404e
7
- data.tar.gz: 9d33cca29d90ed9dda3d7fd93c988b4b48999db4394be62044350648f11083ed03138201f76bb16569fbad6c61c2ff8b917af47d1f4a6e687c527bb9cd32b4b0
6
+ metadata.gz: d9dd7273eeccb284d7ee720fe586dc1e86b6e3355da378a719310cbe3209c508a57a3ee1cfcaa60762cd2860f76960b4f89aa39d7f5eafa90eb97b78adab6123
7
+ data.tar.gz: 26013bb1beb60367d878b451163594c50e36ef046b8ae0864a4728c4d0ae862e79ec22ab5ccb6c0acd88991b9f227b3af4b31daedc76e90755e9d87bce45f25c
data/README.md CHANGED
@@ -6,7 +6,7 @@
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), and [AWS Bedrock](#aws-bedrock).
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 (see the end of this section for an example of using the task generator), where you'll define the prompt and response format for the task and call via `Raif::Task.run`. For example, say you have a `Document` model in your app and want to have a summarization task for the LLM:
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
- def self.json_response_schema
233
- {
234
- type: "object",
235
- additionalProperties: false,
236
- required: ["queries"],
237
- properties: {
238
- queries: {
239
- type: "array",
240
- items: {
241
- type: "string"
242
- }
243
- }
244
- }
245
- }
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/wikipedia_search_tool.rb
438
- # Fetch URL Tool: https://github.com/CultivateLabs/raif/blob/main/app/models/raif/model_tools/fetch_url_tool.rb
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
- def self.example_model_invocation
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
- # Define your tool's argument schema here. It should be a valid JSON schema.
450
- # When the model invokes your tool, the arguments it provides will be validated
451
- # against this schema using JSON::Validator from the json-schema gem.
452
- def self.tool_arguments_schema
453
- # For example:
454
- # {
455
- # type: "object",
456
- # additionalProperties: false,
457
- # required: ["query"],
458
- # properties: {
459
- # query: {
460
- # type: "string",
461
- # description: "The query to search for"
462
- # }
463
- # }
464
- # }
465
- # Would expect the model to invoke your tool with an arguments JSON object like:
466
- # { "query" : "some query here" }
467
- end
468
-
469
- def self.tool_description
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
 
@@ -0,0 +1,12 @@
1
+ .stats-icon {
2
+ min-width: 46px;
3
+ text-align: center;
4
+ }
5
+
6
+ .stats-card {
7
+ transition: transform 0.2s;
8
+ }
9
+
10
+ .stats-card:hover {
11
+ transform: translateY(-5px);
12
+ }
@@ -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
- @selected_types = params[:task_types] || []
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: @selected_types) if @selected_types.present?
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
- head :bad_request unless Raif.config.conversation_types.include?(conversation_type_param)
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
@@ -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 -> { self.system_prompt ||= build_system_prompt }, on: :create
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 execute_agent_iteration"
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
- @available_model_tools_map ||= available_model_tools&.map do |tool_name|
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
- ActionController::Base.helpers.sanitize(fragment.to_html).strip
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