ai_client 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.envrc +3 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE +21 -0
- data/README.md +76 -0
- data/Rakefile +8 -0
- data/examples/Bethany Hamilton.m4a +0 -0
- data/examples/common.rb +24 -0
- data/examples/embed.rb +113 -0
- data/examples/speak.rb +32 -0
- data/examples/text.rb +85 -0
- data/examples/transcribe.rb +59 -0
- data/lib/ai_client/configuration.rb +84 -0
- data/lib/ai_client/logger_middleware.rb +37 -0
- data/lib/ai_client/retry_middleware.rb +37 -0
- data/lib/ai_client/version.rb +5 -0
- data/lib/ai_client.rb +244 -0
- data/lib/extensions/omniai-localai.rb +31 -0
- data/lib/extensions/omniai-ollama.rb +30 -0
- data/sig/ai_client.rbs +4 -0
- data/the_ollama_model_problem.md +5 -0
- metadata +190 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: b12bef21b23a46a47cc409042afd5eaae36d6cf1cdd2fae47f12fd0b958ed961
|
4
|
+
data.tar.gz: 0c70c7f7e562da50876ee65c1f1de81f6ee7d37e97a2a7c114bb692ce5d6155d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 56d14adae8ab29719083dc7bb63edf33fa625237274edef043b3738f39757fa0c15983232c9aab0a2afbf8db33cbcafc89ed0cb39d0180d045e3c2f6f467a5fe
|
7
|
+
data.tar.gz: 5c3d314a10a6aa6f10a51b6f8eaf574dea6d108feb5b5ec64f47fb0e494cc1ebe865a7b2fbc9401aada23c692cae2346a48dbcf062e84a202e080f25f0790d9d
|
data/.envrc
ADDED
data/CHANGELOG.md
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2024 Dewayne VanHoozer
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
# AiClient
|
2
|
+
|
3
|
+
First and foremost a big **THANK YOU** to [Kevin Sylvestre](https://ksylvest.com/) for his gem [OmniAI](https://github.com/ksylvest/omniai) upon which this effort depends.
|
4
|
+
|
5
|
+
**This is a work in progress** Its implemented as a class rather than the typical module for most gems. The `AiClient::Configuration` class is a little first draft-ish. I'm looking to bulk it up a lot. At this point I think some of the current tests are failing; but, over all `AiClien` is working. I've used early versions of it in several projects.
|
6
|
+
|
7
|
+
## Summary
|
8
|
+
|
9
|
+
`ai_client` is a versatile Ruby gem that serves as a generic client for interacting with various AI service providers through a unified API provided by Kevin's gem `OmniAI`. The `AiClient` class is designed to simplify the integration of large language models (LLMs) into applications. `AiClient` allows developers to create instances using just the model name, greatly reducing configuration overhead.
|
10
|
+
|
11
|
+
With built-in support for popular AI providers—including OpenAI, Anthropic, Google, Mistral, LocalAI and Ollama—the gem abstracts the complexities of API interactions, offering methods for tasks such as chatting, transcription, speech synthesis, and embedding.
|
12
|
+
|
13
|
+
The middleware architecture enables customizable processing of requests and responses, making it easy to implement features like logging and retry logic. Seamlessly integrated with the `OmniAI` framework, `ai_client` empowers developers to leverage cutting-edge AI capabilities without vendor lock-in, making it an essential tool for modern AI-driven applications.
|
14
|
+
|
15
|
+
## Installation
|
16
|
+
|
17
|
+
If you are using a Gemfile and bundler in your project just install the gem by executing:
|
18
|
+
|
19
|
+
```bash
|
20
|
+
bundle add ai_client
|
21
|
+
```
|
22
|
+
|
23
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
24
|
+
|
25
|
+
```bash
|
26
|
+
gem install ai_client
|
27
|
+
```
|
28
|
+
|
29
|
+
## Usage
|
30
|
+
|
31
|
+
Basic usage:
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
AI = AiClient.new('gpt-4o')
|
35
|
+
```
|
36
|
+
|
37
|
+
That's it. Just provide the model name that you want to use. If you application is using more than one model, no worries, just create multiple AiClient instances.
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
c1 = AiClient.new('nomic-embeddings-text')
|
41
|
+
c2 = AiClient.new('gpt-4o-mini')
|
42
|
+
```
|
43
|
+
|
44
|
+
### What Now?
|
45
|
+
|
46
|
+
TODO: Document the methods and their options.
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
AI.chat(...)
|
50
|
+
AI.transcribe(...)
|
51
|
+
AI.speak(...)
|
52
|
+
AI.embed(...)
|
53
|
+
AI.batch_embed(...)
|
54
|
+
```
|
55
|
+
|
56
|
+
TODO: see the [examples] directory.
|
57
|
+
|
58
|
+
### System Environment Variables
|
59
|
+
|
60
|
+
The API keys used with each LLM provider have the pattern `XXX_API_KEY` where XXX is the name of the provided. For example `OPENAI_API_KEY1` and `ANTROPIC_API_KEY` etc.
|
61
|
+
|
62
|
+
TODO: list all providers supported and their envar
|
63
|
+
|
64
|
+
### Options
|
65
|
+
|
66
|
+
TODO: document the options like `provider: :ollama`
|
67
|
+
|
68
|
+
## Contributing
|
69
|
+
|
70
|
+
I can sure use your help. This industry is moving faster than I can keep up with. If you have a bug fix or new feature idea then have at it. Send me a pull request so we all can benefit from your efforts.
|
71
|
+
|
72
|
+
If you only have time to report a bug, that's fine. Just create an issue in this repo.
|
73
|
+
|
74
|
+
## License
|
75
|
+
|
76
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
Binary file
|
data/examples/common.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# examples/common.rb
|
2
|
+
#
|
3
|
+
# Stuff common to all the examples
|
4
|
+
|
5
|
+
require_relative '../lib/ai_client'
|
6
|
+
|
7
|
+
require 'amazing_print'
|
8
|
+
|
9
|
+
require 'debug_me'
|
10
|
+
include DebugMe
|
11
|
+
|
12
|
+
def title(a_string='Something Different', chr='=')
|
13
|
+
puts
|
14
|
+
puts a_string
|
15
|
+
puts chr*a_string.size
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
def box(a_string='Something Different', chr='=')
|
20
|
+
a_string = "#{chr*2} #{a_string} #{chr*2}"
|
21
|
+
a_line = "\n" + (chr*a_string.size) + "\n"
|
22
|
+
puts "#{a_line}#{a_string}#{a_line}"
|
23
|
+
puts
|
24
|
+
end
|
data/examples/embed.rb
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# examples/embed.rb
|
3
|
+
|
4
|
+
require_relative 'common'
|
5
|
+
require 'matrix'
|
6
|
+
require 'kmeans-clusterer'
|
7
|
+
|
8
|
+
# We'll use only one model for this example
|
9
|
+
model = 'nomic-embed-text'
|
10
|
+
client = AiClient.new(model, provider: :ollama)
|
11
|
+
|
12
|
+
# More meaningful text samples
|
13
|
+
texts = [
|
14
|
+
"The quick brown fox jumps over the lazy dog.",
|
15
|
+
"Machine learning is a subset of artificial intelligence.",
|
16
|
+
"Natural language processing is crucial for chatbots.",
|
17
|
+
"Deep learning models require large amounts of data.",
|
18
|
+
"Quantum computing may revolutionize cryptography.",
|
19
|
+
"Renewable energy sources include solar and wind power.",
|
20
|
+
"Climate change is affecting global weather patterns.",
|
21
|
+
"Sustainable agriculture practices can help protect the environment.",
|
22
|
+
"The human genome project mapped our DNA sequence.",
|
23
|
+
"CRISPR technology allows for precise gene editing.",
|
24
|
+
"Artificial neural networks are inspired by biological brains.",
|
25
|
+
"The Internet of Things connects everyday devices to the web.",
|
26
|
+
]
|
27
|
+
|
28
|
+
title "Generating Embeddings"
|
29
|
+
|
30
|
+
embeddings = client.batch_embed(texts, batch_size: 1)
|
31
|
+
|
32
|
+
debug_me{[
|
33
|
+
:embeddings,
|
34
|
+
'embeddings.methods.sort'
|
35
|
+
]}
|
36
|
+
|
37
|
+
# Helper function to compute cosine similarity
|
38
|
+
def cosine_similarity(a, b)
|
39
|
+
dot_product = a.zip(b).map { |x, y| x * y }.sum
|
40
|
+
magnitude_a = Math.sqrt(a.map { |x| x**2 }.sum)
|
41
|
+
magnitude_b = Math.sqrt(b.map { |x| x**2 }.sum)
|
42
|
+
dot_product / (magnitude_a * magnitude_b)
|
43
|
+
end
|
44
|
+
|
45
|
+
title "Clustering Embeddings"
|
46
|
+
|
47
|
+
# Convert embeddings to a format suitable for KMeans
|
48
|
+
data = embeddings.map(&:embedding)
|
49
|
+
|
50
|
+
debug_me{[
|
51
|
+
'data.class',
|
52
|
+
'data.size',
|
53
|
+
'data.first.size'
|
54
|
+
]}
|
55
|
+
|
56
|
+
# Perform K-means clustering
|
57
|
+
k = 3 # Number of clusters
|
58
|
+
kmeans = KMeansClusterer.run(k, data, labels: texts, runs: 5)
|
59
|
+
|
60
|
+
puts "Clusters:"
|
61
|
+
kmeans.clusters.each_with_index do |cluster, i|
|
62
|
+
puts "Cluster #{i + 1}:"
|
63
|
+
cluster.points.each { |p| puts " - #{p.label}" }
|
64
|
+
puts
|
65
|
+
end
|
66
|
+
|
67
|
+
title "Finding Similar Texts"
|
68
|
+
sleep 1 # Rate Limit gets exceeded without this
|
69
|
+
|
70
|
+
query_text = "Artificial intelligence and machine learning"
|
71
|
+
query_embedding = client.embed(query_text)
|
72
|
+
|
73
|
+
debug_me{[
|
74
|
+
:query_embedding,
|
75
|
+
'query_embedding.methods.sort'
|
76
|
+
]}
|
77
|
+
|
78
|
+
similarities = texts.zip(embeddings).map do |text, embedding|
|
79
|
+
similarity = cosine_similarity(query_embedding.embedding, embedding.embedding)
|
80
|
+
[text, similarity]
|
81
|
+
end
|
82
|
+
|
83
|
+
puts "Top 3 texts similar to '#{query_text}':"
|
84
|
+
similarities.sort_by { |_, sim| -sim }.first(3).each do |text, sim|
|
85
|
+
puts "#{text} (Similarity: #{sim.round(4)})"
|
86
|
+
end
|
87
|
+
|
88
|
+
title "Simple Classification"
|
89
|
+
|
90
|
+
# Define some categories and their representative texts
|
91
|
+
categories = {
|
92
|
+
"Technology" => "Computers, software, and digital innovations",
|
93
|
+
"Science" => "Scientific research, experiments, and discoveries",
|
94
|
+
"Environment" => "Nature, ecology, and environmental issues"
|
95
|
+
}
|
96
|
+
|
97
|
+
# Generate embeddings for category descriptions
|
98
|
+
category_embeddings = client.batch_embed(categories.values, batch_size: 1)
|
99
|
+
|
100
|
+
# Classify each text
|
101
|
+
puts "Classification results:"
|
102
|
+
texts.each do |text|
|
103
|
+
sleep 1 # DEBUG: Rate Limited
|
104
|
+
text_embedding = client.embed(text)
|
105
|
+
|
106
|
+
# Find the category with the highest similarity
|
107
|
+
best_category = categories.keys.max_by do |category|
|
108
|
+
category_index = categories.keys.index(category)
|
109
|
+
cosine_similarity(text_embedding.embedding, category_embeddings[category_index].embedding)
|
110
|
+
end
|
111
|
+
|
112
|
+
puts "#{text} => #{best_category}"
|
113
|
+
end
|
data/examples/speak.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# examples/speak.rb
|
3
|
+
|
4
|
+
$player = "afplay" # For MacOS
|
5
|
+
|
6
|
+
require_relative 'common'
|
7
|
+
|
8
|
+
def play(audio_file)
|
9
|
+
`#{$player} #{audio_file}`
|
10
|
+
end
|
11
|
+
|
12
|
+
|
13
|
+
models = [
|
14
|
+
'tts-1', # OpenAI
|
15
|
+
# 'google-tts-1', # Google (placeholder, adjust as needed)
|
16
|
+
# 'elevenlabs-v1' # ElevenLabs (if supported)
|
17
|
+
]
|
18
|
+
clients = []
|
19
|
+
|
20
|
+
models.each do |model|
|
21
|
+
clients << AiClient.new(model)
|
22
|
+
end
|
23
|
+
|
24
|
+
title "Default Configuration Text-to-Speech"
|
25
|
+
|
26
|
+
clients.each do |c|
|
27
|
+
puts "\nModel: #{c.model} (#{c.model_type}) Provider: #{c.provider}"
|
28
|
+
text = "Text to speach example using the #{c.model} by provider #{c.provider} with the default voice."
|
29
|
+
result = c.speak(text)
|
30
|
+
puts "Audio generated. Tempfile: #{result.path}"
|
31
|
+
play result.path
|
32
|
+
end
|
data/examples/text.rb
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# examples/text.rb
|
3
|
+
|
4
|
+
require_relative 'common'
|
5
|
+
|
6
|
+
|
7
|
+
###################################
|
8
|
+
## Working with Ollama
|
9
|
+
|
10
|
+
# This is the default configuration which returns
|
11
|
+
# text content from the client.
|
12
|
+
#
|
13
|
+
AiClient.configure do |o|
|
14
|
+
o.return_raw = false
|
15
|
+
end
|
16
|
+
|
17
|
+
title "Using Mistral model with Ollama locally"
|
18
|
+
|
19
|
+
ollama_client = AiClient.new('mistral', provider: :ollama)
|
20
|
+
|
21
|
+
puts "\nModel: mistral Provider: Ollama (local)"
|
22
|
+
result = ollama_client.chat('Hello, how are you?')
|
23
|
+
puts result
|
24
|
+
|
25
|
+
puts "\nRaw response:"
|
26
|
+
puts ollama_client.response.pretty_inspect
|
27
|
+
puts
|
28
|
+
|
29
|
+
|
30
|
+
|
31
|
+
###############################################################
|
32
|
+
## Lets look an generic configurations based upon model name ##
|
33
|
+
###############################################################
|
34
|
+
|
35
|
+
models = [
|
36
|
+
'gpt-3.5-turbo', # OpenAI
|
37
|
+
'claude-2.1', # Anthropic
|
38
|
+
'gemini-1.5-flash', # Google
|
39
|
+
'mistral-large-latest', # Mistral - La Platform
|
40
|
+
]
|
41
|
+
clients = []
|
42
|
+
|
43
|
+
models.each do |model|
|
44
|
+
clients << AiClient.new(model)
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
title "Default Configuration Response to 'hello'"
|
49
|
+
|
50
|
+
clients.each do |c|
|
51
|
+
puts "\nModel: #{c.model} (#{c.model_type}) Provider: #{c.provider}"
|
52
|
+
begin
|
53
|
+
response = c.chat('hello')
|
54
|
+
puts response
|
55
|
+
rescue => e
|
56
|
+
puts e
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
###################################
|
61
|
+
|
62
|
+
AiClient.configure do |o|
|
63
|
+
o.return_raw = true
|
64
|
+
end
|
65
|
+
|
66
|
+
raw_clients = []
|
67
|
+
|
68
|
+
models.each do |model|
|
69
|
+
raw_clients << AiClient.new(model)
|
70
|
+
end
|
71
|
+
|
72
|
+
puts
|
73
|
+
title "Raw Configuration Response to 'hello'"
|
74
|
+
|
75
|
+
raw_clients.each do |c|
|
76
|
+
puts "\nModel: #{c.model} (#{c.model_type}) Provider: #{c.provider}"
|
77
|
+
begin
|
78
|
+
result = c.chat('hello')
|
79
|
+
puts result.pretty_inspect
|
80
|
+
rescue => e
|
81
|
+
puts e
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
puts
|
@@ -0,0 +1,59 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# examples/transcribe.rb
|
3
|
+
|
4
|
+
require_relative 'common'
|
5
|
+
|
6
|
+
box "Bethany Hamilton on Facing Fear"
|
7
|
+
|
8
|
+
audio_file = 'Bethany Hamilton.m4a' # Poor volume level
|
9
|
+
|
10
|
+
models = [
|
11
|
+
'whisper-1', # OpenAI
|
12
|
+
# 'deepgram-nova-2' # Deepgram (if supported)
|
13
|
+
]
|
14
|
+
clients = []
|
15
|
+
|
16
|
+
models.each do |model|
|
17
|
+
clients << AiClient.new(model)
|
18
|
+
end
|
19
|
+
|
20
|
+
title "Default Configuration Speech-to-Text"
|
21
|
+
|
22
|
+
|
23
|
+
clients.each do |c|
|
24
|
+
puts "\nModel: #{c.model} (#{c.model_type}) Provider: #{c.provider}"
|
25
|
+
result = c.transcribe(audio_file)
|
26
|
+
puts "Transcription: #{result.pretty_inspect}"
|
27
|
+
end
|
28
|
+
|
29
|
+
__END__
|
30
|
+
|
31
|
+
Tucker Carlson: How do you deal with fear?
|
32
|
+
|
33
|
+
Bethany Hamilton: Okay, so I deal with fear maybe more naturally and
|
34
|
+
better than your average human, but I would say It's not like a really
|
35
|
+
thoughtful process for me. It's truly just facing my fears and not letting my
|
36
|
+
fears like over take me so much that I get paralyzed. So I think maybe since
|
37
|
+
I, you know, when I lost my arm when I was 13 years old. I had such a deep
|
38
|
+
passion for surfing that my decision to get back in the ocean was based off
|
39
|
+
of like getting back to my passion and my love for riding waves and not just
|
40
|
+
facing my fears, you know, I had like a deeper reason like I just love doing
|
41
|
+
what I did. And so I wanted to see if it was possible with one arm. So I
|
42
|
+
truly just faced my fears and over time, I think facing them over and over
|
43
|
+
and over again. I eventually became less fearful of sharks, so to say. And
|
44
|
+
it's funny, I've heard that sharks and motivational speaking are like
|
45
|
+
people's two greatest fears. That's like the two things that I do I surf with
|
46
|
+
sharks in the ocean, or like, you know, overcome my like incident with the
|
47
|
+
shark and then I do motivational speaking, which I would say I didn't like
|
48
|
+
that at first. But eventually I overcame that like that dislike or that fear
|
49
|
+
or that uncomfortability and I think so often in life where we naturally want
|
50
|
+
to like run from discomfort, you know, we want to make things as easy and
|
51
|
+
comfortable as possible. And so if you can learn to recognize that sometimes
|
52
|
+
you can't do that and sometimes you have to like walk into uncomfortable, you
|
53
|
+
know, I find them like relationships, for example, sometimes you have to have
|
54
|
+
the uncomfortable conversations to make that relationship more beautiful. But
|
55
|
+
a lot of us just want to like avoid that instead. And in the long run, that
|
56
|
+
just makes the relationship less beautiful and less meaningful and less
|
57
|
+
filled with depth, and then eventually that relationship may dissipate.
|
58
|
+
|
59
|
+
Tucker Carlson: Absolutely right.
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# ai_client/configuration.rb
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
class AiClient
|
6
|
+
# TODO: Need a centralized service where
|
7
|
+
# metadata about LLMs are available
|
8
|
+
# via and API call. Would hope that
|
9
|
+
# the providers would add a "list"
|
10
|
+
# endpoint to their API which would
|
11
|
+
# return the metadata for all of their
|
12
|
+
# models.
|
13
|
+
|
14
|
+
PROVIDER_PATTERNS = {
|
15
|
+
anthropic: /^claude/i,
|
16
|
+
openai: /^(gpt|davinci|curie|babbage|ada|whisper|tts|dall-e)/i,
|
17
|
+
google: /^(gemini|palm)/i,
|
18
|
+
mistral: /^(mistral|codestral)/i,
|
19
|
+
localai: /^local-/i,
|
20
|
+
ollama: /(llama-|nomic)/i
|
21
|
+
}
|
22
|
+
|
23
|
+
MODEL_TYPES = {
|
24
|
+
text_to_text: /^(nomic|gpt|davinci|curie|babbage|ada|claude|gemini|palm|command|generate|j2-|mistral|codestral)/i,
|
25
|
+
speech_to_text: /^whisper/i,
|
26
|
+
text_to_speech: /^tts/i,
|
27
|
+
text_to_image: /^dall-e/i
|
28
|
+
}
|
29
|
+
|
30
|
+
class << self
|
31
|
+
|
32
|
+
def configure
|
33
|
+
yield(configuration)
|
34
|
+
end
|
35
|
+
|
36
|
+
def configuration
|
37
|
+
@configuration ||= Configuration.new
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
|
44
|
+
|
45
|
+
# Usage example:
|
46
|
+
# Configure general settings
|
47
|
+
# AiClient.configure do |config|
|
48
|
+
# config.logger = Logger.new('ai_client.log')
|
49
|
+
# config.return_raw = true
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# Configure provider-specific settings
|
53
|
+
# AiClient.configure do |config|
|
54
|
+
# config.configure_provider(:openai) do
|
55
|
+
# {
|
56
|
+
# organization: 'org-123',
|
57
|
+
# api_version: 'v1'
|
58
|
+
# }
|
59
|
+
# end
|
60
|
+
# end
|
61
|
+
#
|
62
|
+
|
63
|
+
class Configuration
|
64
|
+
attr_accessor :logger, :timeout, :return_raw
|
65
|
+
attr_reader :providers, :provider_patterns, :model_types
|
66
|
+
|
67
|
+
def initialize
|
68
|
+
@logger = Logger.new(STDOUT)
|
69
|
+
@timeout = nil
|
70
|
+
@return_raw = false
|
71
|
+
@providers = {}
|
72
|
+
@provider_patterns = AiClient::PROVIDER_PATTERNS.dup
|
73
|
+
@model_types = AiClient::MODEL_TYPES.dup
|
74
|
+
end
|
75
|
+
|
76
|
+
def provider(name, &block)
|
77
|
+
if block_given?
|
78
|
+
@providers[name] = block
|
79
|
+
else
|
80
|
+
@providers[name]&.call || {}
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# ai_client/logger_middleware.rb
|
2
|
+
|
3
|
+
class AiClient
|
4
|
+
|
5
|
+
# logger = Logger.new(STDOUT)
|
6
|
+
#
|
7
|
+
# AiClient.use(
|
8
|
+
# AiClient::LoggingMiddleware.new(logger)
|
9
|
+
# )
|
10
|
+
#
|
11
|
+
# Or, if you want to use the same logger as the AiClient:
|
12
|
+
# AiClient.use(
|
13
|
+
# AiClient::LoggingMiddleware.new(
|
14
|
+
# AiClient.configuration.logger
|
15
|
+
# )
|
16
|
+
# )
|
17
|
+
|
18
|
+
class LoggingMiddleware
|
19
|
+
def initialize(logger)
|
20
|
+
@logger = logger
|
21
|
+
end
|
22
|
+
|
23
|
+
def call(client, next_middleware, *args)
|
24
|
+
method_name = args.first.is_a?(Symbol) ? args.first : 'unknown method'
|
25
|
+
@logger.info("Starting #{method_name} call")
|
26
|
+
start_time = Time.now
|
27
|
+
|
28
|
+
result = next_middleware.call(*args)
|
29
|
+
|
30
|
+
end_time = Time.now
|
31
|
+
duration = end_time - start_time
|
32
|
+
@logger.info("Finished #{method_name} call (took #{duration.round(3)} seconds)")
|
33
|
+
|
34
|
+
result
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# ai_client/retry_middleware.rb
|
2
|
+
|
3
|
+
class AiClient
|
4
|
+
|
5
|
+
# AiClient.use(
|
6
|
+
# AiClient::RetryMiddleware.new(
|
7
|
+
# max_retries: 5,
|
8
|
+
# base_delay: 2,
|
9
|
+
# max_delay: 30
|
10
|
+
# )
|
11
|
+
# )
|
12
|
+
#
|
13
|
+
class RetryMiddleware
|
14
|
+
def initialize(max_retries: 3, base_delay: 2, max_delay: 16)
|
15
|
+
@max_retries = max_retries
|
16
|
+
@base_delay = base_delay
|
17
|
+
@max_delay = max_delay
|
18
|
+
end
|
19
|
+
|
20
|
+
def call(client, next_middleware, *args)
|
21
|
+
retries = 0
|
22
|
+
begin
|
23
|
+
next_middleware.call
|
24
|
+
rescue OmniAI::RateLimitError, OmniAI::NetworkError => e
|
25
|
+
if retries < @max_retries
|
26
|
+
retries += 1
|
27
|
+
delay = [@base_delay * (2 ** (retries - 1)), @max_delay].min
|
28
|
+
client.logger.warn("Retrying in #{delay} seconds due to error: #{e.message}")
|
29
|
+
sleep(delay)
|
30
|
+
retry
|
31
|
+
else
|
32
|
+
raise
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/ai_client.rb
ADDED
@@ -0,0 +1,244 @@
|
|
1
|
+
# ai_client.rb
|
2
|
+
# WIP: a generic client to access LLM providers
|
3
|
+
# kinda like the SaaS "open router"
|
4
|
+
#
|
5
|
+
|
6
|
+
unless defined?(DebugMe)
|
7
|
+
require 'debug_me'
|
8
|
+
include DebugMe
|
9
|
+
end
|
10
|
+
|
11
|
+
require 'omniai'
|
12
|
+
require 'omniai/anthropic'
|
13
|
+
require 'omniai/google'
|
14
|
+
require 'omniai/mistral'
|
15
|
+
require 'omniai/openai'
|
16
|
+
require_relative 'extensions/omniai-ollama'
|
17
|
+
require_relative 'extensions/omniai-localai'
|
18
|
+
|
19
|
+
require_relative 'ai_client/configuration'
|
20
|
+
require_relative 'ai_client/version'
|
21
|
+
|
22
|
+
# Create a generic client instance using only model name
|
23
|
+
# client = AiClient.new('gpt-3.5-turbo')
|
24
|
+
#
|
25
|
+
# Add middlewares
|
26
|
+
# AiClient.use(RetryMiddleware.new(max_retries: 5, base_delay: 2, max_delay: 30))
|
27
|
+
# AiClient.use(LoggingMiddleware.new(AiClient.configuration.logger))
|
28
|
+
#
|
29
|
+
# TODO: As concurrently designed the middleware must
|
30
|
+
# be set before an instance of AiClient is created.
|
31
|
+
# Any `use` commands for middleware made after
|
32
|
+
# the instance is created will not be available
|
33
|
+
# to that instance.
|
34
|
+
# Change this so that middleware can be added
|
35
|
+
# and removed from an existing client.
|
36
|
+
|
37
|
+
class AiClient
|
38
|
+
|
39
|
+
attr_reader :client, :provider, :model, :model_type, :logger, :last_response, :config
|
40
|
+
|
41
|
+
def initialize(model, config: Configuration.new, **options)
|
42
|
+
@model = model
|
43
|
+
@config = config
|
44
|
+
@provider = validate_provider(options[:provider]) || determine_provider(model)
|
45
|
+
@model_type = determine_model_type(model)
|
46
|
+
|
47
|
+
provider_config = @config.provider(@provider)
|
48
|
+
|
49
|
+
@logger = options[:logger] || @config.logger
|
50
|
+
@timeout = options[:timeout] || @config.timeout
|
51
|
+
@base_url = options[:base_url] || provider_config[:base_url]
|
52
|
+
@options = options.merge(provider_config)
|
53
|
+
|
54
|
+
# @client is an instance of an OmniAI::* class
|
55
|
+
@client = create_client
|
56
|
+
|
57
|
+
@last_response = nil
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
def response = last_response
|
62
|
+
def raw? = config.return_raw
|
63
|
+
|
64
|
+
def raw=(value)
|
65
|
+
config.return_raw = value
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
|
70
|
+
######################################
|
71
|
+
def chat(messages, **params)
|
72
|
+
result = call_with_middlewares(:chat_without_middlewares, messages, **params)
|
73
|
+
@last_response = result
|
74
|
+
# debug_me print " (raw: #{raw?}) "
|
75
|
+
raw? ? result : content
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
def chat_without_middlewares(messages, **params)
|
80
|
+
@client.chat(messages, model: @model, **params)
|
81
|
+
end
|
82
|
+
|
83
|
+
######################################
|
84
|
+
def transcribe(audio, format: nil, **params)
|
85
|
+
call_with_middlewares(:transcribe_without_middlewares, audio, format: format, **params)
|
86
|
+
end
|
87
|
+
|
88
|
+
def transcribe_without_middlewares(audio, format: nil, **params)
|
89
|
+
@client.transcribe(audio, model: @model, format: format, **params)
|
90
|
+
end
|
91
|
+
|
92
|
+
######################################
|
93
|
+
def speak(text, **params)
|
94
|
+
call_with_middlewares(:speak_without_middlewares, text, **params)
|
95
|
+
end
|
96
|
+
|
97
|
+
def speak_without_middlewares(text, **params)
|
98
|
+
@client.speak(text, model: @model, **params)
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
######################################
|
103
|
+
def embed(input, **params)
|
104
|
+
@client.embed(input, model: @model, **params)
|
105
|
+
end
|
106
|
+
|
107
|
+
def batch_embed(inputs, batch_size: 100, **params)
|
108
|
+
inputs.each_slice(batch_size).flat_map do |batch|
|
109
|
+
sleep 1 # DEBUG rate limits being exceeded
|
110
|
+
embed(batch, **params)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
######################################
|
115
|
+
## Utilities
|
116
|
+
|
117
|
+
def call_with_middlewares(method, *args, **kwargs, &block)
|
118
|
+
stack = self.class.middlewares.reverse.reduce(-> { send(method, *args, **kwargs, &block) }) do |next_middleware, middleware|
|
119
|
+
-> { middleware.call(self, next_middleware, *args, **kwargs) }
|
120
|
+
end
|
121
|
+
stack.call
|
122
|
+
end
|
123
|
+
|
124
|
+
|
125
|
+
def content
|
126
|
+
case @provider
|
127
|
+
when :openai, :localai, :ollama
|
128
|
+
last_response.data.dig('choices', 0, 'message', 'content')
|
129
|
+
when :anthropic
|
130
|
+
last_response.data.dig('content',0,'text')
|
131
|
+
when :google
|
132
|
+
last_response.data.dig('candidates', 0, 'content', 'parts', 0, 'text')
|
133
|
+
when :mistral
|
134
|
+
last_response.data.dig('choices', 0, 'message', 'content')
|
135
|
+
else
|
136
|
+
raise NotImplementedError, "Content extraction not implemented for provider: #{@provider}"
|
137
|
+
end
|
138
|
+
end
|
139
|
+
alias_method :text, :content
|
140
|
+
|
141
|
+
##############################################
|
142
|
+
## Public Class Methods
|
143
|
+
|
144
|
+
class << self
|
145
|
+
|
146
|
+
def middlewares
|
147
|
+
@middlewares ||= []
|
148
|
+
end
|
149
|
+
|
150
|
+
def use(middleware)
|
151
|
+
middlewares << middleware
|
152
|
+
end
|
153
|
+
|
154
|
+
def clear_middlewares
|
155
|
+
@middlewares = []
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def method_missing(method_name, *args, &block)
|
160
|
+
if @client.respond_to?(method_name)
|
161
|
+
result = @client.send(method_name, *args, &block)
|
162
|
+
@last_response = result if result.is_a?(OmniAI::Response)
|
163
|
+
result
|
164
|
+
else
|
165
|
+
super
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def respond_to_missing?(method_name, include_private = false)
|
170
|
+
@client.respond_to?(method_name) || super
|
171
|
+
end
|
172
|
+
|
173
|
+
|
174
|
+
##############################################
|
175
|
+
private
|
176
|
+
|
177
|
+
def validate_provider(provider)
|
178
|
+
return nil if provider.nil?
|
179
|
+
|
180
|
+
valid_providers = config.provider_patterns.keys
|
181
|
+
unless valid_providers.include?(provider)
|
182
|
+
raise ArgumentError, "Unsupported provider: #{provider}"
|
183
|
+
end
|
184
|
+
|
185
|
+
provider
|
186
|
+
end
|
187
|
+
|
188
|
+
|
189
|
+
def create_client
|
190
|
+
api_key = fetch_api_key # Fetching the API key should only happen for valid providers
|
191
|
+
client_options = {
|
192
|
+
api_key: api_key,
|
193
|
+
logger: @logger,
|
194
|
+
timeout: @timeout
|
195
|
+
}
|
196
|
+
client_options[:base_url] = @base_url if @base_url
|
197
|
+
client_options.merge!(@options).delete(:provider)
|
198
|
+
|
199
|
+
case provider
|
200
|
+
when :openai
|
201
|
+
OmniAI::OpenAI::Client.new(**client_options)
|
202
|
+
when :anthropic
|
203
|
+
OmniAI::Anthropic::Client.new(**client_options)
|
204
|
+
when :google
|
205
|
+
OmniAI::Google::Client.new(**client_options)
|
206
|
+
when :mistral
|
207
|
+
OmniAI::Mistral::Client.new(**client_options)
|
208
|
+
when :ollama
|
209
|
+
OmniAI::Ollama::Client.new(**client_options)
|
210
|
+
when :localai
|
211
|
+
OmniAI::LocalAI::Client.new(**client_options)
|
212
|
+
else
|
213
|
+
raise ArgumentError, "Unsupported provider: #{@provider}"
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
|
218
|
+
def fetch_api_key
|
219
|
+
env_var_name = "#{@provider.upcase}_API_KEY"
|
220
|
+
api_key = ENV[env_var_name]
|
221
|
+
|
222
|
+
if api_key.nil? || api_key.empty?
|
223
|
+
unless [:localai, :ollama].include? provider
|
224
|
+
raise ArgumentError, "API key not found in environment variable #{env_var_name}"
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
api_key
|
229
|
+
end
|
230
|
+
|
231
|
+
|
232
|
+
def determine_provider(model)
|
233
|
+
config.provider_patterns.find { |provider, pattern| model.match?(pattern) }&.first ||
|
234
|
+
raise(ArgumentError, "Unsupported model: #{model}")
|
235
|
+
end
|
236
|
+
|
237
|
+
|
238
|
+
def determine_model_type(model)
|
239
|
+
config.model_types.find { |type, pattern| model.match?(pattern) }&.first ||
|
240
|
+
raise(ArgumentError, "Unable to determine model type for: #{model}")
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# extensions/omniai-localai.rb
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'omniai'
|
5
|
+
require 'omniai/openai'
|
6
|
+
|
7
|
+
module OmniAI
|
8
|
+
|
9
|
+
# Create an alias for OmniAI::OpenAI module
|
10
|
+
module LocalAI
|
11
|
+
extend OmniAI::OpenAI
|
12
|
+
|
13
|
+
# Alias classes from OmniAI::OpenAI
|
14
|
+
class Client < OmniAI::OpenAI::Client
|
15
|
+
def initialize(**options)
|
16
|
+
options[:host] = 'http://localhost:8080' unless options.has_key?(:host)
|
17
|
+
super(**options)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
Config = OmniAI::OpenAI::Config
|
23
|
+
|
24
|
+
# Alias the Thread class and its nested classes
|
25
|
+
Thread = OmniAI::OpenAI::Thread
|
26
|
+
Annotation = OmniAI::OpenAI::Thread::Annotation
|
27
|
+
Attachment = OmniAI::OpenAI::Thread::Attachment
|
28
|
+
Message = OmniAI::OpenAI::Thread::Message
|
29
|
+
Run = OmniAI::OpenAI::Thread::Run
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# extensions/omniai-ollama.rb
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'omniai'
|
5
|
+
require 'omniai/openai'
|
6
|
+
|
7
|
+
module OmniAI
|
8
|
+
|
9
|
+
# Create an alias for OmniAI::OpenAI module
|
10
|
+
module Ollama
|
11
|
+
extend OmniAI::OpenAI
|
12
|
+
|
13
|
+
# Alias classes from OmniAI::OpenAI
|
14
|
+
class Client < OmniAI::OpenAI::Client
|
15
|
+
def initialize(**options)
|
16
|
+
options[:host] = 'http://localhost:11434' unless options.has_key?(:host)
|
17
|
+
super(**options)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
Config = OmniAI::OpenAI::Config
|
22
|
+
|
23
|
+
# Alias the Thread class and its nested classes
|
24
|
+
Thread = OmniAI::OpenAI::Thread
|
25
|
+
Annotation = OmniAI::OpenAI::Thread::Annotation
|
26
|
+
Attachment = OmniAI::OpenAI::Thread::Attachment
|
27
|
+
Message = OmniAI::OpenAI::Thread::Message
|
28
|
+
Run = OmniAI::OpenAI::Thread::Run
|
29
|
+
end
|
30
|
+
end
|
data/sig/ai_client.rbs
ADDED
@@ -0,0 +1,5 @@
|
|
1
|
+
## The Ollama Model Problem
|
2
|
+
|
3
|
+
The ability ot a model-centric configuration where the provider is derived from the model break with those models that can be run locally as well as be accessed through an off-platform provider API. Take for example the `mistral` model family which can be access to the `La Platform` API or downloaded locally and used with `Ollama`.
|
4
|
+
|
5
|
+
If I specify `mistral-large` I should also in the constructor method also provide a `provider:` parameter to specify where that model is going to be accessed.
|
metadata
ADDED
@@ -0,0 +1,190 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ai_client
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Dewayne VanHoozer
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-10-03 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: omniai
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: omniai-anthropic
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: omniai-google
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: omniai-mistral
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: omniai-openai
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: amazing_print
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: debug_me
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: mocha
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
description: "`ai_client` is a versatile Ruby gem that serves as a generic client
|
126
|
+
\nfor interacting with various AI service providers through a unified \nAPI. Designed
|
127
|
+
to simplify the integration of large language models \n(LLMs) into applications,
|
128
|
+
`ai_client` allows developers to create \ninstances using just the model name, greatly
|
129
|
+
reducing configuration \noverhead. With built-in support for popular AI providers—including
|
130
|
+
\nOpenAI, Anthropic, Google, Mistral, LocalAI and Ollama—the gem abstracts the \ncomplexities
|
131
|
+
of API interactions, offering methods for tasks such \nas chatting, transcription,
|
132
|
+
speech synthesis, and embedding. The \nmiddleware architecture enables customizable
|
133
|
+
processing of requests \nand responses, making it easy to implement features like
|
134
|
+
logging and \nretry logic. Seamlessly integrated with the `OmniAI` framework, \n`ai_client`
|
135
|
+
empowers developers to leverage cutting-edge AI capabilities \nwithout vendor lock-in,
|
136
|
+
making it an essential tool for modern AI-driven \napplications.\n"
|
137
|
+
email:
|
138
|
+
- dvanhoozer@gmail.com
|
139
|
+
executables: []
|
140
|
+
extensions: []
|
141
|
+
extra_rdoc_files: []
|
142
|
+
files:
|
143
|
+
- ".envrc"
|
144
|
+
- CHANGELOG.md
|
145
|
+
- LICENSE
|
146
|
+
- README.md
|
147
|
+
- Rakefile
|
148
|
+
- examples/Bethany Hamilton.m4a
|
149
|
+
- examples/common.rb
|
150
|
+
- examples/embed.rb
|
151
|
+
- examples/speak.rb
|
152
|
+
- examples/text.rb
|
153
|
+
- examples/transcribe.rb
|
154
|
+
- lib/ai_client.rb
|
155
|
+
- lib/ai_client/configuration.rb
|
156
|
+
- lib/ai_client/logger_middleware.rb
|
157
|
+
- lib/ai_client/retry_middleware.rb
|
158
|
+
- lib/ai_client/version.rb
|
159
|
+
- lib/extensions/omniai-localai.rb
|
160
|
+
- lib/extensions/omniai-ollama.rb
|
161
|
+
- sig/ai_client.rbs
|
162
|
+
- the_ollama_model_problem.md
|
163
|
+
homepage: https://github.com/MadBomber/ai_client
|
164
|
+
licenses:
|
165
|
+
- MIT
|
166
|
+
metadata:
|
167
|
+
allowed_push_host: https://rubygems.org
|
168
|
+
homepage_uri: https://github.com/MadBomber/ai_client
|
169
|
+
source_code_uri: https://github.com/MadBomber/ai_client
|
170
|
+
changelog_uri: https://github.com/MadBomber/ai_client/blob/main/CHANGELOG.md
|
171
|
+
post_install_message:
|
172
|
+
rdoc_options: []
|
173
|
+
require_paths:
|
174
|
+
- lib
|
175
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
176
|
+
requirements:
|
177
|
+
- - ">="
|
178
|
+
- !ruby/object:Gem::Version
|
179
|
+
version: 3.0.0
|
180
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
181
|
+
requirements:
|
182
|
+
- - ">="
|
183
|
+
- !ruby/object:Gem::Version
|
184
|
+
version: '0'
|
185
|
+
requirements: []
|
186
|
+
rubygems_version: 3.5.20
|
187
|
+
signing_key:
|
188
|
+
specification_version: 4
|
189
|
+
summary: A generic AI Client for many providers
|
190
|
+
test_files: []
|