raif 1.1.0 → 1.2.1.pre
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 +150 -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 +44 -44
- 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 +24 -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 +169 -47
- data/lib/raif/migration_checker.rb +74 -0
- data/lib/raif/utils/html_fragment_processor.rb +170 -0
- data/lib/raif/utils.rb +1 -0
- data/lib/raif/version.rb +1 -1
- data/lib/raif.rb +2 -0
- data/spec/support/complex_test_tool.rb +65 -0
- data/spec/support/rspec_helpers.rb +66 -0
- data/spec/support/test_conversation.rb +18 -0
- data/spec/support/test_embedding_model.rb +27 -0
- data/spec/support/test_llm.rb +22 -0
- data/spec/support/test_model_tool.rb +32 -0
- data/spec/support/test_task.rb +45 -0
- metadata +52 -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: 1fb384de50e5d129f3cc88fd56ba12f8a45c5c25061b8dda631f3ce7593a681b
|
4
|
+
data.tar.gz: 4805d6a421cdff32a2ec857b53ced8e7244b32f918cda927bace04693d840771
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fb2cb0bda00b00cbee10a08d8a634a7a06276ab555cfd7a224ea51d6ab800effc7fbaedb6ba2642687632c705bc4ee953bc0b74316431bfc2ba1e72e091aba7f
|
7
|
+
data.tar.gz: b4dc7c5b059054b5252f1d676f8dfa67253de3548da9a5c4fae813f675a77634d6bd4da477ae26129cebdd6515cb7c09de389445ec80f6bb69f89b10d620755c
|
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
|
|
@@ -144,6 +195,8 @@ Currently included OpenRouter models:
|
|
144
195
|
- `open_router_claude_3_7_sonnet`
|
145
196
|
- `open_router_llama_3_3_70b_instruct`
|
146
197
|
- `open_router_llama_3_1_8b_instruct`
|
198
|
+
- `open_router_llama_4_maverick`
|
199
|
+
- `open_router_llama_4_scout`
|
147
200
|
- `open_router_gemini_2_0_flash`
|
148
201
|
- `open_router_deepseek_chat_v3`
|
149
202
|
|
@@ -184,6 +237,38 @@ puts model_completion.parsed_response # will strip backticks, parse the JSON, an
|
|
184
237
|
# => {"joke" => "Why don't skeletons fight each other? They don't have the guts."}
|
185
238
|
```
|
186
239
|
|
240
|
+
## Streaming Responses
|
241
|
+
|
242
|
+
You can enable streaming for any chat call by passing a block to the `chat` method. When streaming is enabled, the block will be called with partial responses as they're received from the LLM:
|
243
|
+
|
244
|
+
```ruby
|
245
|
+
llm = Raif.llm(:open_ai_gpt_4o)
|
246
|
+
model_completion = llm.chat(message: "Tell me a story") do |model_completion, delta, sse_event|
|
247
|
+
# This block is called multiple times as the response streams in.
|
248
|
+
# You could broadcast these updates via Turbo Streams, WebSockets, etc.
|
249
|
+
Turbo::StreamsChannel.broadcast_replace_to(
|
250
|
+
:my_channel,
|
251
|
+
target: "chat-response",
|
252
|
+
partial: "my_partial_displaying_chat_response",
|
253
|
+
locals: { model_completion: model_completion, delta: delta, sse_event: sse_event }
|
254
|
+
)
|
255
|
+
end
|
256
|
+
|
257
|
+
# The final complete response is available in the model_completion
|
258
|
+
puts model_completion.raw_response
|
259
|
+
```
|
260
|
+
|
261
|
+
You can configure the streaming update frequency by adjusting the chunk size threshold in your Raif configuration:
|
262
|
+
|
263
|
+
```ruby
|
264
|
+
Raif.configure do |config|
|
265
|
+
# Control how often the model completion is updated & the block is called when streaming.
|
266
|
+
# Lower values = more frequent updates but more database writes.
|
267
|
+
# Higher values = less frequent updates but fewer database writes.
|
268
|
+
config.streaming_update_chunk_size_threshold = 50 # default is 25
|
269
|
+
end
|
270
|
+
```
|
271
|
+
|
187
272
|
# Key Raif Concepts
|
188
273
|
|
189
274
|
## Tasks
|
@@ -334,6 +419,10 @@ If your app already includes Bootstrap styles, this will render a conversation i
|
|
334
419
|
|
335
420
|
If your app does not include Bootstrap, you can [override the views](#views) to update styles.
|
336
421
|
|
422
|
+
### Real-time Streaming Responses
|
423
|
+
|
424
|
+
Raif conversations have built-in support for streaming responses, where the LLM's response is displayed progressively as it's being generated. Each time a conversation entry is updated during the streaming response, Raif will call `broadcast_replace_to(conversation)` (where `conversation` is the `Raif::Conversation` associated with the conversation entry). When using the `raif_conversation` view helper, it will automatically set up the subscription for you.
|
425
|
+
|
337
426
|
### Conversation Types
|
338
427
|
|
339
428
|
If your application has a specific type of conversation that you use frequently, you can create a custom conversation type by running the generator. For example, say you are implementing a customer support chatbot in your application and want to have a custom conversation type for doing this with the LLM:
|
@@ -520,7 +609,54 @@ class Raif::ModelTools::GoogleSearch < Raif::ModelTool
|
|
520
609
|
end
|
521
610
|
```
|
522
611
|
|
523
|
-
|
612
|
+
### Provider-Managed Tools
|
613
|
+
|
614
|
+
In addition to the ability to create your own model tools, Raif supports provider-managed tools. These are tools that are built into certain LLM providers and run on the provider's infrastructure:
|
615
|
+
|
616
|
+
- **`Raif::ModelTools::ProviderManaged::WebSearch`**: Performs real-time web searches and returns relevant results
|
617
|
+
- **`Raif::ModelTools::ProviderManaged::CodeExecution`**: Executes code in a secure sandboxed environment (e.g. Python)
|
618
|
+
- **`Raif::ModelTools::ProviderManaged::ImageGeneration`**: Generates images based on text descriptions
|
619
|
+
|
620
|
+
Current provider-managed tool support:
|
621
|
+
| Provider | WebSearch | CodeExecution | ImageGeneration |
|
622
|
+
|----------|-----------|---------------|-----------------|
|
623
|
+
| OpenAI Responses API | ✅ | ✅ | ✅ |
|
624
|
+
| OpenAI Completions API | ❌ | ❌ | ❌ |
|
625
|
+
| Anthropic Claude | ✅ | ✅ | ❌ |
|
626
|
+
| AWS Bedrock (Claude) | ❌ | ❌ | ❌ |
|
627
|
+
| OpenRouter | ❌ | ❌ | ❌ |
|
628
|
+
|
629
|
+
To use provider-managed tools, include them in the `available_model_tools` array:
|
630
|
+
|
631
|
+
```ruby
|
632
|
+
# In a conversation
|
633
|
+
conversation = Raif::Conversation.create!(
|
634
|
+
creator: current_user,
|
635
|
+
available_model_tools: [
|
636
|
+
"Raif::ModelTools::ProviderManaged::WebSearch",
|
637
|
+
"Raif::ModelTools::ProviderManaged::CodeExecution"
|
638
|
+
]
|
639
|
+
)
|
640
|
+
|
641
|
+
# In an agent
|
642
|
+
agent = Raif::Agents::ReActAgent.new(
|
643
|
+
task: "Search for recent news about AI and create a summary chart",
|
644
|
+
available_model_tools: [
|
645
|
+
"Raif::ModelTools::ProviderManaged::WebSearch",
|
646
|
+
"Raif::ModelTools::ProviderManaged::CodeExecution"
|
647
|
+
],
|
648
|
+
creator: current_user
|
649
|
+
)
|
650
|
+
|
651
|
+
# Directly in a chat
|
652
|
+
llm = Raif.llm(:open_ai_responses_gpt_4_1)
|
653
|
+
model_completion = llm.chat(
|
654
|
+
messages: [{ role: "user", content: "What are the latest developments in Ruby on Rails?" }],
|
655
|
+
available_model_tools: [Raif::ModelTools::ProviderManaged::WebSearch]
|
656
|
+
)
|
657
|
+
```
|
658
|
+
|
659
|
+
## Sending Images/Files/PDF's to the LLM
|
524
660
|
|
525
661
|
Raif supports images, files, and PDF's in the messages sent to the LLM.
|
526
662
|
|
@@ -596,7 +732,7 @@ Raif supports generation of vector embeddings. You can enable and configure embe
|
|
596
732
|
```ruby
|
597
733
|
Raif.configure do |config|
|
598
734
|
config.open_ai_embedding_models_enabled = true
|
599
|
-
config.
|
735
|
+
config.bedrock_embedding_models_enabled = true
|
600
736
|
|
601
737
|
config.default_embedding_model_key = "open_ai_text_embedding_3_small"
|
602
738
|
end
|
@@ -649,6 +785,7 @@ The admin interface contains sections for:
|
|
649
785
|
- Conversations
|
650
786
|
- Agents
|
651
787
|
- Model Tool Invocations
|
788
|
+
- Stats
|
652
789
|
|
653
790
|
|
654
791
|
### Model Completions
|
@@ -670,6 +807,9 @@ The admin interface contains sections for:
|
|
670
807
|

|
671
808
|

|
672
809
|
|
810
|
+
### Stats
|
811
|
+

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

|
834
974
|
|
975
|
+
# Contributing
|
976
|
+
|
977
|
+
We welcome contributions to Raif! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
|
978
|
+
|
979
|
+
**Important**: All PR's should be made against the `dev` branch.
|
980
|
+
|
835
981
|
# License
|
836
982
|
|
837
983
|
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
|
|