raix-openai-eight 1.0.1

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.
data/README.md ADDED
@@ -0,0 +1,775 @@
1
+ # Ruby AI eXtensions
2
+
3
+ ## What's Raix
4
+
5
+ Raix (pronounced "ray" because the x is silent) is a library that gives you everything you need to add discrete large-language model (LLM) AI components to your Ruby applications. Raix consists of proven code that has been extracted from [Olympia](https://olympia.chat), the world's leading virtual AI team platform, and probably one of the biggest and most successful AI chat projects written completely in Ruby.
6
+
7
+ Understanding how to use discrete AI components in otherwise normal code is key to productively leveraging Raix, and the subject of a book written by Raix's author Obie Fernandez, titled [Patterns of Application Development Using AI](https://leanpub.com/patterns-of-application-development-using-ai). You can easily support the ongoing development of this project by buying the book at Leanpub.
8
+
9
+ At the moment, Raix natively supports use of either OpenAI or OpenRouter as its underlying AI provider. Eventually you will be able to specify your AI provider via an adapter, kind of like ActiveRecord maps to databases. Note that you can also use Raix to add AI capabilities to non-Rails applications as long as you include ActiveSupport as a dependency. Extracting the base code to its own standalone library without Rails dependencies is on the roadmap, but not a high priority.
10
+
11
+ ### Chat Completions
12
+
13
+ Raix consists of three modules that can be mixed in to Ruby classes to give them AI powers. The first (and mandatory) module is `ChatCompletion`, which provides `transcript` and `chat_completion` methods.
14
+
15
+ ```ruby
16
+ class MeaningOfLife
17
+ include Raix::ChatCompletion
18
+ end
19
+
20
+ >> ai = MeaningOfLife.new
21
+ >> ai.transcript << { user: "What is the meaning of life?" }
22
+ >> ai.chat_completion
23
+
24
+ => "The question of the meaning of life is one of the most profound and enduring inquiries in philosophy, religion, and science.
25
+ Different perspectives offer various answers..."
26
+ ```
27
+
28
+ By default, Raix will automatically add the AI's response to the transcript. This behavior can be controlled with the `save_response` parameter, which defaults to `true`. You may want to set it to `false` when making multiple chat completion calls during the lifecycle of a single object (whether sequentially or in parallel) and want to manage the transcript updates yourself:
29
+
30
+ ```ruby
31
+ >> ai.chat_completion(save_response: false)
32
+ ```
33
+
34
+ #### Transcript Format
35
+
36
+ The transcript accepts both abbreviated and standard OpenAI message hash formats. The abbreviated format, suitable for system, assistant, and user messages is simply a mapping of `role => content`, as shown in the example above.
37
+
38
+ ```ruby
39
+ transcript << { user: "What is the meaning of life?" }
40
+ ```
41
+
42
+ As mentioned, Raix also understands standard OpenAI messages hashes. The previous example could be written as:
43
+
44
+ ```ruby
45
+ transcript << { role: "user", content: "What is the meaning of life?" }
46
+ ```
47
+
48
+ One of the advantages of OpenRouter and the reason that it is used by default by this library is that it handles mapping message formats from the OpenAI standard to whatever other model you're wanting to use (Anthropic, Cohere, etc.)
49
+
50
+ Note that it's possible to override the current object's transcript by passing a `messages` array to `chat_completion`. This allows for multiple threads to share a single conversation context in parallel, by deferring when they write their responses back to the transcript.
51
+
52
+ ```
53
+ chat_completion(openai: "gpt-4.1-nano", messages: [{ user: "What is the meaning of life?" }])
54
+ ```
55
+
56
+ ### Predicted Outputs
57
+
58
+ Raix supports [Predicted Outputs](https://platform.openai.com/docs/guides/latency-optimization#use-predicted-outputs) with the `prediction` parameter for OpenAI.
59
+
60
+ ```ruby
61
+ >> ai.chat_completion(openai: "gpt-4o", params: { prediction: })
62
+ ```
63
+
64
+ ### Prompt Caching
65
+
66
+ Raix supports [Anthropic-style prompt caching](https://openrouter.ai/docs/prompt-caching#anthropic-claude) when using Anthropic's Claude family of models. You can specify a `cache_at` parameter when doing a chat completion. If the character count for the content of a particular message is longer than the cache_at parameter, it will be sent to Anthropic as a multipart message with a cache control "breakpoint" set to "ephemeral".
67
+
68
+ Note that there is a limit of four breakpoints, and the cache will expire within five minutes. Therefore, it is recommended to reserve the cache breakpoints for large bodies of text, such as character cards, CSV data, RAG data, book chapters, etc. Raix does not enforce a limit on the number of breakpoints, which means that you might get an error if you try to cache too many messages.
69
+
70
+ ```ruby
71
+ >> my_class.chat_completion(params: { cache_at: 1000 })
72
+ => {
73
+ "messages": [
74
+ {
75
+ "role": "system",
76
+ "content": [
77
+ {
78
+ "type": "text",
79
+ "text": "HUGE TEXT BODY LONGER THAN 1000 CHARACTERS",
80
+ "cache_control": {
81
+ "type": "ephemeral"
82
+ }
83
+ }
84
+ ]
85
+ },
86
+ ```
87
+
88
+ ### JSON Mode
89
+
90
+ Raix supports JSON mode for chat completions, which ensures that the AI model's response is valid JSON. This is particularly useful when you need structured data from the model.
91
+
92
+ When using JSON mode with OpenAI models, Raix will automatically set the `response_format` parameter on requests accordingly, and attempt to parse the entire response body as JSON.
93
+ When using JSON mode with other models (e.g. Anthropic) that don't support `response_format`, Raix will look for JSON content inside of &lt;json&gt; XML tags in the response, before
94
+ falling back to parsing the entire response body. Make sure you tell the AI to reply with JSON inside of XML tags.
95
+
96
+ ```ruby
97
+ >> my_class.chat_completion(json: true)
98
+ => { "key": "value" }
99
+ ```
100
+
101
+ When using JSON mode with non-OpenAI providers, Raix automatically sets the `require_parameters` flag to ensure proper JSON formatting. You can also combine JSON mode with other parameters:
102
+
103
+ ```ruby
104
+ >> my_class.chat_completion(json: true, openai: "gpt-4o")
105
+ => { "key": "value" }
106
+ ```
107
+
108
+ ### Use of Tools/Functions
109
+
110
+ The second (optional) module that you can add to your Ruby classes after `ChatCompletion` is `FunctionDispatch`. It lets you declare and implement functions to be called at the AI's discretion in a declarative, Rails-like "DSL" fashion.
111
+
112
+ When the AI responds with tool function calls instead of a text message, Raix automatically:
113
+ 1. Executes the requested tool functions
114
+ 2. Adds the function results to the conversation transcript
115
+ 3. Sends the updated transcript back to the AI for another completion
116
+ 4. Repeats this process until the AI responds with a regular text message
117
+
118
+ This automatic continuation ensures that tool calls are seamlessly integrated into the conversation flow. The AI can use tool results to formulate its final response to the user. You can limit the number of tool calls using the `max_tool_calls` parameter to prevent excessive function invocations.
119
+
120
+ ```ruby
121
+ class WhatIsTheWeather
122
+ include Raix::ChatCompletion
123
+ include Raix::FunctionDispatch
124
+
125
+ function :check_weather,
126
+ "Check the weather for a location",
127
+ location: { type: "string", required: true } do |arguments|
128
+ "The weather in #{arguments[:location]} is hot and sunny"
129
+ end
130
+ end
131
+
132
+ RSpec.describe WhatIsTheWeather do
133
+ subject { described_class.new }
134
+
135
+ it "provides a text response after automatically calling weather function" do
136
+ subject.transcript << { user: "What is the weather in Zipolite, Oaxaca?" }
137
+ response = subject.chat_completion(openai: "gpt-4o")
138
+ expect(response).to include("hot and sunny")
139
+ end
140
+ end
141
+ ```
142
+
143
+ Parameters are optional by default. Mark them as required with `required: true` or explicitly optional with `optional: true`.
144
+
145
+ Note that for security reasons, dispatching functions only works with functions implemented using `Raix::FunctionDispatch#function` or directly on the class.
146
+
147
+ #### Tool Filtering
148
+
149
+ You can control which tool functions are exposed to the AI per request using the `available_tools` parameter of the `chat_completion` method:
150
+
151
+ ```ruby
152
+ class WeatherAndTime
153
+ include Raix::ChatCompletion
154
+ include Raix::FunctionDispatch
155
+
156
+ function :check_weather, "Check the weather for a location", location: { type: "string" } do |arguments|
157
+ "The weather in #{arguments[:location]} is sunny"
158
+ end
159
+
160
+ function :get_time, "Get the current time" do |_arguments|
161
+ "The time is 12:00 PM"
162
+ end
163
+ end
164
+
165
+ weather = WeatherAndTime.new
166
+
167
+ # Don't pass any tools to the LLM
168
+ weather.chat_completion(available_tools: false)
169
+
170
+ # Only pass specific tools to the LLM
171
+ weather.chat_completion(available_tools: [:check_weather])
172
+
173
+ # Pass all declared tools (default behavior)
174
+ weather.chat_completion
175
+ ```
176
+
177
+ The `available_tools` parameter accepts three types of values:
178
+ - `nil`: All declared tool functions are passed (default behavior)
179
+ - `false`: No tools are passed to the LLM
180
+ - An array of symbols: Only the specified tools are passed (raises `Raix::UndeclaredToolError` if a specified tool function is not declared)
181
+
182
+ #### Multiple Tool Calls
183
+
184
+ Some AI models (like GPT-4) can make multiple tool calls in a single response. When this happens, Raix will automatically handle all the function calls sequentially.
185
+ If you need to capture the arguments to the function calls, do so in the block passed to `function`. The response from `chat_completion` is always the final text
186
+ response from the assistant, and is not affected by function calls.
187
+
188
+ ```ruby
189
+ class MultipleToolExample
190
+ include Raix::ChatCompletion
191
+ include Raix::FunctionDispatch
192
+
193
+ attr_reader :invocations
194
+
195
+ function :first_tool do |arguments|
196
+ @invocations << :first
197
+ "Result from first tool"
198
+ end
199
+
200
+ function :second_tool do |arguments|
201
+ @invocations << :second
202
+ "Result from second tool"
203
+ end
204
+
205
+ def initialize
206
+ @invocations = []
207
+ end
208
+ end
209
+
210
+ example = MultipleToolExample.new
211
+ example.transcript << { user: "Please use both tools" }
212
+ example.chat_completion(openai: "gpt-4o")
213
+ # => "I used both tools, as requested"
214
+
215
+ example.invocations
216
+ # => [:first, :second]
217
+ ```
218
+
219
+ #### Customizing Function Dispatch
220
+
221
+ You can customize how function calls are handled by overriding the `dispatch_tool_function` in your class. This is useful if you need to add logging, caching, error handling, or other custom behavior around function calls.
222
+
223
+ ```ruby
224
+ class CustomDispatchExample
225
+ include Raix::ChatCompletion
226
+ include Raix::FunctionDispatch
227
+
228
+ function :example_tool do |arguments|
229
+ "Result from example tool"
230
+ end
231
+
232
+ def dispatch_tool_function(function_name, arguments)
233
+ puts "Calling #{function_name} with #{arguments}"
234
+ result = super
235
+ puts "Result: #{result}"
236
+ result
237
+ end
238
+ end
239
+ ```
240
+
241
+ #### Function Call Caching
242
+
243
+ You can use ActiveSupport's Cache to cache function call results, which can be particularly useful for expensive operations or external API calls that don't need to be repeated frequently.
244
+
245
+ ```ruby
246
+ class CachedFunctionExample
247
+ include Raix::ChatCompletion
248
+ include Raix::FunctionDispatch
249
+
250
+ function :expensive_operation do |arguments|
251
+ "Result of expensive operation with #{arguments}"
252
+ end
253
+
254
+ # Override dispatch_tool_function to enable caching for all functions
255
+ def dispatch_tool_function(function_name, arguments)
256
+ # Pass the cache to the superclass implementation
257
+ super(function_name, arguments, cache: Rails.cache)
258
+ end
259
+ end
260
+ ```
261
+
262
+ The caching mechanism works by:
263
+ 1. Passing the cache object through `dispatch_tool_function` to the function implementation
264
+ 2. Using the function name and arguments as cache keys
265
+ 3. Automatically fetching from cache when available or executing the function when not cached
266
+
267
+ This is particularly useful for:
268
+ - Expensive database operations
269
+ - External API calls
270
+ - Resource-intensive computations
271
+ - Functions with deterministic outputs for the same inputs
272
+
273
+ #### Limiting Tool Calls
274
+
275
+ You can control the maximum number of tool calls before the AI must provide a text response:
276
+
277
+ ```ruby
278
+ # Limit to 5 tool calls (default is 25)
279
+ response = my_ai.chat_completion(max_tool_calls: 5)
280
+
281
+ # Configure globally
282
+ Raix.configure do |config|
283
+ config.max_tool_calls = 10
284
+ end
285
+ ```
286
+
287
+ #### Manually Stopping Tool Calls
288
+
289
+ For AI components that process tasks without end-user interaction, you can use `stop_tool_calls_and_respond!` within a function to force the AI to provide a text response without making additional tool calls.
290
+
291
+ ```ruby
292
+ class OrderProcessor
293
+ include Raix::ChatCompletion
294
+ include Raix::FunctionDispatch
295
+
296
+ SYSTEM_DIRECTIVE = "You are an order processor, tasked with order validation, inventory check,
297
+ payment processing, and shipping."
298
+
299
+ attr_accessor :order
300
+
301
+ def initialize(order)
302
+ self.order = order
303
+ transcript << { system: SYSTEM_DIRECTIVE }
304
+ transcript << { user: order.to_json }
305
+ end
306
+
307
+ def perform
308
+ # will automatically continue after tool calls until finished_processing is called
309
+ chat_completion
310
+ end
311
+
312
+
313
+ # implementation of functions that can be called by the AI
314
+ # entirely at its discretion, depending on the needs of the order.
315
+ # The return value of each `perform` method will be added to the
316
+ # transcript of the conversation as a function result.
317
+
318
+ function :validate_order do
319
+ OrderValidationWorker.perform(@order)
320
+ end
321
+
322
+ function :check_inventory do
323
+ InventoryCheckWorker.perform(@order)
324
+ end
325
+
326
+ function :process_payment do
327
+ PaymentProcessingWorker.perform(@order)
328
+ end
329
+
330
+ function :schedule_shipping do
331
+ ShippingSchedulerWorker.perform(@order)
332
+ end
333
+
334
+ function :send_confirmation do
335
+ OrderConfirmationWorker.perform(@order)
336
+ end
337
+
338
+ function :finished_processing do
339
+ order.update!(transcript:, processed_at: Time.current)
340
+ stop_tool_calls_and_respond!
341
+ "Order processing completed successfully"
342
+ end
343
+ end
344
+ ```
345
+
346
+ ### Prompt Declarations
347
+
348
+ The third (also optional) module that you can add mix in along with `ChatCompletion` is `PromptDeclarations`. It provides the ability to declare a "Prompt Chain" (series of prompts to be called in a sequence), and also features a declarative, Rails-like "DSL" of its own. Prompts can be defined inline or delegate to callable prompt objects, which themselves implement `ChatCompletion`.
349
+
350
+ The following example is a rough excerpt of the main "Conversation Loop" in Olympia, which pre-processes user messages to check for
351
+ the presence of URLs and scan memory before submitting as a prompt to GPT-4. Note that prompt declarations are executed in the order
352
+ that they are declared. The `FetchUrlCheck` callable prompt class is included for instructional purposes. Note that it is passed the
353
+ an instance of the object that is calling it in its initializer as its `context`. The passing of context means that you can assemble
354
+ composite prompt structures of arbitrary depth.
355
+
356
+ ```ruby
357
+ class PromptSubscriber
358
+ include Raix::ChatCompletion
359
+ include Raix::PromptDeclarations
360
+
361
+ attr_accessor :conversation, :bot_message, :user_message
362
+
363
+ # many other declarations omitted...
364
+
365
+ prompt call: FetchUrlCheck
366
+
367
+ prompt call: MemoryScan
368
+
369
+ prompt text: -> { user_message.content }, stream: -> { ReplyStream.new(self) }, until: -> { bot_message.complete? }
370
+
371
+ def initialize(conversation)
372
+ self.conversation = conversation
373
+ end
374
+
375
+ def message_created(user_message)
376
+ self.user_message = user_message
377
+ self.bot_message = conversation.bot_message!(responding_to: user_message)
378
+
379
+ chat_completion(loop: true, openai: "gpt-4o")
380
+ end
381
+
382
+ ...
383
+ end
384
+
385
+ class FetchUrlCheck
386
+ include ChatCompletion
387
+ include FunctionDispatch
388
+
389
+ REGEX = %r{\b(?:http(s)?://)?(?:www\.)?[a-zA-Z0-9-]+(\.[a-zA-Z]{2,})+(/[^\s]*)?\b}
390
+
391
+ attr_accessor :context, :conversation
392
+
393
+ delegate :user_message, to: :context
394
+ delegate :content, to: :user_message
395
+
396
+ def initialize(context)
397
+ self.context = context
398
+ self.conversation = context.conversation
399
+ self.model = "anthropic/claude-3-haiku"
400
+ end
401
+
402
+ def call
403
+ return unless content&.match?(REGEX)
404
+
405
+ transcript << { system: "Call the `fetch` function if the user mentions a website, otherwise say nil" }
406
+ transcript << { user: content }
407
+
408
+ chat_completion # TODO: consider looping to fetch more than one URL per user message
409
+ end
410
+
411
+ function :fetch, "Gets the plain text contents of a web page", url: { type: "string" } do |arguments|
412
+ Tools::FetchUrl.fetch(arguments[:url]).tap do |result|
413
+ parent = conversation.function_call!("fetch_url", arguments, parent: user_message)
414
+ conversation.function_result!("fetch_url", result, parent:)
415
+ end
416
+ end
417
+
418
+ ```
419
+
420
+ Notably, Olympia does not use the `FunctionDispatch` module in its primary conversation loop because it does not have a fixed set of tools that are included in every single prompt. Functions are made available dynamically based on a number of factors including the user's plan tier and capabilities of the assistant with whom the user is conversing.
421
+
422
+ Streaming of the AI's response to the end user is handled by the `ReplyStream` class, passed to the final prompt declaration as its `stream` parameter. [Patterns of Application Development Using AI](https://leanpub.com/patterns-of-application-development-using-ai) devotes a whole chapter to describing how to write your own `ReplyStream` class.
423
+
424
+ #### Additional PromptDeclarations Options
425
+
426
+ The `PromptDeclarations` module supports several additional options that can be used to customize prompt behavior:
427
+
428
+ ```ruby
429
+ class CustomPromptExample
430
+ include Raix::ChatCompletion
431
+ include Raix::PromptDeclarations
432
+
433
+ # Basic prompt with text
434
+ prompt text: "Process this input"
435
+
436
+ # Prompt with system directive
437
+ prompt system: "You are a helpful assistant",
438
+ text: "Analyze this text"
439
+
440
+ # Prompt with conditions
441
+ prompt text: "Process this input",
442
+ if: -> { some_condition },
443
+ unless: -> { some_other_condition }
444
+
445
+ # Prompt with success callback
446
+ prompt text: "Process this input",
447
+ success: ->(response) { handle_response(response) }
448
+
449
+ # Prompt with custom parameters
450
+ prompt text: "Process with custom settings",
451
+ params: { temperature: 0.7, max_tokens: 1000 }
452
+
453
+ # Prompt with until condition for looping
454
+ prompt text: "Keep processing until complete",
455
+ until: -> { processing_complete? }
456
+
457
+ # Prompt with raw response
458
+ prompt text: "Get raw response",
459
+ raw: true
460
+
461
+ # Prompt using OpenAI directly
462
+ prompt text: "Use OpenAI",
463
+ openai: "gpt-4o"
464
+ end
465
+ ```
466
+
467
+ The available options include:
468
+
469
+ - `system`: Set a system directive for the prompt
470
+ - `if`/`unless`: Control prompt execution with conditions
471
+ - `success`: Handle prompt responses with callbacks
472
+ - `params`: Customize API parameters per prompt
473
+ - `until`: Control prompt looping
474
+ - `raw`: Get raw API responses
475
+ - `openai`: Use OpenAI directly
476
+ - `stream`: Control response streaming
477
+ - `call`: Delegate to callable prompt objects
478
+
479
+ You can also access the current prompt context and previous responses:
480
+
481
+ ```ruby
482
+ class ContextAwarePrompt
483
+ include Raix::ChatCompletion
484
+ include Raix::PromptDeclarations
485
+
486
+ def process_with_context
487
+ # Access current prompt
488
+ current_prompt.params[:temperature]
489
+
490
+ # Access previous response
491
+ last_response
492
+
493
+ chat_completion
494
+ end
495
+ end
496
+ ```
497
+
498
+ ## Predicate Module
499
+
500
+ The `Raix::Predicate` module provides a simple way to handle yes/no/maybe questions using AI chat completion. It allows you to define blocks that handle different types of responses with their explanations. It is one of the concrete patterns described in the "Discrete Components" chapter of [Patterns of Application Development Using AI](https://leanpub.com/patterns-of-application-development-using-ai).
501
+
502
+ ### Usage
503
+
504
+ Include the `Raix::Predicate` module in your class and define handlers using block syntax:
505
+
506
+ ```ruby
507
+ class Question
508
+ include Raix::Predicate
509
+
510
+ yes? do |explanation|
511
+ puts "Affirmative: #{explanation}"
512
+ end
513
+
514
+ no? do |explanation|
515
+ puts "Negative: #{explanation}"
516
+ end
517
+
518
+ maybe? do |explanation|
519
+ puts "Uncertain: #{explanation}"
520
+ end
521
+ end
522
+
523
+ question = Question.new
524
+ question.ask("Is Ruby a programming language?")
525
+ # => Affirmative: Yes, Ruby is a dynamic, object-oriented programming language...
526
+ ```
527
+
528
+ ### Features
529
+
530
+ - Define handlers for yes, no, and/or maybe responses using the declarative class level block syntax.
531
+ - At least one handler (yes, no, or maybe) must be defined.
532
+ - Handlers receive the full AI response including explanation as an argument.
533
+ - Responses always start with "Yes, ", "No, ", or "Maybe, " followed by an explanation.
534
+ - Make sure to ask a question that can be answered with yes, no, or maybe (otherwise the results are indeterminate).
535
+
536
+ ### Example with Single Handler
537
+
538
+ You can define only the handlers you need:
539
+
540
+ ```ruby
541
+ class SimpleQuestion
542
+ include Raix::Predicate
543
+
544
+ # Only handle positive responses
545
+ yes? do |explanation|
546
+ puts "✅ #{explanation}"
547
+ end
548
+ end
549
+
550
+ question = SimpleQuestion.new
551
+ question.ask("Is 2 + 2 = 4?")
552
+ # => ✅ Yes, 2 + 2 equals 4, this is a fundamental mathematical fact.
553
+ ```
554
+
555
+ ### Error Handling
556
+
557
+ The module will raise a RuntimeError if you attempt to ask a question without defining any response handlers:
558
+
559
+ ```ruby
560
+ class InvalidQuestion
561
+ include Raix::Predicate
562
+ end
563
+
564
+ question = InvalidQuestion.new
565
+ question.ask("Any question")
566
+ # => RuntimeError: Please define a yes and/or no block
567
+ ```
568
+
569
+ ## Model Context Protocol (Experimental)
570
+
571
+ The `Raix::MCP` module provides integration with the Model Context Protocol, allowing you to connect your Raix-powered application to remote MCP servers. This feature is currently **experimental**.
572
+
573
+ ### Usage
574
+
575
+ Include the `Raix::MCP` module in your class and declare MCP servers using the `mcp` DSL:
576
+
577
+ ```ruby
578
+ class McpConsumer
579
+ include Raix::ChatCompletion
580
+ include Raix::FunctionDispatch
581
+ include Raix::MCP
582
+
583
+ mcp "https://your-mcp-server.example.com/sse"
584
+ end
585
+ ```
586
+
587
+ ### Features
588
+
589
+ - Automatically fetches available tools from the remote MCP server using `tools/list`
590
+ - Registers remote tools as OpenAI-compatible function schemas
591
+ - Defines proxy methods that forward requests to the remote server via `tools/call`
592
+ - Seamlessly integrates with the existing `FunctionDispatch` workflow
593
+ - Handles transcript recording to maintain consistent conversation history
594
+
595
+ ### Filtering Tools
596
+
597
+ You can filter which remote tools to include:
598
+
599
+ ```ruby
600
+ class FilteredMcpConsumer
601
+ include Raix::ChatCompletion
602
+ include Raix::FunctionDispatch
603
+ include Raix::MCP
604
+
605
+ # Only include specific tools
606
+ mcp "https://server.example.com/sse", only: [:tool_one, :tool_two]
607
+
608
+ # Or exclude specific tools
609
+ mcp "https://server.example.com/sse", except: [:tool_to_exclude]
610
+ end
611
+ ```
612
+
613
+ ## Response Format (Experimental)
614
+
615
+ The `ResponseFormat` class provides a way to declare a JSON schema for the response format of an AI chat completion. It's particularly useful when you need structured responses from AI models, ensuring the output conforms to your application's requirements.
616
+
617
+ ### Features
618
+
619
+ - Converts Ruby hashes and arrays into JSON schema format
620
+ - Supports nested structures and arrays
621
+ - Enforces strict validation with `additionalProperties: false`
622
+ - Automatically marks all top-level properties as required
623
+ - Handles both simple type definitions and complex nested schemas
624
+
625
+ ### Basic Usage
626
+
627
+ ```ruby
628
+ # Simple schema with basic types
629
+ format = Raix::ResponseFormat.new("PersonInfo", {
630
+ name: { type: "string" },
631
+ age: { type: "integer" }
632
+ })
633
+
634
+ # Use in chat completion
635
+ my_ai.chat_completion(response_format: format)
636
+ ```
637
+
638
+ ### Complex Structures
639
+
640
+ ```ruby
641
+ # Nested structure with arrays
642
+ format = Raix::ResponseFormat.new("CompanyInfo", {
643
+ company: {
644
+ name: { type: "string" },
645
+ employees: [
646
+ {
647
+ name: { type: "string" },
648
+ role: { type: "string" },
649
+ skills: ["string"]
650
+ }
651
+ ],
652
+ locations: ["string"]
653
+ }
654
+ })
655
+ ```
656
+
657
+ ### Generated Schema
658
+
659
+ The ResponseFormat class generates a schema that follows this structure:
660
+
661
+ ```json
662
+ {
663
+ "type": "json_schema",
664
+ "json_schema": {
665
+ "name": "SchemaName",
666
+ "schema": {
667
+ "type": "object",
668
+ "properties": {
669
+ "property1": { "type": "string" },
670
+ "property2": { "type": "integer" }
671
+ },
672
+ "required": ["property1", "property2"],
673
+ "additionalProperties": false
674
+ },
675
+ "strict": true
676
+ }
677
+ }
678
+ ```
679
+
680
+ ### Using with Chat Completion
681
+
682
+ When used with chat completion, the AI model will format its response according to your schema:
683
+
684
+ ```ruby
685
+ class StructuredResponse
686
+ include Raix::ChatCompletion
687
+
688
+ def analyze_person(name)
689
+ format = Raix::ResponseFormat.new("PersonAnalysis", {
690
+ full_name: { type: "string" },
691
+ age_estimate: { type: "integer" },
692
+ personality_traits: ["string"]
693
+ })
694
+
695
+ transcript << { user: "Analyze the person named #{name}" }
696
+ chat_completion(params: { response_format: format })
697
+ end
698
+ end
699
+
700
+ response = StructuredResponse.new.analyze_person("Alice")
701
+ # Returns a hash matching the defined schema
702
+ ```
703
+
704
+ ## Installation
705
+
706
+ Install the gem and add to the application's Gemfile by executing:
707
+
708
+ $ bundle add raix
709
+
710
+ If bundler is not being used to manage dependencies, install the gem by executing:
711
+
712
+ $ gem install raix
713
+
714
+ If you are using the default OpenRouter API, Raix expects `Raix.configuration.openrouter_client` to initialized with the OpenRouter API client instance.
715
+
716
+ You can add an initializer to your application's `config/initializers` directory that looks like this example (setting up both providers, OpenRouter and OpenAI):
717
+
718
+ ```ruby
719
+ # config/initializers/raix.rb
720
+ OpenRouter.configure do |config|
721
+ config.faraday do |f|
722
+ f.request :retry, retry_options
723
+ f.response :logger, Logger.new($stdout), { headers: true, bodies: true, errors: true } do |logger|
724
+ logger.filter(/(Bearer) (\S+)/, '\1[REDACTED]')
725
+ end
726
+ end
727
+ end
728
+
729
+ Raix.configure do |config|
730
+ config.openrouter_client = OpenRouter::Client.new(access_token: ENV.fetch("OR_ACCESS_TOKEN", nil))
731
+ config.openai_client = OpenAI::Client.new(access_token: ENV.fetch("OAI_ACCESS_TOKEN", nil)) do |f|
732
+ f.request :retry, retry_options
733
+ f.response :logger, Logger.new($stdout), { headers: true, bodies: true, errors: true } do |logger|
734
+ logger.filter(/(Bearer) (\S+)/, '\1[REDACTED]')
735
+ end
736
+ end
737
+ end
738
+ ```
739
+
740
+ You will also need to configure the OpenRouter API access token as per the instructions here: https://github.com/OlympiaAI/open_router?tab=readme-ov-file#quickstart
741
+
742
+ ### Global vs class level configuration
743
+
744
+ You can either configure Raix globally or at the class level. The global configuration is set in the initializer as shown above. You can however also override all configuration options of the `Configuration` class on the class level with the
745
+ same syntax:
746
+
747
+ ```ruby
748
+ class MyClass
749
+ include Raix::ChatCompletion
750
+
751
+ configure do |config|
752
+ config.openrouter_client = OpenRouter::Client.new # with my special options
753
+ end
754
+ end
755
+ ```
756
+
757
+ ## Development
758
+
759
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
760
+
761
+ Specs require `OR_ACCESS_TOKEN` and `OAI_ACCESS_TOKEN` environment variables, for access to OpenRouter and OpenAI, respectively. You can add those keys to a local unversioned `.env` file and they will be picked up by the `dotenv` gem.
762
+
763
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
764
+
765
+ ## Contributing
766
+
767
+ Bug reports and pull requests are welcome on GitHub at https://github.com/OlympiaAI/raix. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/OlympiaAI/raix/blob/main/CODE_OF_CONDUCT.md).
768
+
769
+ ## License
770
+
771
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
772
+
773
+ ## Code of Conduct
774
+
775
+ Everyone interacting in the Raix project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/OlympiaAI/raix/blob/main/CODE_OF_CONDUCT.md).