ai_client 0.2.5 → 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: f2b330f59ff7f380306ec354ce5f7197adce8dd101a4b506ab137568f098242a
4
- data.tar.gz: f169296b9f20479b1c608f2202d9ab8a8eb416aa80b07f4308ec4648b9c86fcc
3
+ metadata.gz: 2453f748447f37e755f0087615c845570fe3ea3c4bd06a687947dceedee3e89b
4
+ data.tar.gz: 79359bcd209448add248514d9a8ee5dc4e0fa69833666df0e779715b574d450a
5
5
  SHA512:
6
- metadata.gz: ce0e2cddd8ec6935a5bea24125358fbcc8fda902340d7767972142505af41eaeef7d7f6f5791bd7d26469fd45e59020237fd0061e75d6a987d8a4181597cdbe9
7
- data.tar.gz: 4b2513fba82802eda13cf26c1af0e73f0903a2140765401ed686540d80480d887f2152e342a5fe3546c89482320f61cc6bd04b4e41b8851668bbda31b2496fc9
6
+ metadata.gz: 28007804ea1e223b22846cc199c4bd14d7349f5e051bbc7007ced3641e7174c3fac59b7a225bc4926ffc68d5018a35ed96a8809bf959c8007e978bc831770b9a
7
+ data.tar.gz: 18ce04f9f83d0c12caadab051b81c48f7dbafa73ff0b2a34df28e3ddfb5ff3c088dcec019a15bc2f2dfc909bb6db0a308b512b6711baaab058629705a1448040
data/CHANGELOG.md CHANGED
@@ -2,8 +2,13 @@
2
2
 
3
3
  ## Released
4
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
+
5
10
  ### [0.2.5] - 2024-10-11
6
- - Added examples/tool.rb to demonstrate use of function callbacks to provide information to the LLM when it needs it.
11
+ - Added examples/tools.rb to demonstrate use of function callbacks to provide information to the LLM when it needs it.
7
12
 
8
13
  ### [0.2.4] - 2024-10-10
9
14
  - constrained gem omniai-openai to version 1.8.3+ for access to 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,13 +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.
292
+
293
+ **Example**
294
+
295
+ Here's an example illustrating how to define a callback function using the new convention:
296
+
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
+ ```
284
327
 
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.
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.
286
331
 
287
- Take a look at the [examples/tools.rb](examples/tools.rb) file to see different ways in which these callable processes can be defined.
332
+ See the [examples/tools.rb file](examples/tools.rb) for additional examples.
288
333
 
289
334
 
290
335
  ## Best ?? Practices
@@ -302,6 +347,7 @@ AI.speak "warning Will Robinson! #{bad_things_happened}"
302
347
 
303
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.
304
349
 
350
+
305
351
  ## OmniAI and OpenRouter
306
352
 
307
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/tools.rb CHANGED
@@ -1,90 +1,123 @@
1
1
  #!/usr/bin/env ruby
2
2
  # examples/tools.rb
3
- # See: https://ksylvest.com/posts/2024-08-16/using-omniai-to-leverage-tools-with-llms
3
+ #
4
+ # Uses the AiClient::Function class to encapsulate the
5
+ # tools used as callback functions when specified in a
6
+ # chat prompt.
7
+
4
8
 
5
9
  require_relative 'common'
6
10
 
7
11
  AI = AiClient.new('gpt-4o')
8
12
 
9
- box "omniai-openai's random temp example"
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
10
21
 
11
- my_weather_function = Proc.new do |location:, unit: 'Celsius'|
12
- "#{rand(20..50)}° #{unit} in #{location}"
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
13
37
  end
14
38
 
15
- weather = AiClient::Tool.new(
16
- my_weather_function,
17
- name: 'weather',
18
- description: 'Lookup the weather in a location',
19
- parameters: AiClient::Tool::Parameters.new(
20
- properties: {
21
- location: AiClient::Tool::Property.string(description: 'e.g. Toronto'),
22
- unit: AiClient::Tool::Property.string(enum: %w[Celsius Fahrenheit]),
23
- },
24
- required: %i[location]
25
- )
26
- )
39
+ # Register the tool for MyFunction
40
+ WeatherFunction.register
27
41
 
28
42
  simple_prompt = <<~TEXT
29
43
  What is the weather in "London" in Celsius and "Paris" in Fahrenheit?
30
44
  Also what are some ideas for activities in both cities given the weather?
31
45
  TEXT
32
46
 
33
- response = AI.chat(simple_prompt, tools: [weather])
47
+ response = AI.chat(
48
+ simple_prompt,
49
+ tools: ['weather'] # must match the details[:name] value
50
+ )
51
+
34
52
  puts response
35
53
 
54
+
36
55
  ##########################################
37
56
  box "Accessing a database to get information"
57
+ title "Uses one named parameter to the callback function"
38
58
 
39
- llm_db_function = Proc.new do |params|
40
- records = AiClient::LLM.where(id: /#{params[:model_name]}/i)
41
- records.inspect
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
42
77
  end
43
78
 
79
+ # Registering the LLM detail function
80
+ LLMDetailedFunction.register
44
81
 
45
- llm_db = AiClient::Tool.new(
46
- llm_db_function,
47
- name: 'llm_db',
48
- description: 'lookup details about an LLM model name',
49
- parameters: AiClient::Tool::Parameters.new(
50
- properties: {
51
- model_name: AiClient::Tool::Property.string
52
- },
53
- required: %i[model_name]
54
- )
55
- )
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
56
88
 
57
- response = AI.chat("Get details on an LLM model named bison. Which one is the cheapest per prompt token.", tools: [llm_db])
89
+ response = AI.chat(simple_prompt, tools: ['llm_db'])
58
90
  puts response
59
91
 
60
- ##########################################
61
92
 
62
- # TODO: Look at creating a better function
63
- # process such that the tools parameter
64
- # is an Array of Symbols which is
65
- # maintained as a class variable.
66
- # The symboles are looked up and the
67
- # proper instance is inserted in its
68
- # place.
93
+
94
+ ##########################################
69
95
 
70
96
  box "Using a function class and multiple tools"
97
+ title "Callback function has no parameters; but uses two functions"
71
98
 
72
- class FunctionClass
99
+ class PerfectDateFunction < AiClient::Function
73
100
  def self.call
74
- "April 25th its not to hot nor too cold."
101
+ "April 25th, it's not too hot nor too cold."
75
102
  end
76
103
 
77
- def function(my_name)
78
- AiClient::Tool.new(
79
- self.class, # with a self.call method
80
- name: my_name,
81
- description: 'what is the perfect date'
82
- )
104
+ def self.details
105
+ {
106
+ name: 'perfect_date',
107
+ description: 'what is the perfect date'
108
+ }
83
109
  end
84
110
  end
85
111
 
86
- perfect_date = FunctionClass.new.function('perfect_date')
112
+ # Registering the perfect date function
113
+ PerfectDateFunction.register
87
114
 
88
- response = AI.chat("what is the perfect date for paris weather?", tools: [weather, perfect_date])
115
+ response = AI.chat("what is the perfect date for current weather in Paris?",
116
+ tools: %w[weather perfect_date])
89
117
  puts response
90
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
@@ -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
+
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class AiClient
4
- VERSION = "0.2.5"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/ai_client.rb CHANGED
@@ -29,6 +29,7 @@ require_relative 'ai_client/middleware'
29
29
  require_relative 'ai_client/open_router_extensions'
30
30
  require_relative 'ai_client/llm' # SMELL: must come after the open router stuff
31
31
  require_relative 'ai_client/tool'
32
+ require_relative 'ai_client/function'
32
33
 
33
34
  # Create a generic client instance using only model name
34
35
  # client = AiClient.new('gpt-3.5-turbo')
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.5
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-11 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
@@ -226,6 +226,7 @@ files:
226
226
  - lib/ai_client/config.yml
227
227
  - lib/ai_client/configuration.rb
228
228
  - lib/ai_client/embed.rb
229
+ - lib/ai_client/function.rb
229
230
  - lib/ai_client/llm.rb
230
231
  - lib/ai_client/logger_middleware.rb
231
232
  - lib/ai_client/middleware.rb