ai_client 0.2.4 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7b12150b0f3f29804d520aa319a99cc71adc96f023ec99bccc0a9a6682bdba6e
4
- data.tar.gz: 59d811a6b9de974f76672a9a1386d6efd3e4547d53ae7b4d24c532a250c00c86
3
+ metadata.gz: 2453f748447f37e755f0087615c845570fe3ea3c4bd06a687947dceedee3e89b
4
+ data.tar.gz: 79359bcd209448add248514d9a8ee5dc4e0fa69833666df0e779715b574d450a
5
5
  SHA512:
6
- metadata.gz: 0beacebae38d5c2101498a59245c5112469ff8fddda2723b5d916ae6d5f731fa6e9e55de46422a6daf463242356e1575c0bbdf77612f9edf9fd05d6b4f885407
7
- data.tar.gz: 36b2b5f5fd3d51577b31ac8cbcd57980cb7d0a912428f37e8f345764620aa02198a65b905505136ea40ea2866e274b1f8d6b9b1c9390ccd3608eaad138c37ebb
6
+ metadata.gz: 28007804ea1e223b22846cc199c4bd14d7349f5e051bbc7007ced3641e7174c3fac59b7a225bc4926ffc68d5018a35ed96a8809bf959c8007e978bc831770b9a
7
+ data.tar.gz: 18ce04f9f83d0c12caadab051b81c48f7dbafa73ff0b2a34df28e3ddfb5ff3c088dcec019a15bc2f2dfc909bb6db0a308b512b6711baaab058629705a1448040
data/CHANGELOG.md CHANGED
@@ -1,6 +1,15 @@
1
1
  ## [Unreleased]
2
2
 
3
3
  ## Released
4
+
5
+ ### [0.3.0] - 2024-10-13
6
+ - Breaking Change
7
+ - Added new class AiClient::Function to encapsulate the callback functions used as tools in chats.
8
+ - Updated the examples/tools.rb file to use the new function class
9
+
10
+ ### [0.2.5] - 2024-10-11
11
+ - Added examples/tools.rb to demonstrate use of function callbacks to provide information to the LLM when it needs it.
12
+
4
13
  ### [0.2.4] - 2024-10-10
5
14
  - constrained gem omniai-openai to version 1.8.3+ for access to open_router.ai
6
15
  - caching models database from open_router.ai
data/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  First and foremost a big **THANK YOU** to [Kevin Sylvestre](https://ksylvest.com/) for his gem [OmniAI](https://github.com/ksylvest/omniai) and [Olympia](https://olympia.chat/) for their [open_router gem](https://github.com/OlympiaAI/open_router) upon which this effort depends.
4
4
 
5
+ Version 0.3.0 has a breaking change w/r/t how [Callback Functions (aka Tools)](#callback-functions-aka-tools) are defined and used.
6
+
5
7
  See the [change log](CHANGELOG.md) for recent modifications.
6
8
 
7
9
 
@@ -33,7 +35,8 @@ See the [change log](CHANGELOG.md) for recent modifications.
33
35
  - [transcribe](#transcribe)
34
36
  - [Options](#options)
35
37
  - [Advanced Prompts](#advanced-prompts)
36
- - [Advanced Prompts with Tools](#advanced-prompts-with-tools)
38
+ - [Callback Functions (aka Tools)](#callback-functions-aka-tools)
39
+ - [Defining a Callback Function](#defining-a-callback-function)
37
40
  - [Best ?? Practices](#best--practices)
38
41
  - [OmniAI and OpenRouter](#omniai-and-openrouter)
39
42
  - [Contributing](#contributing)
@@ -278,14 +281,55 @@ completion #=> 'The photos are of a cat, a dog, and a hamster.'
278
281
 
279
282
  Of course if `client.config.return_raw` is true, the completion value will be the complete response object.
280
283
 
281
- ### Advanced Prompts with Tools
282
284
 
283
- One of the latest innovations in LLMs is the ability to use functions (aka tools) as `callbacks` to gather more information or to execute a task at the direction of the LLM prompt processing.
285
+ ### Callback Functions (aka Tools)
286
+
287
+ With the release of version 0.3.0, the way callback functions (also referred to as tools) are defined in the `ai_client` gem has undergone significant changes. This section outlines the new approach in detail. These changes are designed to create a clearer and more robust interface for developers when working with callback functions. If you encounter any issues while updating your functions, please consult the official documentation or raise an issue in the repository.
288
+
289
+ ##### Defining a Callback Function
290
+
291
+ To define a callback function, you need to create a subclass of `AiClient::Function`. In this subclass, both the `call` and `details` methods must be implemented.
284
292
 
285
- See [blog post](https://ksylvest.com/posts/2024-08-16/using-omniai-to-leverage-tools-with-llms) by Kevin Sylvestre, author of the OmniAI gem.
293
+ **Example**
286
294
 
295
+ Here's an example illustrating how to define a callback function using the new convention:
287
296
 
288
- TODO: Need to create an example RAG that does not need another access token to a service
297
+ ```ruby
298
+ class WeatherFunction < AiClient::Function
299
+ # The call class method returns a String to be used by the LLM
300
+ def self.call(location:, unit: 'Celsius')
301
+ "#{rand(20..50)}° #{unit} in #{location}"
302
+ end
303
+
304
+ # The details method must return a hash with metadata about the function.
305
+ def self.details
306
+ {
307
+ name: 'weather',
308
+ description: "Lookup the weather in a location",
309
+ parameters: AiClient::Tool::Parameters.new(
310
+ properties: {
311
+ location: AiClient::Tool::Property.string(description: 'e.g. Toronto'),
312
+ unit: AiClient::Tool::Property.string(enum: %w[Celsius Fahrenheit]),
313
+ },
314
+ required: [:location]
315
+ )
316
+ }
317
+ end
318
+ end
319
+
320
+ # Register the WeatherFunction for use.
321
+ WeatherFunction.register
322
+
323
+ # Use the *.details[:name] value to reference the tools available for
324
+ # the LLM to use in processing the prompt.
325
+ response = AI.chat("what is the weather in London", tools: ['weather'])
326
+ ```
327
+
328
+ In this example:
329
+ - The `call` method is defined to accept named parameters: `location` and `unit`. The default value for `unit` is set to `'Celsius'`.
330
+ - The `details` method provides metadata about the function, ensuring that the parameters section clearly indicates which parameters are required.
331
+
332
+ See the [examples/tools.rb file](examples/tools.rb) for additional examples.
289
333
 
290
334
 
291
335
  ## Best ?? Practices
@@ -303,6 +347,7 @@ AI.speak "warning Will Robinson! #{bad_things_happened}"
303
347
 
304
348
  Using the constant for the instance allows you to reference the same client instance inside any method through out your application. Of course it does not apply to only one instance. You could assign multiple instances for different models/providers. For example you could have `AI` for your primary client and `AIbackup` for a fallback client in case you have a problem on the primary; or, maybe `Vectorizer` as a client name tied to a model specializing in embedding vectorization.
305
349
 
350
+
306
351
  ## OmniAI and OpenRouter
307
352
 
308
353
  Both OmniAI and OpenRouter have similar goals - to provide a common interface to multiple providers and LLMs. OmniAI is a Ruby gem that supports specific providers directly using a common-ish API. You incur costs directly from those providers for which you have individual API keys (aka access tokens.) OpenRouter, on the other hand, is a web service that also establishes a common API for many providers and models; however, OpenRouter adds a small fee on top of the fee charged by those providers. You trade off cost for flexibility. With OpenRouter you only need one API key (OPEN_ROUTER_API_KEY) to access all of its supported services.
data/examples/README.md CHANGED
@@ -7,6 +7,7 @@
7
7
  | embed.rb | Demonstrates using Ollama locally to vectorize text for embeddings|
8
8
  | speak.rb | Demonstrates using OpenAI's text to speech models |
9
9
  | text.rb | Demonstrates text-to-text transformers "chat" |
10
+ | tools.rb | Demonstrates usage of functional callbacks (i.e. tools) |
10
11
  | transcribe.rb | Uses OpenAI's audio-to-text model |
11
12
 
12
13
  Many of these example programs show both the raw response object as well as just the
data/examples/tools.rb ADDED
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env ruby
2
+ # examples/tools.rb
3
+ #
4
+ # Uses the AiClient::Function class to encapsulate the
5
+ # tools used as callback functions when specified in a
6
+ # chat prompt.
7
+
8
+
9
+ require_relative 'common'
10
+
11
+ AI = AiClient.new('gpt-4o')
12
+
13
+ box "Random Weather (temperature) Example"
14
+ title "Uses two named parameters to the callback function"
15
+
16
+ # Example subclass implementation
17
+ class WeatherFunction < AiClient::Function
18
+ def self.call(location:, unit: 'Celsius')
19
+ "#{rand(20..50)}° #{unit} in #{location}"
20
+ end
21
+
22
+ # Encapsulates registration details for the function
23
+ def self.details
24
+ # SMELL: reconcile regester_tool and details
25
+ {
26
+ name: 'weather', # Must be a String
27
+ description: "Lookup the weather in a location",
28
+ parameters: AiClient::Tool::Parameters.new(
29
+ properties: {
30
+ location: AiClient::Tool::Property.string(description: 'e.g. Toronto'),
31
+ unit: AiClient::Tool::Property.string(enum: %w[Celsius Fahrenheit]),
32
+ },
33
+ required: [:location]
34
+ )
35
+ }
36
+ end
37
+ end
38
+
39
+ # Register the tool for MyFunction
40
+ WeatherFunction.register
41
+
42
+ simple_prompt = <<~TEXT
43
+ What is the weather in "London" in Celsius and "Paris" in Fahrenheit?
44
+ Also what are some ideas for activities in both cities given the weather?
45
+ TEXT
46
+
47
+ response = AI.chat(
48
+ simple_prompt,
49
+ tools: ['weather'] # must match the details[:name] value
50
+ )
51
+
52
+ puts response
53
+
54
+
55
+ ##########################################
56
+ box "Accessing a database to get information"
57
+ title "Uses one named parameter to the callback function"
58
+
59
+ class LLMDetailedFunction < AiClient::Function
60
+ def self.call(model_name:)
61
+ records = AiClient::LLM.where(id: /#{model_name}/i)&.first
62
+ records.inspect
63
+ end
64
+
65
+ def self.details
66
+ {
67
+ name: 'llm_db',
68
+ description: 'lookup details about an LLM model name',
69
+ parameters: AiClient::Tool::Parameters.new(
70
+ properties: {
71
+ model_name: AiClient::Tool::Property.string
72
+ },
73
+ required: %i[model_name]
74
+ )
75
+ }
76
+ end
77
+ end
78
+
79
+ # Registering the LLM detail function
80
+ LLMDetailedFunction.register
81
+
82
+ simple_prompt = <<~PROMPT
83
+ Get the details on an LLM model named 'bison' from the models database
84
+ of #{AiClient::LLM.count} models. Format the details for the model
85
+ using markdown. Format pricing information in terms of number of
86
+ tokens per US dollar.
87
+ PROMPT
88
+
89
+ response = AI.chat(simple_prompt, tools: ['llm_db'])
90
+ puts response
91
+
92
+
93
+
94
+ ##########################################
95
+
96
+ box "Using a function class and multiple tools"
97
+ title "Callback function has no parameters; but uses two functions"
98
+
99
+ class PerfectDateFunction < AiClient::Function
100
+ def self.call
101
+ "April 25th, it's not too hot nor too cold."
102
+ end
103
+
104
+ def self.details
105
+ {
106
+ name: 'perfect_date',
107
+ description: 'what is the perfect date'
108
+ }
109
+ end
110
+ end
111
+
112
+ # Registering the perfect date function
113
+ PerfectDateFunction.register
114
+
115
+ response = AI.chat("what is the perfect date for current weather in Paris?",
116
+ tools: %w[weather perfect_date])
117
+ puts response
118
+ puts
119
+
120
+ debug_me{[
121
+ #'AiClient::Function.registry',
122
+ 'AiClient::Function.functions'
123
+ ]}
@@ -10,7 +10,19 @@ class AiClient
10
10
  # tools: @tools [Array<OmniAI::Tool>] optional
11
11
  # temperature: @temperature [Float, nil] optional
12
12
 
13
- def chat(messages, **params)
13
+ def chat(messages, **params)
14
+ if params.has_key? :tools
15
+ tools = params[:tools]
16
+ if tools.is_a? Array
17
+ tools.map!{|function_name| AiClient::Function.registry[function_name]}
18
+ elsif true == tools
19
+ tools = AiClient::Function.registry.values
20
+ else
21
+ raise 'what is this'
22
+ end
23
+ params[:tools] = tools
24
+ end
25
+
14
26
  result = call_with_middlewares(:chat_without_middlewares, messages, **params)
15
27
  @last_response = result
16
28
  raw? ? result : content
@@ -92,7 +92,11 @@ class AiClient
92
92
  include Hashie::Extensions::Mash::SymbolizeKeys
93
93
  include Hashie::Extensions::Mash::DefineAccessors
94
94
 
95
-
95
+ # Saves the current configuration to the specified file.
96
+ #
97
+ # @param filepath [String] The path to the file where the configuration will be saved.
98
+ # Defaults to '~/aiclient_config.yml' if not provided.
99
+ #
96
100
  def save(filepath=ENV['HOME']+'/aiclient_config.yml')
97
101
  filepath = Pathname.new(filepath) unless filepath.is_a? Pathname
98
102
 
@@ -101,6 +105,13 @@ class AiClient
101
105
 
102
106
 
103
107
  class << self
108
+ # Loads configuration from the specified YAML file.
109
+ #
110
+ # @param filepath [String] The path to the configuration file.
111
+ # Defaults to 'config.yml' if not provided.
112
+ # @return [AiClient::Config] The loaded configuration.
113
+ # @raise [ArgumentError] If the specified file does not exist.
114
+ #
104
115
  def load(filepath=DEFAULT_CONFIG_FILEPATH)
105
116
  filepath = Pathname.new(filepath) unless Pathname == filepath.class
106
117
  if filepath.exist?
@@ -115,17 +126,30 @@ class AiClient
115
126
  class << self
116
127
  attr_accessor :class_config, :default_config
117
128
 
129
+ # Configures the AiClient with a given block.
130
+ #
131
+ # @yieldparam config [AiClient::Config] The configuration instance.
132
+ # @return [void]
133
+ #
118
134
  def configure(&block)
119
135
  yield(class_config)
120
136
  end
121
137
 
138
+ # Resets the default configuration to the value defined in the class.
139
+ #
140
+ # @return [void]
141
+ #
122
142
  def reset_default_config
123
143
  initialize_defaults
124
144
  .save(Config::DEFAULT_CONFIG_FILEPATH)
125
145
  end
126
146
 
127
147
  private
128
-
148
+
149
+ # Initializes the default configuration.
150
+ #
151
+ # @return [void]
152
+ #
129
153
  def initialize_defaults
130
154
  @default_config = Config.new(
131
155
  logger: Logger.new(STDOUT),
@@ -0,0 +1,100 @@
1
+ # lib/ai_client/function.rb
2
+
3
+ class AiClient
4
+
5
+ # The Function class serves as a base class for creating functions
6
+ # that can be registered and managed within the AiClient.
7
+ #
8
+ # Subclasses must implement the `call` and `details` methods
9
+ # to define their specific behavior and properties.
10
+ #
11
+ class Function
12
+ @@registry = {} # key is known by name (from details) and value is the AiClient::Tool
13
+
14
+ class << self
15
+
16
+ # Calls the function with the provided parameters.
17
+ #
18
+ # @param params [Hash] Named parameters required by the function.
19
+ # @raise [NotImplementedError] if not implemented in the subclass.
20
+ #
21
+ def call(**params)
22
+ raise NotImplementedError, "You must implement the call method"
23
+ end
24
+
25
+ # Provides the details about the function including its metadata.
26
+ #
27
+ # @return [Hash] Metadata containing details about the function.
28
+ # @raise [NotImplementedError] if not implemented in the subclass.
29
+ #
30
+ def details
31
+ raise NotImplementedError, "You must implement the details method"
32
+ end
33
+
34
+
35
+ # Registers a tool with the specified properties and parameters.
36
+ #
37
+ # This method creates an instance of AiClient::Tool with the
38
+ # function class and its details and adds it to the registry.
39
+ #
40
+ def register
41
+ this_tool = AiClient::Tool.new(
42
+ self, # This is the sub-class
43
+ **details
44
+ )
45
+
46
+ registry[known_by] = this_tool
47
+ end
48
+ alias_method :enable, :register
49
+
50
+
51
+ # Disables the function by removing its name from the registry.
52
+ #
53
+ # @return [void]
54
+ #
55
+ def disable
56
+ registry.delete(known_by)
57
+ end
58
+
59
+
60
+ # Returns a list of enabled functions.
61
+ #
62
+ # @return [Array<Symbol>] Sorted list of function names.
63
+ #
64
+ def functions
65
+ registry.keys.sort
66
+ end
67
+
68
+
69
+ # Returns the registry of currently registered functions.
70
+ # This method is private to limit access to the registry's state.
71
+ #
72
+ # @return [Hash] The registry of registered functions.
73
+ #
74
+ def registry
75
+ @@registry
76
+ end
77
+
78
+
79
+ # Returns the name under which the function is known.
80
+ #
81
+ # @return [Symbol] The symbol representation of the function's name.
82
+ #
83
+ def known_by
84
+ details[:name]
85
+ end
86
+
87
+
88
+ private
89
+
90
+ # Converts the class name to a symbol (e.g., MyFunction -> :my_function).
91
+ #
92
+ # @return [Symbol] The function name derived from the class name.
93
+ #
94
+ def function_name
95
+ name.split('::').last.gsub(/([a-z])([A-Z])/, '\1_\2').downcase
96
+ end
97
+ end
98
+ end
99
+ end
100
+
data/lib/ai_client/llm.rb CHANGED
@@ -9,11 +9,26 @@ class AiClient
9
9
  DATA_PATH = Pathname.new( __dir__ + '/models.yml')
10
10
  self.data = YAML.parse(DATA_PATH.read).to_ruby
11
11
 
12
+ # Extracts the model name from the LLM ID.
13
+ #
14
+ # @return [String] the model name.
15
+ #
12
16
  def model = id.split('/')[1]
17
+
18
+ # Extracts the provider name from the LLM ID.
19
+ #
20
+ # @return [String] the provider name.
21
+ #
13
22
  def provider = id.split('/')[0]
14
23
  end
15
24
 
16
25
  class << self
26
+
27
+ # Resets the LLM data by fetching models from the Orc client
28
+ # and writing it to the models.yml file.
29
+ #
30
+ # @return [void]
31
+ #
17
32
  def reset_llm_data
18
33
  orc_models = AiClient.orc_client.models
19
34
  AiClient::LLM.data = orc_models
@@ -16,10 +16,23 @@ class AiClient
16
16
  # )
17
17
 
18
18
  class LoggingMiddleware
19
+
20
+ # Initializes the LoggingMiddleware with a logger.
21
+ #
22
+ # @param logger [Logger] The logger used for logging middleware actions.
23
+ #
19
24
  def initialize(logger)
20
25
  @logger = logger
21
26
  end
22
27
 
28
+ # Calls the next middleware in the stack while logging the start and finish times.
29
+ #
30
+ # @param client [Object] The client instance.
31
+ # @param next_middleware [Proc] The next middleware to call.
32
+ # @param args [Array] The arguments passed to the middleware call, with the first being the method name.
33
+ #
34
+ # @return [Object] The result of the next middleware call.
35
+ #
23
36
  def call(client, next_middleware, *args)
24
37
  method_name = args.first.is_a?(Symbol) ? args.first : 'unknown method'
25
38
  @logger.info("Starting #{method_name} call")
@@ -8,9 +8,19 @@
8
8
  # Change this so that middleware can be added
9
9
  # and removed from an existing client.
10
10
 
11
-
11
+ # AiClient class that handles middleware functionality
12
+ # for API calls.
12
13
  class AiClient
13
14
 
15
+ # Calls the specified method with middlewares applied.
16
+ #
17
+ # @param method [Symbol] the name of the method to be called
18
+ # @param args [Array] additional arguments for the method
19
+ # @param kwargs [Hash] named parameters for the method
20
+ # @param block [Proc] optional block to be passed to the method
21
+ #
22
+ # @return [Object] result of the method call after applying middlewares
23
+ #
14
24
  def call_with_middlewares(method, *args, **kwargs, &block)
15
25
  stack = self.class.middlewares.reverse.reduce(-> { send(method, *args, **kwargs, &block) }) do |next_middleware, middleware|
16
26
  -> { middleware.call(self, next_middleware, *args, **kwargs) }
@@ -21,17 +31,30 @@ class AiClient
21
31
 
22
32
  class << self
23
33
 
34
+ # Returns the list of middlewares applied to the client.
35
+ #
36
+ # @return [Array] list of middlewares
37
+ #
24
38
  def middlewares
25
39
  @middlewares ||= []
26
40
  end
27
41
 
42
+ # Adds a middleware to the stack.
43
+ #
44
+ # @param middleware [Proc] the middleware to be added
45
+ #
46
+ # @return [void]
47
+ #
28
48
  def use(middleware)
29
49
  middlewares << middleware
30
50
  end
31
51
 
52
+ # Clears all middlewares from the client.
53
+ #
54
+ # @return [void]
55
+ #
32
56
  def clear_middlewares
33
57
  @middlewares = []
34
58
  end
35
59
  end
36
-
37
60
  end
@@ -8,14 +8,53 @@ require 'open_router'
8
8
  require 'yaml'
9
9
 
10
10
  class AiClient
11
+
12
+ # Retrieves the available models.
13
+ #
14
+ # @return [Array<String>] List of model IDs.
15
+ #
16
+ def models
17
+ self.class.models
18
+ end
19
+
20
+ # Retrieves the available providers.
21
+ #
22
+ # @return [Array<String>] List of provider names.
23
+ def providers
24
+ self.class.providers
25
+ end
26
+
27
+ # Retrieves model names, optionally filtered by provider.
28
+ #
29
+ # @param provider [String, nil] The provider to filter models by.
30
+ # @return [Array<String>] List of model names.
31
+ def model_names(provider = nil)
32
+ self.class.model_names(provider)
33
+ end
34
+
35
+ # Retrieves details for a specific model.
36
+ #
37
+ # @param a_model [String] The model ID to retrieve details for.
38
+ # @return [Hash, nil] Details of the model or nil if not found.
39
+ def model_details(a_model)
40
+ self.class.model_details(a_model)
41
+ end
42
+
43
+ # Finds models matching a given substring.
44
+ #
45
+ # @param a_model_substring [String] The substring to search for.
46
+ # @return [Array<String>] List of matching model names.
47
+ def find_model(a_model_substring)
48
+ self.class.find_model(a_model_substring)
49
+ end
11
50
 
12
- def models = self.class.models
13
- def providers = self.class.providers
14
- def model_names(a_provider=nil) = self.class.model_names(a_provider)
15
- def model_details(a_model) = self.class.model_details(a_model)
16
- def find_model(a_model_substring) = self.class.find_model(a_model_substring)
17
51
 
18
52
  class << self
53
+
54
+ # Adds OpenRouter extensions to AiClient.
55
+ #
56
+ # @return [void]
57
+ #
19
58
  def add_open_router_extensions
20
59
  access_token = fetch_access_token
21
60
 
@@ -25,10 +64,18 @@ class AiClient
25
64
  initialize_orc_client
26
65
  end
27
66
 
67
+ # Retrieves ORC client instance.
68
+ #
69
+ # @return [OpenRouter::Client] Instance of the OpenRouter client.
70
+ #
28
71
  def orc_client
29
72
  @orc_client ||= add_open_router_extensions || raise("OpenRouter extensions are not available")
30
73
  end
31
74
 
75
+ # Retrieves models from the ORC client.
76
+ #
77
+ # @return [Array<Hash>] List of models.
78
+ #
32
79
  def orc_models
33
80
  @orc_models ||= orc_client.models
34
81
  end
@@ -36,6 +83,11 @@ class AiClient
36
83
  # TODO: Refactor these DB like methods to take
37
84
  # advantage of AiClient::LLM
38
85
 
86
+ # Retrieves model names associated with a provider.
87
+ #
88
+ # @param provider [String, nil] The provider to filter models by.
89
+ # @return [Array<String>] List of model names.
90
+ #
39
91
  def model_names(provider=nil)
40
92
  model_ids = models.map { _1['id'] }
41
93
 
@@ -44,18 +96,36 @@ class AiClient
44
96
  model_ids.filter_map { _1.split('/')[1] if _1.start_with?(provider.to_s.downcase) }
45
97
  end
46
98
 
99
+ # Retrieves details of a specific model.
100
+ #
101
+ # @param model [String] The model ID to retrieve details for.
102
+ # @return [Hash, nil] Details of the model or nil if not found.
103
+ #
47
104
  def model_details(model)
48
105
  orc_models.find { _1['id'].include?(model) }
49
106
  end
50
107
 
108
+ # Retrieves the available providers.
109
+ #
110
+ # @return [Array<String>] List of unique provider names.
111
+ #
51
112
  def providers
52
113
  @providers ||= models.map{ _1['id'].split('/')[0] }.sort.uniq
53
114
  end
54
115
 
116
+ # Finds models matching a given substring.
117
+ #
118
+ # @param a_model_substring [String] The substring to search for.
119
+ # @return [Array<String>] List of matching model names.
120
+ #
55
121
  def find_model(a_model_substring)
56
122
  model_names.select{ _1.include?(a_model_substring) }
57
123
  end
58
124
 
125
+ # Resets LLM data with the available ORC models.
126
+ #
127
+ # @return [void]
128
+ #
59
129
  def reset_llm_data
60
130
  LLM.data = orc_models
61
131
  LLM::DATA_PATH.write(orc_models.to_yaml)
@@ -64,23 +134,34 @@ class AiClient
64
134
 
65
135
  private
66
136
 
67
- # Similar to fetch_api_key but for the class_config
137
+ # Fetches the access token from environment variables.
138
+ #
139
+ # @return [String, nil] The access token or nil if not found.
140
+ #
68
141
  def fetch_access_token
69
142
  class_config.envar_api_key_names.open_router
70
143
  .map { |key| ENV[key] }
71
144
  .compact
72
145
  .first
73
146
  end
74
-
147
+
148
+ # Configures the OpenRouter client with the access token.
149
+ #
150
+ # @param access_token [String] The access token to configure.
151
+ # @return [void]
152
+ #
75
153
  def configure_open_router(access_token)
76
154
  OpenRouter.configure { |config| config.access_token = access_token }
77
155
  end
78
156
 
157
+ # Initializes the ORC client.
158
+ #
159
+ # @return [void]
160
+ #
79
161
  def initialize_orc_client
80
162
  @orc_client ||= OpenRouter::Client.new
81
163
  end
82
164
  end
83
165
  end
84
166
 
85
-
86
167
  AiClient.add_open_router_extensions
@@ -11,12 +11,27 @@ class AiClient
11
11
  # )
12
12
  #
13
13
  class RetryMiddleware
14
+
15
+ # Initializes a new instance of RetryMiddleware.
16
+ #
17
+ # @param max_retries [Integer] The maximum number of retries to attempt (default: 3).
18
+ # @param base_delay [Integer] The base delay in seconds before retrying (default: 2).
19
+ # @param max_delay [Integer] The maximum delay in seconds between retries (default: 16).
20
+ #
14
21
  def initialize(max_retries: 3, base_delay: 2, max_delay: 16)
15
22
  @max_retries = max_retries
16
23
  @base_delay = base_delay
17
24
  @max_delay = max_delay
18
25
  end
19
26
 
27
+ # Calls the next middleware, retrying on specific errors.
28
+ #
29
+ # @param client [AiClient] The client instance that invokes the middleware.
30
+ # @param next_middleware [Proc] The next middleware in the chain to call.
31
+ # @param args [Array] Any additional arguments to pass to the next middleware.
32
+ #
33
+ # @raise [StandardError] Reraise the error if max retries are exceeded.
34
+ #
20
35
  def call(client, next_middleware, *args)
21
36
  retries = 0
22
37
  begin
@@ -0,0 +1,18 @@
1
+ # lib/ai_client/tool.rb
2
+
3
+ # TODO: Turn this into a Function class using the pattern
4
+ # in examples/tools.rb
5
+ # put the function names as symbols into a class Array
6
+ # In the AiClient class transform the tools: []
7
+ # parameter from an Array of Symbols into an Array
8
+ # of FUnction instances.
9
+
10
+ class AiClient::Tool < OmniAI::Tool
11
+
12
+ def xyzzy = self.class.xyzzy
13
+
14
+ class << self
15
+ def xyzzy = puts "Magic"
16
+ end
17
+ end
18
+
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class AiClient
4
- VERSION = "0.2.4"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/ai_client.rb CHANGED
@@ -16,6 +16,8 @@ require 'omniai/openai'
16
16
 
17
17
  require 'open_router'
18
18
 
19
+ require_relative 'ai_client/version'
20
+
19
21
  require_relative 'ai_client/chat'
20
22
  require_relative 'ai_client/embed'
21
23
  require_relative 'ai_client/speak'
@@ -23,10 +25,11 @@ require_relative 'ai_client/transcribe'
23
25
 
24
26
  require_relative 'ai_client/configuration'
25
27
  require_relative 'ai_client/middleware'
26
- require_relative 'ai_client/version'
27
28
 
28
29
  require_relative 'ai_client/open_router_extensions'
29
30
  require_relative 'ai_client/llm' # SMELL: must come after the open router stuff
31
+ require_relative 'ai_client/tool'
32
+ require_relative 'ai_client/function'
30
33
 
31
34
  # Create a generic client instance using only model name
32
35
  # client = AiClient.new('gpt-3.5-turbo')
@@ -76,6 +79,8 @@ class AiClient
76
79
  :timeout,
77
80
  :config # Instance configuration
78
81
 
82
+ # Initializes a new AiClient instance.
83
+ #
79
84
  # You can over-ride the class config by providing a block like this
80
85
  # c = AiClient.new(...) do |config|
81
86
  # config.logger = nil
@@ -90,6 +95,14 @@ class AiClient
90
95
  # The options object is basically those things that the
91
96
  # OmniAI clients want to see.
92
97
  #
98
+ # @param model [String] The model name to use for the client.
99
+ # @param options [Hash] Optional named parameters:
100
+ # - :provider [Symbol] Specify the provider.
101
+ # - :config [String] Path to a YAML configuration file.
102
+ # - :logger [Logger] Logger instance for the client.
103
+ # - :timeout [Integer] Timeout value for requests.
104
+ # @yield [config] An optional block to configure the instance.
105
+ #
93
106
  def initialize(model, **options, &block)
94
107
  # Assign the instance variable @config from the class variable @@config
95
108
  @config = self.class.class_config.dup
@@ -121,13 +134,34 @@ class AiClient
121
134
  @last_response = nil
122
135
  end
123
136
 
137
+ # TODO: Review these raw-ish methods are they really needed?
138
+ # raw? should be a private method ??
139
+
140
+ # Returns the last response received from the client.
141
+ #
142
+ # @return [OmniAI::Response] The last response.
143
+ #
124
144
  def response = last_response
145
+
146
+ # Checks if the client is set to return raw responses.
147
+ #
148
+ # @return [Boolean] True if raw responses are to be returned.
125
149
  def raw? = config.return_raw
126
150
 
151
+
152
+ # Sets whether to return raw responses.
153
+ #
154
+ # @param value [Boolean] The value to set for raw responses return.
155
+ #
127
156
  def raw=(value)
128
157
  config.return_raw = value
129
158
  end
130
159
 
160
+ # Extracts the content from the last response based on the provider.
161
+ #
162
+ # @return [String] The extracted content.
163
+ # @raise [NotImplementedError] If content extraction is not implemented for the provider.
164
+ #
131
165
  def content
132
166
  case @provider
133
167
  when :localai, :mistral, :ollama, :open_router, :openai
@@ -142,6 +176,13 @@ class AiClient
142
176
  end
143
177
  alias_method :text, :content
144
178
 
179
+ # Handles calls to methods that are missing on the AiClient instance.
180
+ #
181
+ # @param method_name [Symbol] The name of the method called.
182
+ # @param args [Array] Arguments passed to the method.
183
+ # @param block [Proc] Optional block associated with the method call.
184
+ # @return [Object] The result from the underlying client or raises NoMethodError.
185
+ #
145
186
  def method_missing(method_name, *args, &block)
146
187
  if @client.respond_to?(method_name)
147
188
  result = @client.send(method_name, *args, &block)
@@ -152,6 +193,12 @@ class AiClient
152
193
  end
153
194
  end
154
195
 
196
+ # Checks if the instance responds to the missing method.
197
+ #
198
+ # @param method_name [Symbol] The name of the method to check.
199
+ # @param include_private [Boolean] Whether to include private methods in the check.
200
+ # @return [Boolean] True if the method is supported by the client, false otherwise.
201
+ #
155
202
  def respond_to_missing?(method_name, include_private = false)
156
203
  @client.respond_to?(method_name) || super
157
204
  end
@@ -160,6 +207,12 @@ class AiClient
160
207
  ##############################################
161
208
  private
162
209
 
210
+ # Validates the specified provider.
211
+ #
212
+ # @param provider [Symbol] The provider to validate.
213
+ # @return [Symbol, nil] Returns the validated provider or nil.
214
+ # @raise [ArgumentError] If the provider is unsupported.
215
+ #
163
216
  def validate_provider(provider)
164
217
  return nil if provider.nil?
165
218
 
@@ -171,7 +224,11 @@ class AiClient
171
224
  provider
172
225
  end
173
226
 
174
-
227
+ # Creates an instance of the appropriate OmniAI client based on the provider.
228
+ #
229
+ # @return [OmniAI::Client] An instance of the configured OmniAI client.
230
+ # @raise [ArgumentError] If the provider is unsupported.
231
+ #
175
232
  def create_client
176
233
  client_options = {
177
234
  api_key: fetch_api_key,
@@ -209,7 +266,10 @@ class AiClient
209
266
  end
210
267
 
211
268
 
212
- # Similar to fetch_access_tokne but for the instance config
269
+ # Similar to fetch_access_token but for the instance config
270
+ #
271
+ # @return [String, nil] The retrieved API key or nil if not found.
272
+ #
213
273
  def fetch_api_key
214
274
  config.envar_api_key_names[@provider]
215
275
  &.map { |key| ENV[key] }
@@ -217,6 +277,12 @@ class AiClient
217
277
  &.first
218
278
  end
219
279
 
280
+ # Determines the provider based on the provided model.
281
+ #
282
+ # @param model [String] The model name.
283
+ # @return [Symbol] The corresponding provider.
284
+ # @raise [ArgumentError] If the model is unsupported.
285
+ #
220
286
  def determine_provider(model)
221
287
  config.provider_patterns.find { |provider, pattern| model.match?(pattern) }&.first ||
222
288
  raise(ArgumentError, "Unsupported model: #{model}")
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ai_client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.4
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dewayne VanHoozer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-10-10 00:00:00.000000000 Z
11
+ date: 2024-10-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: active_hash
@@ -219,12 +219,14 @@ files:
219
219
  - examples/embed.rb
220
220
  - examples/speak.rb
221
221
  - examples/text.rb
222
+ - examples/tools.rb
222
223
  - examples/transcribe.rb
223
224
  - lib/ai_client.rb
224
225
  - lib/ai_client/chat.rb
225
226
  - lib/ai_client/config.yml
226
227
  - lib/ai_client/configuration.rb
227
228
  - lib/ai_client/embed.rb
229
+ - lib/ai_client/function.rb
228
230
  - lib/ai_client/llm.rb
229
231
  - lib/ai_client/logger_middleware.rb
230
232
  - lib/ai_client/middleware.rb
@@ -232,6 +234,7 @@ files:
232
234
  - lib/ai_client/open_router_extensions.rb
233
235
  - lib/ai_client/retry_middleware.rb
234
236
  - lib/ai_client/speak.rb
237
+ - lib/ai_client/tool.rb
235
238
  - lib/ai_client/transcribe.rb
236
239
  - lib/ai_client/version.rb
237
240
  - sig/ai_client.rbs