raif 1.1.0 → 1.2.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 +148 -4
- data/app/assets/builds/raif.css +26 -1
- data/app/assets/stylesheets/raif/loader.scss +27 -1
- data/app/models/raif/concerns/llm_response_parsing.rb +22 -16
- data/app/models/raif/concerns/llms/anthropic/tool_formatting.rb +56 -0
- data/app/models/raif/concerns/llms/{bedrock_claude → bedrock}/message_formatting.rb +4 -4
- data/app/models/raif/concerns/llms/bedrock/tool_formatting.rb +37 -0
- data/app/models/raif/concerns/llms/message_formatting.rb +7 -6
- data/app/models/raif/concerns/llms/open_ai/json_schema_validation.rb +138 -0
- data/app/models/raif/concerns/llms/{open_ai → open_ai_completions}/message_formatting.rb +1 -1
- data/app/models/raif/concerns/llms/open_ai_completions/tool_formatting.rb +26 -0
- data/app/models/raif/concerns/llms/open_ai_responses/message_formatting.rb +43 -0
- data/app/models/raif/concerns/llms/open_ai_responses/tool_formatting.rb +42 -0
- data/app/models/raif/conversation.rb +17 -4
- data/app/models/raif/conversation_entry.rb +18 -2
- data/app/models/raif/embedding_models/{bedrock_titan.rb → bedrock.rb} +2 -2
- data/app/models/raif/llm.rb +73 -7
- data/app/models/raif/llms/anthropic.rb +56 -36
- data/app/models/raif/llms/{bedrock_claude.rb → bedrock.rb} +62 -45
- data/app/models/raif/llms/open_ai_base.rb +66 -0
- data/app/models/raif/llms/open_ai_completions.rb +100 -0
- data/app/models/raif/llms/open_ai_responses.rb +144 -0
- data/app/models/raif/llms/open_router.rb +38 -43
- data/app/models/raif/model_completion.rb +2 -0
- data/app/models/raif/model_tool.rb +4 -0
- data/app/models/raif/model_tools/provider_managed/base.rb +9 -0
- data/app/models/raif/model_tools/provider_managed/code_execution.rb +5 -0
- data/app/models/raif/model_tools/provider_managed/image_generation.rb +5 -0
- data/app/models/raif/model_tools/provider_managed/web_search.rb +5 -0
- data/app/models/raif/streaming_responses/anthropic.rb +63 -0
- data/app/models/raif/streaming_responses/bedrock.rb +89 -0
- data/app/models/raif/streaming_responses/open_ai_completions.rb +76 -0
- data/app/models/raif/streaming_responses/open_ai_responses.rb +54 -0
- data/app/views/raif/admin/conversations/_conversation_entry.html.erb +48 -0
- data/app/views/raif/admin/conversations/show.html.erb +1 -1
- data/app/views/raif/admin/model_completions/_model_completion.html.erb +7 -0
- data/app/views/raif/admin/model_completions/index.html.erb +1 -0
- data/app/views/raif/admin/model_completions/show.html.erb +28 -0
- data/app/views/raif/conversation_entries/_citations.html.erb +9 -0
- data/app/views/raif/conversation_entries/_conversation_entry.html.erb +5 -1
- data/app/views/raif/conversation_entries/_message.html.erb +4 -0
- data/config/locales/admin.en.yml +2 -0
- data/config/locales/en.yml +22 -0
- data/db/migrate/20250224234252_create_raif_tables.rb +1 -1
- data/db/migrate/20250421202149_add_response_format_to_raif_conversations.rb +1 -1
- data/db/migrate/20250424200755_add_cost_columns_to_raif_model_completions.rb +1 -1
- data/db/migrate/20250424232946_add_created_at_indexes.rb +1 -1
- data/db/migrate/20250502155330_add_status_indexes_to_raif_tasks.rb +1 -1
- data/db/migrate/20250527213016_add_response_id_and_response_array_to_model_completions.rb +14 -0
- data/db/migrate/20250603140622_add_citations_to_raif_model_completions.rb +13 -0
- data/db/migrate/20250603202013_add_stream_response_to_raif_model_completions.rb +7 -0
- data/lib/generators/raif/conversation/templates/conversation.rb.tt +3 -3
- data/lib/generators/raif/install/templates/initializer.rb +14 -2
- data/lib/raif/configuration.rb +27 -5
- data/lib/raif/embedding_model_registry.rb +1 -1
- data/lib/raif/engine.rb +25 -9
- data/lib/raif/errors/streaming_error.rb +18 -0
- data/lib/raif/errors.rb +1 -0
- data/lib/raif/llm_registry.rb +157 -47
- data/lib/raif/migration_checker.rb +74 -0
- data/lib/raif/utils/html_fragment_processor.rb +169 -0
- data/lib/raif/utils.rb +1 -0
- data/lib/raif/version.rb +1 -1
- data/lib/raif.rb +2 -0
- metadata +45 -8
- data/app/models/raif/llms/open_ai.rb +0 -256
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 93d75d28d64da2a5559de740890291cb3a125d981bb58aa56ff4c45be0169954
|
4
|
+
data.tar.gz: 9f3ca2e5142f43be17c9fe27e3572cd08a4241e72d6e43893b74e6ba954169ae
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 90726f18f49a312f0e8d2ceb3913c1bb45c50ad237f0fceeefa825e28374214a8c61ae84c4a83e6d3188a27adea3dc0162001792d867babadca5e30f5f736398
|
7
|
+
data.tar.gz: e053003fd9c510509cbc749d5f20d9d2596396224499fe7cc40467920a216f4da6efb3c281ed451ff7e29bced98ff17476c4779b7760a3e361c4a09285667deb
|
data/README.md
CHANGED
@@ -13,16 +13,21 @@ Raif is built by [Cultivate Labs](https://www.cultivatelabs.com) and is used to
|
|
13
13
|
## Table of Contents
|
14
14
|
- [Setup](#setup)
|
15
15
|
- [OpenAI](#openai)
|
16
|
+
- [OpenAI Completions API](#openai-completions-api)
|
17
|
+
- [OpenAI Responses API](#openai-responses-api)
|
16
18
|
- [Anthropic Claude](#anthropic-claude)
|
17
19
|
- [AWS Bedrock (Claude)](#aws-bedrock-claude)
|
18
20
|
- [OpenRouter](#openrouter)
|
19
21
|
- [Chatting with the LLM](#chatting-with-the-llm)
|
22
|
+
- [Streaming Responses](#streaming-responses)
|
20
23
|
- [Key Raif Concepts](#key-raif-concepts)
|
21
24
|
- [Tasks](#tasks)
|
22
25
|
- [Conversations](#conversations)
|
26
|
+
- [Real-time Streaming Responses](#real-time-streaming-responses)
|
23
27
|
- [Conversation Types](#conversation-types)
|
24
28
|
- [Agents](#agents)
|
25
29
|
- [Model Tools](#model-tools)
|
30
|
+
- [Provider-Managed Tools](#provider-managed-tools)
|
26
31
|
- [Images/Files/PDF's](#imagesfilespdfs)
|
27
32
|
- [Images/Files/PDF's in Tasks](#imagesfilespdfs-in-tasks)
|
28
33
|
- [Embedding Models](#embedding-models)
|
@@ -35,6 +40,7 @@ Raif is built by [Cultivate Labs](https://www.cultivatelabs.com) and is used to
|
|
35
40
|
- [Adding LLM Models](#adding-llm-models)
|
36
41
|
- [Testing](#testing)
|
37
42
|
- [Demo App](#demo-app)
|
43
|
+
- [Contributing](#contributing)
|
38
44
|
- [License](#license)
|
39
45
|
|
40
46
|
# Setup
|
@@ -60,6 +66,8 @@ This will:
|
|
60
66
|
- Copy Raif's database migrations to your application
|
61
67
|
- Mount Raif's engine at `/raif` in your application's `config/routes.rb` file
|
62
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
|
+
|
63
71
|
Run the migrations. Raif is compatible with both PostgreSQL and MySQL databases.
|
64
72
|
```bash
|
65
73
|
rails db:migrate
|
@@ -82,6 +90,10 @@ end
|
|
82
90
|
Configure your LLM providers. You'll need at least one of:
|
83
91
|
|
84
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
|
85
97
|
```ruby
|
86
98
|
Raif.configure do |config|
|
87
99
|
config.open_ai_models_enabled = true
|
@@ -90,10 +102,44 @@ Raif.configure do |config|
|
|
90
102
|
end
|
91
103
|
```
|
92
104
|
|
93
|
-
Currently supported OpenAI models:
|
105
|
+
Currently supported OpenAI Completions API models:
|
94
106
|
- `open_ai_gpt_4o_mini`
|
95
107
|
- `open_ai_gpt_4o`
|
96
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.
|
97
143
|
|
98
144
|
## Anthropic Claude
|
99
145
|
```ruby
|
@@ -110,10 +156,12 @@ Currently supported Anthropic models:
|
|
110
156
|
- `anthropic_claude_3_5_haiku`
|
111
157
|
- `anthropic_claude_3_opus`
|
112
158
|
|
159
|
+
The Anthropic adapter provides access to [provider-managed tools](#provider-managed-tools) for web search and code execution.
|
160
|
+
|
113
161
|
## AWS Bedrock (Claude)
|
114
162
|
```ruby
|
115
163
|
Raif.configure do |config|
|
116
|
-
config.
|
164
|
+
config.bedrock_models_enabled = true
|
117
165
|
config.aws_bedrock_region = "us-east-1"
|
118
166
|
config.default_llm_model_key = "bedrock_claude_3_5_sonnet"
|
119
167
|
end
|
@@ -124,6 +172,9 @@ Currently supported Bedrock models:
|
|
124
172
|
- `bedrock_claude_3_7_sonnet`
|
125
173
|
- `bedrock_claude_3_5_haiku`
|
126
174
|
- `bedrock_claude_3_opus`
|
175
|
+
- `bedrock_amazon_nova_micro`
|
176
|
+
- `bedrock_amazon_nova_lite`
|
177
|
+
- `bedrock_amazon_nova_pro`
|
127
178
|
|
128
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.)
|
129
180
|
|
@@ -184,6 +235,38 @@ puts model_completion.parsed_response # will strip backticks, parse the JSON, an
|
|
184
235
|
# => {"joke" => "Why don't skeletons fight each other? They don't have the guts."}
|
185
236
|
```
|
186
237
|
|
238
|
+
## Streaming Responses
|
239
|
+
|
240
|
+
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:
|
241
|
+
|
242
|
+
```ruby
|
243
|
+
llm = Raif.llm(:open_ai_gpt_4o)
|
244
|
+
model_completion = llm.chat(message: "Tell me a story") do |model_completion, delta, sse_event|
|
245
|
+
# This block is called multiple times as the response streams in.
|
246
|
+
# You could broadcast these updates via Turbo Streams, WebSockets, etc.
|
247
|
+
Turbo::StreamsChannel.broadcast_replace_to(
|
248
|
+
:my_channel,
|
249
|
+
target: "chat-response",
|
250
|
+
partial: "my_partial_displaying_chat_response",
|
251
|
+
locals: { model_completion: model_completion, delta: delta, sse_event: sse_event }
|
252
|
+
)
|
253
|
+
end
|
254
|
+
|
255
|
+
# The final complete response is available in the model_completion
|
256
|
+
puts model_completion.raw_response
|
257
|
+
```
|
258
|
+
|
259
|
+
You can configure the streaming update frequency by adjusting the chunk size threshold in your Raif configuration:
|
260
|
+
|
261
|
+
```ruby
|
262
|
+
Raif.configure do |config|
|
263
|
+
# Control how often the model completion is updated & the block is called when streaming.
|
264
|
+
# Lower values = more frequent updates but more database writes.
|
265
|
+
# Higher values = less frequent updates but fewer database writes.
|
266
|
+
config.streaming_update_chunk_size_threshold = 50 # default is 25
|
267
|
+
end
|
268
|
+
```
|
269
|
+
|
187
270
|
# Key Raif Concepts
|
188
271
|
|
189
272
|
## Tasks
|
@@ -334,6 +417,10 @@ If your app already includes Bootstrap styles, this will render a conversation i
|
|
334
417
|
|
335
418
|
If your app does not include Bootstrap, you can [override the views](#views) to update styles.
|
336
419
|
|
420
|
+
### Real-time Streaming Responses
|
421
|
+
|
422
|
+
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.
|
423
|
+
|
337
424
|
### Conversation Types
|
338
425
|
|
339
426
|
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:
|
@@ -520,7 +607,54 @@ class Raif::ModelTools::GoogleSearch < Raif::ModelTool
|
|
520
607
|
end
|
521
608
|
```
|
522
609
|
|
523
|
-
|
610
|
+
### Provider-Managed Tools
|
611
|
+
|
612
|
+
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:
|
613
|
+
|
614
|
+
- **`Raif::ModelTools::ProviderManaged::WebSearch`**: Performs real-time web searches and returns relevant results
|
615
|
+
- **`Raif::ModelTools::ProviderManaged::CodeExecution`**: Executes code in a secure sandboxed environment (e.g. Python)
|
616
|
+
- **`Raif::ModelTools::ProviderManaged::ImageGeneration`**: Generates images based on text descriptions
|
617
|
+
|
618
|
+
Current provider-managed tool support:
|
619
|
+
| Provider | WebSearch | CodeExecution | ImageGeneration |
|
620
|
+
|----------|-----------|---------------|-----------------|
|
621
|
+
| OpenAI Responses API | ✅ | ✅ | ✅ |
|
622
|
+
| OpenAI Completions API | ❌ | ❌ | ❌ |
|
623
|
+
| Anthropic Claude | ✅ | ✅ | ❌ |
|
624
|
+
| AWS Bedrock (Claude) | ❌ | ❌ | ❌ |
|
625
|
+
| OpenRouter | ❌ | ❌ | ❌ |
|
626
|
+
|
627
|
+
To use provider-managed tools, include them in the `available_model_tools` array:
|
628
|
+
|
629
|
+
```ruby
|
630
|
+
# In a conversation
|
631
|
+
conversation = Raif::Conversation.create!(
|
632
|
+
creator: current_user,
|
633
|
+
available_model_tools: [
|
634
|
+
"Raif::ModelTools::ProviderManaged::WebSearch",
|
635
|
+
"Raif::ModelTools::ProviderManaged::CodeExecution"
|
636
|
+
]
|
637
|
+
)
|
638
|
+
|
639
|
+
# In an agent
|
640
|
+
agent = Raif::Agents::ReActAgent.new(
|
641
|
+
task: "Search for recent news about AI and create a summary chart",
|
642
|
+
available_model_tools: [
|
643
|
+
"Raif::ModelTools::ProviderManaged::WebSearch",
|
644
|
+
"Raif::ModelTools::ProviderManaged::CodeExecution"
|
645
|
+
],
|
646
|
+
creator: current_user
|
647
|
+
)
|
648
|
+
|
649
|
+
# Directly in a chat
|
650
|
+
llm = Raif.llm(:open_ai_responses_gpt_4_1)
|
651
|
+
model_completion = llm.chat(
|
652
|
+
messages: [{ role: "user", content: "What are the latest developments in Ruby on Rails?" }],
|
653
|
+
available_model_tools: [Raif::ModelTools::ProviderManaged::WebSearch]
|
654
|
+
)
|
655
|
+
```
|
656
|
+
|
657
|
+
## Sending Images/Files/PDF's to the LLM
|
524
658
|
|
525
659
|
Raif supports images, files, and PDF's in the messages sent to the LLM.
|
526
660
|
|
@@ -596,7 +730,7 @@ Raif supports generation of vector embeddings. You can enable and configure embe
|
|
596
730
|
```ruby
|
597
731
|
Raif.configure do |config|
|
598
732
|
config.open_ai_embedding_models_enabled = true
|
599
|
-
config.
|
733
|
+
config.bedrock_embedding_models_enabled = true
|
600
734
|
|
601
735
|
config.default_embedding_model_key = "open_ai_text_embedding_3_small"
|
602
736
|
end
|
@@ -649,6 +783,7 @@ The admin interface contains sections for:
|
|
649
783
|
- Conversations
|
650
784
|
- Agents
|
651
785
|
- Model Tool Invocations
|
786
|
+
- Stats
|
652
787
|
|
653
788
|
|
654
789
|
### Model Completions
|
@@ -670,6 +805,9 @@ The admin interface contains sections for:
|
|
670
805
|

|
671
806
|

|
672
807
|
|
808
|
+
### Stats
|
809
|
+

|
810
|
+
|
673
811
|
# Customization
|
674
812
|
|
675
813
|
## Controllers
|
@@ -832,6 +970,12 @@ You can then access the app at [http://localhost:3000](http://localhost:3000).
|
|
832
970
|
|
833
971
|

|
834
972
|
|
973
|
+
# Contributing
|
974
|
+
|
975
|
+
We welcome contributions to Raif! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
|
976
|
+
|
977
|
+
**Important**: All PR's should be made against the `dev` branch.
|
978
|
+
|
835
979
|
# License
|
836
980
|
|
837
981
|
The gem is available as open source under the terms of the MIT License.
|
data/app/assets/builds/raif.css
CHANGED
@@ -28,6 +28,31 @@
|
|
28
28
|
animation-delay: 0.4s;
|
29
29
|
}
|
30
30
|
|
31
|
+
.raif-streaming-cursor {
|
32
|
+
display: inline-block;
|
33
|
+
width: 2px;
|
34
|
+
height: 1.1em;
|
35
|
+
margin-bottom: -2px;
|
36
|
+
background-color: currentColor;
|
37
|
+
animation: blink 1s infinite;
|
38
|
+
transform: none;
|
39
|
+
border-radius: 0;
|
40
|
+
position: relative;
|
41
|
+
}
|
42
|
+
|
43
|
+
.raif-streaming-cursor:before,
|
44
|
+
.raif-streaming-cursor:after {
|
45
|
+
display: none;
|
46
|
+
}
|
47
|
+
|
48
|
+
@keyframes blink {
|
49
|
+
0%, 50% {
|
50
|
+
opacity: 1;
|
51
|
+
}
|
52
|
+
51%, 100% {
|
53
|
+
opacity: 0;
|
54
|
+
}
|
55
|
+
}
|
31
56
|
@keyframes rotate {
|
32
57
|
0% {
|
33
58
|
transform: translate(-50%, -50%) rotateZ(0deg);
|
@@ -71,4 +96,4 @@
|
|
71
96
|
}
|
72
97
|
}
|
73
98
|
|
74
|
-
/*# sourceMappingURL=data:application/json;base64,
|
99
|
+
/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInJhaWYuY3NzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBO0VBQ0UseUJBQXlCO0VBQ3pCLG1CQUFtQjtFQUNuQixrQkFBa0I7RUFDbEIsV0FBVztFQUNYLFlBQVk7RUFDWixjQUFjO0VBQ2QscUJBQXFCO0FBQ3ZCOztBQUVBOztFQUVFLFdBQVc7RUFDWCxjQUFjO0VBQ2Qsa0JBQWtCO0VBQ2xCLE1BQU07RUFDTixPQUFPO0VBQ1AsY0FBYztFQUNkLGVBQWU7RUFDZixrQkFBa0I7RUFDbEIseUJBQXlCO0VBQ3pCLGtDQUFrQztBQUNwQzs7QUFFQTtFQUNFLGNBQWM7RUFDZCx5QkFBeUI7RUFDekIscUJBQXFCO0FBQ3ZCOztBQUVBO0VBQ0UscUJBQXFCO0VBQ3JCLFVBQVU7RUFDVixhQUFhO0VBQ2IsbUJBQW1CO0VBQ25CLDhCQUE4QjtFQUM5Qiw0QkFBNEI7RUFDNUIsZUFBZTtFQUNmLGdCQUFnQjtFQUNoQixrQkFBa0I7QUFDcEI7O0FBRUE7O0VBRUUsYUFBYTtBQUNmOztBQUVBO0VBQ0U7SUFDRSxVQUFVO0VBQ1o7RUFDQTtJQUNFLFVBQVU7RUFDWjtBQUNGO0FBQ0E7RUFDRTtJQUNFLDhDQUE4QztFQUNoRDtFQUNBO0lBQ0UsZ0RBQWdEO0VBQ2xEO0FBQ0Y7QUFDQTtFQUNFO0lBQ0UsNkNBQTZDO0VBQy9DO0VBQ0E7SUFDRSxnREFBZ0Q7RUFDbEQ7QUFDRjtBQUNBO0VBQ0U7SUFDRSx3Q0FBd0M7RUFDMUM7RUFDQTtJQUNFLHdDQUF3QztFQUMxQztFQUNBO0lBQ0Usc0NBQXNDO0VBQ3hDO0VBQ0E7SUFDRSx5Q0FBeUM7RUFDM0M7RUFDQTtJQUNFLHFDQUFxQztFQUN2QztFQUNBO0lBQ0UsMENBQTBDO0VBQzVDO0VBQ0E7SUFDRSx1Q0FBdUM7RUFDekM7RUFDQTtJQUNFLHlDQUF5QztFQUMzQztBQUNGIiwiZmlsZSI6InJhaWYuY3NzIiwic291cmNlc0NvbnRlbnQiOlsiLnJhaWYtbG9hZGVyIHtcbiAgdHJhbnNmb3JtOiByb3RhdGVaKDQ1ZGVnKTtcbiAgcGVyc3BlY3RpdmU6IDEwMDBweDtcbiAgYm9yZGVyLXJhZGl1czogNTAlO1xuICB3aWR0aDogMjVweDtcbiAgaGVpZ2h0OiAyNXB4O1xuICBjb2xvcjogIzM4NzRmZjtcbiAgZGlzcGxheTogaW5saW5lLWJsb2NrO1xufVxuXG4ucmFpZi1sb2FkZXI6YmVmb3JlLFxuLnJhaWYtbG9hZGVyOmFmdGVyIHtcbiAgY29udGVudDogXCJcIjtcbiAgZGlzcGxheTogYmxvY2s7XG4gIHBvc2l0aW9uOiBhYnNvbHV0ZTtcbiAgdG9wOiAwO1xuICBsZWZ0OiAwO1xuICB3aWR0aDogaW5oZXJpdDtcbiAgaGVpZ2h0OiBpbmhlcml0O1xuICBib3JkZXItcmFkaXVzOiA1MCU7XG4gIHRyYW5zZm9ybTogcm90YXRlWCg3MGRlZyk7XG4gIGFuaW1hdGlvbjogMXMgc3BpbiBsaW5lYXIgaW5maW5pdGU7XG59XG5cbi5yYWlmLWxvYWRlcjphZnRlciB7XG4gIGNvbG9yOiAjMjViMDAzO1xuICB0cmFuc2Zvcm06IHJvdGF0ZVkoNzBkZWcpO1xuICBhbmltYXRpb24tZGVsYXk6IDAuNHM7XG59XG5cbi5yYWlmLXN0cmVhbWluZy1jdXJzb3Ige1xuICBkaXNwbGF5OiBpbmxpbmUtYmxvY2s7XG4gIHdpZHRoOiAycHg7XG4gIGhlaWdodDogMS4xZW07XG4gIG1hcmdpbi1ib3R0b206IC0ycHg7XG4gIGJhY2tncm91bmQtY29sb3I6IGN1cnJlbnRDb2xvcjtcbiAgYW5pbWF0aW9uOiBibGluayAxcyBpbmZpbml0ZTtcbiAgdHJhbnNmb3JtOiBub25lO1xuICBib3JkZXItcmFkaXVzOiAwO1xuICBwb3NpdGlvbjogcmVsYXRpdmU7XG59XG5cbi5yYWlmLXN0cmVhbWluZy1jdXJzb3I6YmVmb3JlLFxuLnJhaWYtc3RyZWFtaW5nLWN1cnNvcjphZnRlciB7XG4gIGRpc3BsYXk6IG5vbmU7XG59XG5cbkBrZXlmcmFtZXMgYmxpbmsge1xuICAwJSwgNTAlIHtcbiAgICBvcGFjaXR5OiAxO1xuICB9XG4gIDUxJSwgMTAwJSB7XG4gICAgb3BhY2l0eTogMDtcbiAgfVxufVxuQGtleWZyYW1lcyByb3RhdGUge1xuICAwJSB7XG4gICAgdHJhbnNmb3JtOiB0cmFuc2xhdGUoLTUwJSwgLTUwJSkgcm90YXRlWigwZGVnKTtcbiAgfVxuICAxMDAlIHtcbiAgICB0cmFuc2Zvcm06IHRyYW5zbGF0ZSgtNTAlLCAtNTAlKSByb3RhdGVaKDM2MGRlZyk7XG4gIH1cbn1cbkBrZXlmcmFtZXMgcm90YXRlY2N3IHtcbiAgMCUge1xuICAgIHRyYW5zZm9ybTogdHJhbnNsYXRlKC01MCUsIC01MCUpIHJvdGF0ZSgwZGVnKTtcbiAgfVxuICAxMDAlIHtcbiAgICB0cmFuc2Zvcm06IHRyYW5zbGF0ZSgtNTAlLCAtNTAlKSByb3RhdGUoLTM2MGRlZyk7XG4gIH1cbn1cbkBrZXlmcmFtZXMgc3BpbiB7XG4gIDAlLCAxMDAlIHtcbiAgICBib3gtc2hhZG93OiAwLjNlbSAwcHggMCAwcHggY3VycmVudGNvbG9yO1xuICB9XG4gIDEyJSB7XG4gICAgYm94LXNoYWRvdzogMC4zZW0gMC4zZW0gMCAwIGN1cnJlbnRjb2xvcjtcbiAgfVxuICAyNSUge1xuICAgIGJveC1zaGFkb3c6IDAgMC4zZW0gMCAwcHggY3VycmVudGNvbG9yO1xuICB9XG4gIDM3JSB7XG4gICAgYm94LXNoYWRvdzogLTAuM2VtIDAuM2VtIDAgMCBjdXJyZW50Y29sb3I7XG4gIH1cbiAgNTAlIHtcbiAgICBib3gtc2hhZG93OiAtMC4zZW0gMCAwIDAgY3VycmVudGNvbG9yO1xuICB9XG4gIDYyJSB7XG4gICAgYm94LXNoYWRvdzogLTAuM2VtIC0wLjNlbSAwIDAgY3VycmVudGNvbG9yO1xuICB9XG4gIDc1JSB7XG4gICAgYm94LXNoYWRvdzogMHB4IC0wLjNlbSAwIDAgY3VycmVudGNvbG9yO1xuICB9XG4gIDg3JSB7XG4gICAgYm94LXNoYWRvdzogMC4zZW0gLTAuM2VtIDAgMCBjdXJyZW50Y29sb3I7XG4gIH1cbn1cbiJdfQ== */
|
@@ -28,6 +28,33 @@
|
|
28
28
|
animation-delay: .4s;
|
29
29
|
}
|
30
30
|
|
31
|
+
// Streaming cursor - a simple blinking cursor
|
32
|
+
.raif-streaming-cursor {
|
33
|
+
display: inline-block;
|
34
|
+
width: 2px;
|
35
|
+
height: 1.1em;
|
36
|
+
margin-bottom: -2px;
|
37
|
+
background-color: currentColor;
|
38
|
+
animation: blink 1s infinite;
|
39
|
+
transform: none;
|
40
|
+
border-radius: 0;
|
41
|
+
position: relative;
|
42
|
+
}
|
43
|
+
|
44
|
+
.raif-streaming-cursor:before,
|
45
|
+
.raif-streaming-cursor:after {
|
46
|
+
display: none;
|
47
|
+
}
|
48
|
+
|
49
|
+
@keyframes blink {
|
50
|
+
0%, 50% {
|
51
|
+
opacity: 1;
|
52
|
+
}
|
53
|
+
51%, 100% {
|
54
|
+
opacity: 0;
|
55
|
+
}
|
56
|
+
}
|
57
|
+
|
31
58
|
@keyframes rotate {
|
32
59
|
0% {
|
33
60
|
transform: translate(-50%, -50%) rotateZ(0deg);
|
@@ -49,7 +76,6 @@
|
|
49
76
|
}
|
50
77
|
|
51
78
|
@keyframes spin {
|
52
|
-
|
53
79
|
0%,
|
54
80
|
100% {
|
55
81
|
box-shadow: .3em 0px 0 0px currentcolor;
|
@@ -3,6 +3,8 @@
|
|
3
3
|
module Raif::Concerns::LlmResponseParsing
|
4
4
|
extend ActiveSupport::Concern
|
5
5
|
|
6
|
+
ASCII_CONTROL_CHARS = /[\x00-\x1f\x7f]/
|
7
|
+
|
6
8
|
included do
|
7
9
|
normalizes :raw_response, with: ->(text){ text&.strip }
|
8
10
|
|
@@ -35,32 +37,36 @@ module Raif::Concerns::LlmResponseParsing
|
|
35
37
|
# If the response format is HTML, it will be sanitized via ActionController::Base.helpers.sanitize.
|
36
38
|
#
|
37
39
|
# @return [Object] The parsed response.
|
38
|
-
def parsed_response
|
40
|
+
def parsed_response(force_reparse: false)
|
39
41
|
return if raw_response.blank?
|
42
|
+
return @parsed_response if @parsed_response.present? && !force_reparse
|
40
43
|
|
41
|
-
@parsed_response
|
42
|
-
|
43
|
-
JSON.parse(json)
|
44
|
+
@parsed_response = if response_format_json?
|
45
|
+
parse_json_response
|
44
46
|
elsif response_format_html?
|
45
|
-
|
46
|
-
clean_html_fragment(html)
|
47
|
+
parse_html_response
|
47
48
|
else
|
48
49
|
raw_response.strip
|
49
50
|
end
|
50
51
|
end
|
51
52
|
|
52
|
-
def
|
53
|
-
|
53
|
+
def parse_json_response
|
54
|
+
json = raw_response.gsub(/#{ASCII_CONTROL_CHARS}|^```json|```$/, "").strip
|
54
55
|
|
55
|
-
|
56
|
-
if node.text? && node.text.strip.empty?
|
57
|
-
node.remove
|
58
|
-
end
|
59
|
-
end
|
56
|
+
raise JSON::ParserError, "Invalid JSON" if json.blank?
|
60
57
|
|
61
|
-
|
62
|
-
|
58
|
+
JSON.parse(json)
|
59
|
+
end
|
63
60
|
|
64
|
-
|
61
|
+
def parse_html_response
|
62
|
+
html = raw_response.strip.gsub("```html", "").chomp("```")
|
63
|
+
|
64
|
+
html_with_converted_links = Raif::Utils::HtmlFragmentProcessor.convert_markdown_links_to_html(html)
|
65
|
+
Raif::Utils::HtmlFragmentProcessor.clean_html_fragment(
|
66
|
+
html_with_converted_links,
|
67
|
+
allowed_tags: allowed_tags,
|
68
|
+
allowed_attributes: allowed_attributes
|
69
|
+
)
|
65
70
|
end
|
71
|
+
|
66
72
|
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Raif::Concerns::Llms::Anthropic::ToolFormatting
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
def build_tools_parameter(model_completion)
|
7
|
+
tools = []
|
8
|
+
|
9
|
+
# If we're looking for a JSON response, add a tool to the request that the model can use to provide a JSON response
|
10
|
+
if model_completion.response_format_json? && model_completion.json_response_schema.present?
|
11
|
+
tools << {
|
12
|
+
name: "json_response",
|
13
|
+
description: "Generate a structured JSON response based on the provided schema.",
|
14
|
+
input_schema: model_completion.json_response_schema
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
# If we support native tool use and have tools available, add them to the request
|
19
|
+
if supports_native_tool_use? && model_completion.available_model_tools.any?
|
20
|
+
model_completion.available_model_tools_map.each do |_tool_name, tool|
|
21
|
+
tools << if tool.provider_managed?
|
22
|
+
format_provider_managed_tool(tool)
|
23
|
+
else
|
24
|
+
{
|
25
|
+
name: tool.tool_name,
|
26
|
+
description: tool.tool_description,
|
27
|
+
input_schema: tool.tool_arguments_schema
|
28
|
+
}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
tools
|
34
|
+
end
|
35
|
+
|
36
|
+
def format_provider_managed_tool(tool)
|
37
|
+
validate_provider_managed_tool_support!(tool)
|
38
|
+
|
39
|
+
case tool.name
|
40
|
+
when "Raif::ModelTools::ProviderManaged::WebSearch"
|
41
|
+
{
|
42
|
+
type: "web_search_20250305",
|
43
|
+
name: "web_search",
|
44
|
+
max_uses: 5
|
45
|
+
}
|
46
|
+
when "Raif::ModelTools::ProviderManaged::CodeExecution"
|
47
|
+
{
|
48
|
+
type: "code_execution_20250522",
|
49
|
+
name: "code_execution"
|
50
|
+
}
|
51
|
+
else
|
52
|
+
raise Raif::Errors::UnsupportedFeatureError,
|
53
|
+
"Invalid provider-managed tool: #{tool.name} for #{key}"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -1,9 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
module Raif::Concerns::Llms::
|
3
|
+
module Raif::Concerns::Llms::Bedrock::MessageFormatting
|
4
4
|
extend ActiveSupport::Concern
|
5
5
|
|
6
|
-
def format_string_message(content)
|
6
|
+
def format_string_message(content, role: nil)
|
7
7
|
{ "text" => content }
|
8
8
|
end
|
9
9
|
|
@@ -13,7 +13,7 @@ module Raif::Concerns::Llms::BedrockClaude::MessageFormatting
|
|
13
13
|
elsif image_input.source_type == :file_content
|
14
14
|
# The AWS Bedrock SDK requires data sent as bytes (and doesn't support base64 like everyone else)
|
15
15
|
# The ModelCompletion stores the messages as JSON though, so it can't be raw bytes (it will throw an encoding error).
|
16
|
-
# We store the image data as base64 and then it will get converted to bytes in Raif::Llms::
|
16
|
+
# We store the image data as base64 and then it will get converted to bytes in Raif::Llms::Bedrock#perform_model_completion!
|
17
17
|
# before sending to AWS.
|
18
18
|
{
|
19
19
|
"image" => {
|
@@ -34,7 +34,7 @@ module Raif::Concerns::Llms::BedrockClaude::MessageFormatting
|
|
34
34
|
elsif file_input.source_type == :file_content
|
35
35
|
# The AWS Bedrock SDK requires data sent as bytes (and doesn't support base64 like everyone else)
|
36
36
|
# The ModelCompletion stores the messages as JSON though, so it can't be raw bytes (it will throw an encoding error).
|
37
|
-
# We store the image data as base64 and then it will get converted to bytes in Raif::Llms::
|
37
|
+
# We store the image data as base64 and then it will get converted to bytes in Raif::Llms::Bedrock#perform_model_completion!
|
38
38
|
# before sending to AWS.
|
39
39
|
{
|
40
40
|
"document" => {
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Raif::Concerns::Llms::Bedrock::ToolFormatting
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
def build_tools_parameter(model_completion)
|
7
|
+
tools = []
|
8
|
+
|
9
|
+
# If we're looking for a JSON response, add a tool to the request that the model can use to provide a JSON response
|
10
|
+
if model_completion.response_format_json? && model_completion.json_response_schema.present?
|
11
|
+
tools << {
|
12
|
+
name: "json_response",
|
13
|
+
description: "Generate a structured JSON response based on the provided schema.",
|
14
|
+
input_schema: { json: model_completion.json_response_schema }
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
model_completion.available_model_tools_map.each do |_tool_name, tool|
|
19
|
+
tools << if tool.provider_managed?
|
20
|
+
raise Raif::Errors::UnsupportedFeatureError,
|
21
|
+
"Invalid provider-managed tool: #{tool.name} for #{key}"
|
22
|
+
else
|
23
|
+
{
|
24
|
+
name: tool.tool_name,
|
25
|
+
description: tool.tool_description,
|
26
|
+
input_schema: { json: tool.tool_arguments_schema }
|
27
|
+
}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
return {} if tools.blank?
|
32
|
+
|
33
|
+
{
|
34
|
+
tools: tools.map{|tool| { tool_spec: tool } }
|
35
|
+
}
|
36
|
+
end
|
37
|
+
end
|
@@ -5,9 +5,10 @@ module Raif::Concerns::Llms::MessageFormatting
|
|
5
5
|
|
6
6
|
def format_messages(messages)
|
7
7
|
messages.map do |message|
|
8
|
+
role = message["role"] || message[:role]
|
8
9
|
{
|
9
|
-
"role" =>
|
10
|
-
"content" => format_message_content(message["content"] || message[:content])
|
10
|
+
"role" => role,
|
11
|
+
"content" => format_message_content(message["content"] || message[:content], role: role)
|
11
12
|
}
|
12
13
|
end
|
13
14
|
end
|
@@ -15,11 +16,11 @@ module Raif::Concerns::Llms::MessageFormatting
|
|
15
16
|
# Content could be a string or an array.
|
16
17
|
# If it's an array, it could contain Raif::ModelImageInput or Raif::ModelFileInput objects,
|
17
18
|
# which need to be formatted according to each model provider's API.
|
18
|
-
def format_message_content(content)
|
19
|
+
def format_message_content(content, role: nil)
|
19
20
|
raise ArgumentError,
|
20
21
|
"Message content must be an array or a string. Content was: #{content.inspect}" unless content.is_a?(Array) || content.is_a?(String)
|
21
22
|
|
22
|
-
return [format_string_message(content)] if content.is_a?(String)
|
23
|
+
return [format_string_message(content, role: role)] if content.is_a?(String)
|
23
24
|
|
24
25
|
content.map do |item|
|
25
26
|
if item.is_a?(Raif::ModelImageInput)
|
@@ -27,14 +28,14 @@ module Raif::Concerns::Llms::MessageFormatting
|
|
27
28
|
elsif item.is_a?(Raif::ModelFileInput)
|
28
29
|
format_model_file_input_message(item)
|
29
30
|
elsif item.is_a?(String)
|
30
|
-
format_string_message(item)
|
31
|
+
format_string_message(item, role: role)
|
31
32
|
else
|
32
33
|
item
|
33
34
|
end
|
34
35
|
end
|
35
36
|
end
|
36
37
|
|
37
|
-
def format_string_message(content)
|
38
|
+
def format_string_message(content, role: nil)
|
38
39
|
{ "type" => "text", "text" => content }
|
39
40
|
end
|
40
41
|
|