vapi-ruby 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f22947bfc1aff0910a372e417b00d3d627896f43d9bd9e2760a897c1343f75e5
4
+ data.tar.gz: d22223f4a7f7c5ad0b0144b93882ee78cb7fb274c0df14cbda07c135c489c4ce
5
+ SHA512:
6
+ metadata.gz: f1cee1b70f543e6bdb66c20f2d7c1272c77cc54e9103ee0829bedf3c2411426f5f0cf5b964c214f94289145e4584dce2857dcb7baa4ebecc9091f2b0371881e9
7
+ data.tar.gz: 6f33b0187a90d34223e80b862abae104c954525f0e06f7dc9b711afe1c216899c8c84df4043d58cc1202225e1871345929e4d459c2c2a8b749accf371b349242
data/CHANGELOG.md ADDED
@@ -0,0 +1,23 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2026-03-25
9
+
10
+ ### Added
11
+ - Initial release
12
+ - `Vapi::Assistant` — CRUD for voice AI assistants
13
+ - `Vapi::Call` — create outbound calls, list, and manage call records
14
+ - `Vapi::PhoneNumber` — manage phone numbers (Twilio, Vonage, VAPI)
15
+ - `Vapi::Squad` — manage multi-assistant squads
16
+ - `Vapi::Tool` — CRUD for function/API tools attached to assistants
17
+ - `Vapi::File` — upload and manage files
18
+ - `Vapi::KnowledgeBase` — manage knowledge bases for RAG
19
+ - `Vapi::Log` — retrieve call and API logs
20
+ - `Vapi::Analytics` — query call analytics
21
+ - Bearer token authentication
22
+ - Comprehensive error handling (401, 404, 429, 400/422, 5xx)
23
+ - Full RSpec test suite with WebMock
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Eduardo Souza
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
13
+ all 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
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,305 @@
1
+ # VAPI Ruby [![Gem Version](https://badge.fury.io/rb/vapi-ruby.svg?icon=si%3Arubygems)](https://badge.fury.io/rb/vapi-ruby)
2
+
3
+ Ruby gem for integrating with the [VAPI](https://vapi.ai) voice AI platform. Supports assistants, calls, phone numbers, squads, tools, files, knowledge bases, logs, and analytics.
4
+
5
+ ## Installation
6
+
7
+ Add to your Gemfile:
8
+
9
+ ```ruby
10
+ gem 'vapi-ruby'
11
+ ```
12
+
13
+ Then run:
14
+
15
+ ```bash
16
+ bundle install
17
+ ```
18
+
19
+ Or install directly:
20
+
21
+ ```bash
22
+ gem install vapi-ruby
23
+ ```
24
+
25
+ ## Configuration
26
+
27
+ ```ruby
28
+ Vapi.configure do |config|
29
+ config.api_key = ENV['VAPI_API_KEY']
30
+
31
+ # Optional
32
+ config.timeout = 30 # seconds, default
33
+ end
34
+ ```
35
+
36
+ ## Usage
37
+
38
+ All resources can be initialized without arguments (uses `Vapi.client` by default) or with an explicit client.
39
+
40
+ ### Assistants
41
+
42
+ ```ruby
43
+ assistants = Vapi::Assistant.new
44
+
45
+ # List all assistants
46
+ assistants.list
47
+
48
+ # List with filters
49
+ assistants.list(limit: 10, createdAtGt: '2024-01-01T00:00:00Z')
50
+
51
+ # Get an assistant
52
+ assistants.find('assistant_id')
53
+
54
+ # Create an assistant
55
+ assistants.create(
56
+ name: 'Sales Agent',
57
+ model: { provider: 'openai', model: 'gpt-4' },
58
+ voice: { provider: '11labs', voiceId: 'voice_id' },
59
+ firstMessage: 'Hello! How can I help you today?'
60
+ )
61
+
62
+ # Update an assistant
63
+ assistants.update('assistant_id', name: 'Updated Name')
64
+
65
+ # Delete an assistant
66
+ assistants.delete('assistant_id')
67
+ ```
68
+
69
+ ### Calls
70
+
71
+ ```ruby
72
+ calls = Vapi::Call.new
73
+
74
+ # List all calls
75
+ calls.list
76
+
77
+ # Filter by assistant
78
+ calls.list(assistantId: 'assistant_id')
79
+
80
+ # Get a call
81
+ calls.find('call_id')
82
+
83
+ # Create an outbound call
84
+ calls.create(
85
+ assistantId: 'assistant_id',
86
+ phoneNumberId: 'phone_number_id',
87
+ customer: { number: '+1234567890' }
88
+ )
89
+
90
+ # Update a call
91
+ calls.update('call_id', name: 'Follow-up Call')
92
+
93
+ # Delete a call
94
+ calls.delete('call_id')
95
+ ```
96
+
97
+ ### Phone Numbers
98
+
99
+ ```ruby
100
+ phone_numbers = Vapi::PhoneNumber.new
101
+
102
+ # List phone numbers
103
+ phone_numbers.list
104
+
105
+ # Get a phone number
106
+ phone_numbers.find('phone_number_id')
107
+
108
+ # Create a phone number
109
+ phone_numbers.create(
110
+ provider: 'twilio',
111
+ number: '+1234567890',
112
+ twilioAccountSid: 'sid',
113
+ twilioAuthToken: 'token'
114
+ )
115
+
116
+ # Update a phone number
117
+ phone_numbers.update('phone_number_id', name: 'Main Line')
118
+
119
+ # Delete a phone number
120
+ phone_numbers.delete('phone_number_id')
121
+ ```
122
+
123
+ ### Squads
124
+
125
+ ```ruby
126
+ squads = Vapi::Squad.new
127
+
128
+ # List squads
129
+ squads.list
130
+
131
+ # Get a squad
132
+ squads.find('squad_id')
133
+
134
+ # Create a squad
135
+ squads.create(
136
+ name: 'Sales Team',
137
+ members: [{ assistantId: 'assistant_id' }]
138
+ )
139
+
140
+ # Update a squad
141
+ squads.update('squad_id', name: 'Updated Team')
142
+
143
+ # Delete a squad
144
+ squads.delete('squad_id')
145
+ ```
146
+
147
+ ### Tools
148
+
149
+ ```ruby
150
+ tools = Vapi::Tool.new
151
+
152
+ # List tools
153
+ tools.list
154
+
155
+ # Get a tool
156
+ tools.find('tool_id')
157
+
158
+ # Create a tool
159
+ tools.create(
160
+ type: 'function',
161
+ function: {
162
+ name: 'lookup_order',
163
+ description: 'Look up an order by ID',
164
+ parameters: { type: 'object', properties: { orderId: { type: 'string' } } }
165
+ }
166
+ )
167
+
168
+ # Update a tool
169
+ tools.update('tool_id', function: { name: 'updated_lookup' })
170
+
171
+ # Delete a tool
172
+ tools.delete('tool_id')
173
+ ```
174
+
175
+ ### Files
176
+
177
+ ```ruby
178
+ files = Vapi::File.new
179
+
180
+ # List files
181
+ files.list
182
+
183
+ # Get a file
184
+ files.find('file_id')
185
+
186
+ # Upload a file
187
+ files.create(name: 'training_data.pdf')
188
+
189
+ # Update a file
190
+ files.update('file_id', name: 'renamed.pdf')
191
+
192
+ # Delete a file
193
+ files.delete('file_id')
194
+ ```
195
+
196
+ ### Knowledge Bases
197
+
198
+ ```ruby
199
+ knowledge_bases = Vapi::KnowledgeBase.new
200
+
201
+ # List knowledge bases
202
+ knowledge_bases.list
203
+
204
+ # Get a knowledge base
205
+ knowledge_bases.find('knowledge_base_id')
206
+
207
+ # Create a knowledge base
208
+ knowledge_bases.create(
209
+ name: 'Product FAQ',
210
+ provider: 'trieve'
211
+ )
212
+
213
+ # Update a knowledge base
214
+ knowledge_bases.update('knowledge_base_id', name: 'Updated FAQ')
215
+
216
+ # Delete a knowledge base
217
+ knowledge_bases.delete('knowledge_base_id')
218
+ ```
219
+
220
+ ### Logs
221
+
222
+ ```ruby
223
+ logs = Vapi::Log.new
224
+
225
+ # List all logs
226
+ logs.list
227
+
228
+ # Filter by call
229
+ logs.list(callId: 'call_id')
230
+ ```
231
+
232
+ ### Analytics
233
+
234
+ ```ruby
235
+ analytics = Vapi::Analytics.new
236
+
237
+ # Query analytics
238
+ analytics.query(
239
+ queries: [
240
+ {
241
+ name: 'total_calls',
242
+ table: 'call',
243
+ operations: [{ operation: 'count', column: 'id' }]
244
+ }
245
+ ]
246
+ )
247
+ ```
248
+
249
+ ### Using Multiple Clients
250
+
251
+ ```ruby
252
+ config1 = Vapi::Configuration.new
253
+ config1.api_key = 'key_for_org_1'
254
+ client1 = Vapi::Client.new(config1)
255
+
256
+ config2 = Vapi::Configuration.new
257
+ config2.api_key = 'key_for_org_2'
258
+ client2 = Vapi::Client.new(config2)
259
+
260
+ # Use explicit clients
261
+ Vapi::Assistant.new(client1).list
262
+ Vapi::Assistant.new(client2).list
263
+ ```
264
+
265
+ ### Error Handling
266
+
267
+ ```ruby
268
+ begin
269
+ calls.create(assistantId: 'invalid')
270
+ rescue Vapi::ConfigurationError => e
271
+ # Missing API key
272
+ rescue Vapi::AuthenticationError => e
273
+ # Invalid API key
274
+ rescue Vapi::ValidationError => e
275
+ puts e.errors
276
+ rescue Vapi::NotFoundError => e
277
+ # 404 - Resource not found
278
+ rescue Vapi::RateLimitError => e
279
+ puts e.retry_after # Seconds to wait
280
+ rescue Vapi::APIError => e
281
+ puts e.status_code
282
+ puts e.response_body
283
+ end
284
+ ```
285
+
286
+ ## Development
287
+
288
+ ```bash
289
+ # Install dependencies
290
+ bundle install
291
+
292
+ # Run tests
293
+ bundle exec rspec
294
+
295
+ # Run linter
296
+ bundle exec rubocop
297
+ ```
298
+
299
+ ## Contributing
300
+
301
+ Bug reports and pull requests are welcome on [GitHub](https://github.com/esouza/vapi-ruby). See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
302
+
303
+ ## License
304
+
305
+ The gem is available as open source under the terms of the [MIT License](LICENSE.txt).
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vapi
4
+ class Analytics < Base
5
+ def query(params)
6
+ client.post("/analytics", params)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vapi
4
+ class Assistant < Base
5
+ def list(**params)
6
+ client.get("/assistant", params)
7
+ end
8
+
9
+ def find(id)
10
+ client.get("/assistant/#{id}")
11
+ end
12
+
13
+ def create(params)
14
+ client.post("/assistant", params)
15
+ end
16
+
17
+ def update(id, params)
18
+ client.patch("/assistant/#{id}", params)
19
+ end
20
+
21
+ def delete(id)
22
+ client.delete("/assistant/#{id}")
23
+ end
24
+ end
25
+ end
data/lib/vapi/base.rb ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vapi
4
+ class Base
5
+ attr_reader :client
6
+
7
+ def initialize(client = nil)
8
+ @client = client || Vapi.client
9
+ end
10
+ end
11
+ end
data/lib/vapi/call.rb ADDED
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vapi
4
+ class Call < Base
5
+ def list(**params)
6
+ client.get("/call", params)
7
+ end
8
+
9
+ def find(id)
10
+ client.get("/call/#{id}")
11
+ end
12
+
13
+ def create(params)
14
+ client.post("/call", params)
15
+ end
16
+
17
+ def update(id, params)
18
+ client.patch("/call/#{id}", params)
19
+ end
20
+
21
+ def delete(id)
22
+ client.delete("/call/#{id}")
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vapi
4
+ class Client
5
+ include HTTParty
6
+
7
+ attr_reader :configuration
8
+
9
+ def initialize(configuration = nil)
10
+ @configuration = configuration || Vapi.configuration
11
+ validate_configuration!
12
+ end
13
+
14
+ def get(path, params = {})
15
+ request(:get, path, query: params)
16
+ end
17
+
18
+ def post(path, data = {})
19
+ request(:post, path, body: data.to_json)
20
+ end
21
+
22
+ def patch(path, data = {})
23
+ request(:patch, path, body: data.to_json)
24
+ end
25
+
26
+ def delete(path)
27
+ request(:delete, path)
28
+ end
29
+
30
+ private
31
+
32
+ def validate_configuration!
33
+ raise ConfigurationError unless configuration&.valid?
34
+ end
35
+
36
+ def request(method, path, options = {})
37
+ url = "#{configuration.base_url}#{path}"
38
+ request_options = {
39
+ headers: default_headers,
40
+ timeout: configuration.timeout
41
+ }
42
+
43
+ if options[:query] && !options[:query].empty?
44
+ request_options[:query] = options[:query]
45
+ end
46
+
47
+ if options[:body]
48
+ request_options[:body] = options[:body]
49
+ end
50
+
51
+ response = self.class.send(method, url, request_options)
52
+ handle_response(response)
53
+ end
54
+
55
+ def default_headers
56
+ {
57
+ 'Authorization' => "Bearer #{configuration.api_key}",
58
+ 'Content-Type' => 'application/json',
59
+ 'Accept' => 'application/json'
60
+ }
61
+ end
62
+
63
+ def handle_response(response)
64
+ case response.code
65
+ when 200, 201, 202, 204
66
+ response.parsed_response
67
+ when 401
68
+ raise AuthenticationError, "Authentication failed: #{response.body}"
69
+ when 404
70
+ raise NotFoundError.new("Resource not found", response.code, response.body)
71
+ when 429
72
+ retry_after = response.headers['retry-after']
73
+ raise RateLimitError.new("Rate limit exceeded", response.code, response.body, retry_after)
74
+ when 400, 422
75
+ parsed = response.parsed_response || {}
76
+ errors = parsed['errors'] || parsed['message'] || {}
77
+ raise ValidationError.new(
78
+ parsed['message'] || 'Validation failed',
79
+ response.code,
80
+ response.body,
81
+ errors
82
+ )
83
+ when 500..599
84
+ raise APIError.new("Server error", response.code, response.body)
85
+ else
86
+ raise APIError.new("Unexpected response", response.code, response.body)
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vapi
4
+ class Configuration
5
+ attr_accessor :api_key, :base_url, :timeout
6
+
7
+ def initialize
8
+ @base_url = "https://api.vapi.ai"
9
+ @timeout = 30
10
+ end
11
+
12
+ def valid?
13
+ !api_key.nil? && !api_key.empty?
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vapi
4
+ class Error < StandardError; end
5
+
6
+ class ConfigurationError < Error
7
+ def initialize(message = "Missing required configuration: api_key")
8
+ super(message)
9
+ end
10
+ end
11
+
12
+ class AuthenticationError < Error
13
+ def initialize(message = "Authentication failed")
14
+ super(message)
15
+ end
16
+ end
17
+
18
+ class APIError < Error
19
+ attr_reader :status_code, :response_body
20
+
21
+ def initialize(message, status_code = nil, response_body = nil)
22
+ super(message)
23
+ @status_code = status_code
24
+ @response_body = response_body
25
+ end
26
+ end
27
+
28
+ class ValidationError < APIError
29
+ attr_reader :errors
30
+
31
+ def initialize(message, status_code = nil, response_body = nil, errors = {})
32
+ super(message, status_code, response_body)
33
+ @errors = errors
34
+ end
35
+ end
36
+
37
+ class NotFoundError < APIError
38
+ def initialize(message = "Resource not found", status_code = 404, response_body = nil)
39
+ super(message, status_code, response_body)
40
+ end
41
+ end
42
+
43
+ class RateLimitError < APIError
44
+ attr_reader :retry_after
45
+
46
+ def initialize(message = "Rate limit exceeded", status_code = 429, response_body = nil, retry_after = nil)
47
+ super(message, status_code, response_body)
48
+ @retry_after = retry_after
49
+ end
50
+ end
51
+ end
data/lib/vapi/file.rb ADDED
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vapi
4
+ class File < Base
5
+ def list(**params)
6
+ client.get("/file", params)
7
+ end
8
+
9
+ def find(id)
10
+ client.get("/file/#{id}")
11
+ end
12
+
13
+ def create(params)
14
+ client.post("/file", params)
15
+ end
16
+
17
+ def update(id, params)
18
+ client.patch("/file/#{id}", params)
19
+ end
20
+
21
+ def delete(id)
22
+ client.delete("/file/#{id}")
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vapi
4
+ class KnowledgeBase < Base
5
+ def list(**params)
6
+ client.get("/knowledge-base", params)
7
+ end
8
+
9
+ def find(id)
10
+ client.get("/knowledge-base/#{id}")
11
+ end
12
+
13
+ def create(params)
14
+ client.post("/knowledge-base", params)
15
+ end
16
+
17
+ def update(id, params)
18
+ client.patch("/knowledge-base/#{id}", params)
19
+ end
20
+
21
+ def delete(id)
22
+ client.delete("/knowledge-base/#{id}")
23
+ end
24
+ end
25
+ end
data/lib/vapi/log.rb ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vapi
4
+ class Log < Base
5
+ def list(**params)
6
+ client.get("/logs", params)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vapi
4
+ class PhoneNumber < Base
5
+ def list(**params)
6
+ client.get("/phone-number", params)
7
+ end
8
+
9
+ def find(id)
10
+ client.get("/phone-number/#{id}")
11
+ end
12
+
13
+ def create(params)
14
+ client.post("/phone-number", params)
15
+ end
16
+
17
+ def update(id, params)
18
+ client.patch("/phone-number/#{id}", params)
19
+ end
20
+
21
+ def delete(id)
22
+ client.delete("/phone-number/#{id}")
23
+ end
24
+ end
25
+ end
data/lib/vapi/squad.rb ADDED
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vapi
4
+ class Squad < Base
5
+ def list(**params)
6
+ client.get("/squad", params)
7
+ end
8
+
9
+ def find(id)
10
+ client.get("/squad/#{id}")
11
+ end
12
+
13
+ def create(params)
14
+ client.post("/squad", params)
15
+ end
16
+
17
+ def update(id, params)
18
+ client.patch("/squad/#{id}", params)
19
+ end
20
+
21
+ def delete(id)
22
+ client.delete("/squad/#{id}")
23
+ end
24
+ end
25
+ end