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 +4 -4
- data/CHANGELOG.md +6 -1
- data/README.md +51 -5
- data/examples/tools.rb +83 -50
- data/lib/ai_client/chat.rb +13 -1
- data/lib/ai_client/function.rb +100 -0
- data/lib/ai_client/version.rb +1 -1
- data/lib/ai_client.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2453f748447f37e755f0087615c845570fe3ea3c4bd06a687947dceedee3e89b
|
4
|
+
data.tar.gz: 79359bcd209448add248514d9a8ee5dc4e0fa69833666df0e779715b574d450a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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/
|
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
|
-
- [
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
#
|
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 "
|
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
|
-
|
12
|
-
|
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
|
-
|
16
|
-
|
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(
|
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
|
-
|
40
|
-
|
41
|
-
|
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
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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(
|
89
|
+
response = AI.chat(simple_prompt, tools: ['llm_db'])
|
58
90
|
puts response
|
59
91
|
|
60
|
-
##########################################
|
61
92
|
|
62
|
-
|
63
|
-
|
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
|
99
|
+
class PerfectDateFunction < AiClient::Function
|
73
100
|
def self.call
|
74
|
-
"April 25th
|
101
|
+
"April 25th, it's not too hot nor too cold."
|
75
102
|
end
|
76
103
|
|
77
|
-
def
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
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
|
-
|
112
|
+
# Registering the perfect date function
|
113
|
+
PerfectDateFunction.register
|
87
114
|
|
88
|
-
response = AI.chat("what is the perfect date for
|
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
|
+
]}
|
data/lib/ai_client/chat.rb
CHANGED
@@ -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
|
+
|
data/lib/ai_client/version.rb
CHANGED
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.
|
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
|
+
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
|