tavus 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.
data/README.md ADDED
@@ -0,0 +1,736 @@
1
+ # Tavus Ruby Client
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/tavus.svg)](https://badge.fury.io/rb/tavus)
4
+
5
+ A Ruby gem for interacting with the [Tavus API](https://tavusapi.com), providing a simple and intuitive interface for managing Conversational Video Interfaces (CVI).
6
+
7
+ ## Features
8
+
9
+ - **Conversations Management**: Create, retrieve, list, end, and delete real-time video conversations
10
+ - **Personas Management**: Create, retrieve, list, update, and delete AI personas with customizable behavior
11
+ - **Replicas Management**: Create, train, retrieve, list, rename, and delete AI replicas
12
+ - **Objectives Management**: Create, retrieve, list, update, and delete conversation objectives
13
+ - **Guardrails Management**: Create, retrieve, list, update, and delete behavioral guardrails
14
+ - **Knowledge Base (Documents)**: Upload, retrieve, list, update, and delete documents for personas
15
+ - **Video Generation**: Generate videos from text or audio, retrieve, list, rename, and delete videos
16
+ - **Full API Coverage**: Support for all Tavus API v2 endpoints
17
+ - **Easy Configuration**: Simple API key configuration
18
+ - **Comprehensive Error Handling**: Detailed error classes for different API responses
19
+ - **JSON Patch Support**: Update personas, objectives, and guardrails using JSON Patch operations (RFC 6902)
20
+
21
+ ## Installation
22
+
23
+ Add this line to your application's Gemfile:
24
+
25
+ ```ruby
26
+ gem 'tavus'
27
+ ```
28
+
29
+ And then execute:
30
+
31
+ ```bash
32
+ $ bundle install
33
+ ```
34
+
35
+ Or install it yourself as:
36
+
37
+ ```bash
38
+ $ gem install tavus
39
+ ```
40
+
41
+ ## Configuration
42
+
43
+ ### Global Configuration
44
+
45
+ Configure the client globally (recommended for Rails applications):
46
+
47
+ ```ruby
48
+ # config/initializers/tavus.rb
49
+ Tavus.configure do |config|
50
+ config.api_key = ENV['TAVUS_API_KEY']
51
+ config.base_url = 'https://tavusapi.com' # Optional, defaults to this
52
+ config.timeout = 30 # Optional, defaults to 30 seconds
53
+ end
54
+ ```
55
+
56
+ ### Per-Instance Configuration
57
+
58
+ You can also configure individual client instances:
59
+
60
+ ```ruby
61
+ client = Tavus::Client.new(
62
+ api_key: 'your-api-key',
63
+ base_url: 'https://tavusapi.com',
64
+ timeout: 30
65
+ )
66
+ ```
67
+
68
+ ## Usage
69
+
70
+ ### Conversations
71
+
72
+ #### Create a Conversation
73
+
74
+ ```ruby
75
+ client = Tavus::Client.new(api_key: 'your-api-key')
76
+
77
+ # Create a conversation with required parameters
78
+ conversation = client.conversations.create(
79
+ replica_id: 'rfe12d8b9597',
80
+ persona_id: 'pdced222244b'
81
+ )
82
+
83
+ # Create a conversation with optional parameters
84
+ conversation = client.conversations.create(
85
+ replica_id: 'rfe12d8b9597',
86
+ persona_id: 'pdced222244b',
87
+ conversation_name: 'Interview User',
88
+ callback_url: 'https://yourwebsite.com/webhook',
89
+ audio_only: false,
90
+ conversational_context: 'I want to improve my sales techniques.',
91
+ custom_greeting: 'Hey there!',
92
+ memory_stores: ['anna'],
93
+ document_ids: ['doc_1234567890'],
94
+ document_retrieval_strategy: 'balanced',
95
+ document_tags: ['sales', 'marketing'],
96
+ test_mode: false
97
+ )
98
+
99
+ # Response includes:
100
+ # {
101
+ # "conversation_id" => "c123456",
102
+ # "conversation_name" => "Interview User",
103
+ # "status" => "active",
104
+ # "conversation_url" => "https://tavus.daily.co/c123456",
105
+ # "replica_id" => "r79e1c033f",
106
+ # "persona_id" => "p5317866",
107
+ # "created_at" => "2025-10-04T12:00:00Z"
108
+ # }
109
+ ```
110
+
111
+ #### Get a Conversation
112
+
113
+ ```ruby
114
+ conversation = client.conversations.get('c123456')
115
+
116
+ # Response includes conversation details with status, URLs, etc.
117
+ ```
118
+
119
+ #### List Conversations
120
+
121
+ ```ruby
122
+ # List all conversations
123
+ conversations = client.conversations.list
124
+
125
+ # List with pagination and filters
126
+ conversations = client.conversations.list(
127
+ limit: 20,
128
+ page: 1,
129
+ status: 'active' # or 'ended'
130
+ )
131
+
132
+ # Response includes:
133
+ # {
134
+ # "data" => [...],
135
+ # "total_count" => 123
136
+ # }
137
+ ```
138
+
139
+ #### End a Conversation
140
+
141
+ ```ruby
142
+ result = client.conversations.end('c123456')
143
+ ```
144
+
145
+ #### Delete a Conversation
146
+
147
+ ```ruby
148
+ result = client.conversations.delete('c123456')
149
+ ```
150
+
151
+ ### Personas
152
+
153
+ #### Create a Persona
154
+
155
+ ```ruby
156
+ # Create a basic persona
157
+ persona = client.personas.create(
158
+ system_prompt: 'As a Life Coach, you are a dedicated professional...',
159
+ persona_name: 'Life Coach',
160
+ pipeline_mode: 'full'
161
+ )
162
+
163
+ # Create a persona with advanced configuration
164
+ persona = client.personas.create(
165
+ system_prompt: 'As a Life Coach...',
166
+ persona_name: 'Life Coach',
167
+ pipeline_mode: 'full',
168
+ context: 'Here are a few times that you have helped...',
169
+ default_replica_id: 'rfe12d8b9597',
170
+ document_ids: ['d1234567890', 'd2468101214'],
171
+ document_tags: ['product_info', 'company_policies'],
172
+ layers: {
173
+ llm: {
174
+ model: 'tavus-gpt-4o',
175
+ base_url: 'your-base-url',
176
+ api_key: 'your-api-key',
177
+ tools: [
178
+ {
179
+ type: 'function',
180
+ function: {
181
+ name: 'get_current_weather',
182
+ description: 'Get the current weather in a given location',
183
+ parameters: {
184
+ type: 'object',
185
+ properties: {
186
+ location: {
187
+ type: 'string',
188
+ description: 'The city and state, e.g. San Francisco, CA'
189
+ },
190
+ unit: {
191
+ type: 'string',
192
+ enum: ['celsius', 'fahrenheit']
193
+ }
194
+ },
195
+ required: ['location']
196
+ }
197
+ }
198
+ }
199
+ ]
200
+ },
201
+ tts: {
202
+ tts_engine: 'cartesia',
203
+ external_voice_id: 'external-voice-id',
204
+ voice_settings: {
205
+ speed: 0.5,
206
+ emotion: ['positivity:high', 'curiosity']
207
+ },
208
+ tts_emotion_control: 'false',
209
+ tts_model_name: 'sonic'
210
+ },
211
+ perception: {
212
+ perception_model: 'raven-0',
213
+ ambient_awareness_queries: [
214
+ 'Is the user showing an ID card?',
215
+ 'Does the user appear distressed or uncomfortable?'
216
+ ]
217
+ },
218
+ stt: {
219
+ stt_engine: 'tavus-turbo',
220
+ participant_pause_sensitivity: 'low',
221
+ participant_interrupt_sensitivity: 'low',
222
+ hotwords: 'This is a hotword example',
223
+ smart_turn_detection: true
224
+ }
225
+ }
226
+ )
227
+
228
+ # Response includes:
229
+ # {
230
+ # "persona_id" => "p5317866",
231
+ # "persona_name" => "Life Coach",
232
+ # "created_at" => "2025-10-04T12:00:00Z"
233
+ # }
234
+ ```
235
+
236
+ #### Get a Persona
237
+
238
+ ```ruby
239
+ persona = client.personas.get('p5317866')
240
+ ```
241
+
242
+ #### List Personas
243
+
244
+ ```ruby
245
+ # List all personas
246
+ personas = client.personas.list
247
+
248
+ # List with pagination and filters
249
+ personas = client.personas.list(
250
+ limit: 20,
251
+ page: 1,
252
+ persona_type: 'user' # or 'system'
253
+ )
254
+
255
+ # Response includes:
256
+ # {
257
+ # "data" => [...],
258
+ # "total_count" => 123
259
+ # }
260
+ ```
261
+
262
+ #### Update a Persona (JSON Patch)
263
+
264
+ ```ruby
265
+ # Update multiple fields using patch operations
266
+ operations = [
267
+ { op: 'replace', path: '/persona_name', value: 'Wellness Advisor' },
268
+ { op: 'replace', path: '/default_replica_id', value: 'r79e1c033f' },
269
+ { op: 'replace', path: '/context', value: 'Updated context...' },
270
+ { op: 'replace', path: '/layers/llm/model', value: 'tavus-gpt-4o' },
271
+ { op: 'add', path: '/layers/tts/tts_emotion_control', value: 'true' },
272
+ { op: 'remove', path: '/layers/stt/hotwords' }
273
+ ]
274
+
275
+ result = client.personas.patch('p5317866', operations)
276
+
277
+ # Helper method to build patch operations
278
+ operation = client.personas.build_patch_operation(
279
+ '/persona_name',
280
+ 'New Name',
281
+ operation: 'replace'
282
+ )
283
+
284
+ # Convenient method to update a single field
285
+ result = client.personas.update_field(
286
+ 'p5317866',
287
+ '/persona_name',
288
+ 'New Name'
289
+ )
290
+ ```
291
+
292
+ #### Delete a Persona
293
+
294
+ ```ruby
295
+ result = client.personas.delete('p5317866')
296
+ ```
297
+
298
+ ### Replicas
299
+
300
+ #### Create a Replica
301
+
302
+ ```ruby
303
+ # Create a replica for conversational video
304
+ replica = client.replicas.create(
305
+ train_video_url: 'https://my-bucket.s3.amazonaws.com/training-video.mp4',
306
+ replica_name: 'My Replica',
307
+ model_name: 'phoenix-3', # Optional, defaults to phoenix-3
308
+ consent_video_url: 'https://my-bucket.s3.amazonaws.com/consent-video.mp4', # Optional
309
+ callback_url: 'https://yourwebsite.com/webhook'
310
+ )
311
+
312
+ # Response includes:
313
+ # {
314
+ # "replica_id" => "r783537ef5",
315
+ # "status" => "started"
316
+ # }
317
+ ```
318
+
319
+ #### Get a Replica
320
+
321
+ ```ruby
322
+ # Get basic replica info
323
+ replica = client.replicas.get('r783537ef5')
324
+
325
+ # Get detailed replica info with verbose flag
326
+ replica = client.replicas.get('r783537ef5', verbose: true)
327
+ ```
328
+
329
+ #### List Replicas
330
+
331
+ ```ruby
332
+ # List all replicas
333
+ replicas = client.replicas.list
334
+
335
+ # List with filters
336
+ replicas = client.replicas.list(
337
+ limit: 20,
338
+ page: 1,
339
+ replica_type: 'user', # or 'system'
340
+ replica_ids: 're1074c227,r243eed46c',
341
+ verbose: true
342
+ )
343
+ ```
344
+
345
+ #### Rename a Replica
346
+
347
+ ```ruby
348
+ client.replicas.rename('r783537ef5', 'Updated Replica Name')
349
+ ```
350
+
351
+ #### Delete a Replica
352
+
353
+ ```ruby
354
+ # Soft delete
355
+ client.replicas.delete('r783537ef5')
356
+
357
+ # Hard delete (irreversible - deletes all assets)
358
+ client.replicas.delete('r783537ef5', hard: true)
359
+ ```
360
+
361
+ ### Objectives
362
+
363
+ #### Create Objectives
364
+
365
+ ```ruby
366
+ objectives = client.objectives.create(
367
+ data: [
368
+ {
369
+ objective_name: 'Gather User Feedback',
370
+ objective_prompt: 'Ask the user about their experience with the product',
371
+ confirmation_mode: 'automatic',
372
+ modality: 'verbal'
373
+ }
374
+ ]
375
+ )
376
+ ```
377
+
378
+ #### Get an Objective
379
+
380
+ ```ruby
381
+ objective = client.objectives.get('o12345')
382
+ ```
383
+
384
+ #### List Objectives
385
+
386
+ ```ruby
387
+ objectives = client.objectives.list(limit: 20, page: 1)
388
+ ```
389
+
390
+ #### Update an Objective
391
+
392
+ ```ruby
393
+ operations = [
394
+ { op: 'replace', path: '/data/0/objective_name', value: 'Updated Objective' },
395
+ { op: 'replace', path: '/data/0/confirmation_mode', value: 'manual' }
396
+ ]
397
+
398
+ client.objectives.patch('o12345', operations)
399
+
400
+ # Or update a single field
401
+ client.objectives.update_field('o12345', '/data/0/objective_name', 'New Name')
402
+ ```
403
+
404
+ #### Delete an Objective
405
+
406
+ ```ruby
407
+ client.objectives.delete('o12345')
408
+ ```
409
+
410
+ ### Guardrails
411
+
412
+ #### Create Guardrails
413
+
414
+ ```ruby
415
+ guardrails = client.guardrails.create(
416
+ name: 'Healthcare Compliance Guardrails',
417
+ data: [
418
+ {
419
+ guardrails_prompt: 'Never discuss competitor products or share sensitive medical information',
420
+ modality: 'verbal',
421
+ callback_url: 'https://your-server.com/webhook'
422
+ }
423
+ ]
424
+ )
425
+ ```
426
+
427
+ #### Get Guardrails
428
+
429
+ ```ruby
430
+ guardrails = client.guardrails.get('g12345')
431
+ ```
432
+
433
+ #### List Guardrails
434
+
435
+ ```ruby
436
+ guardrails = client.guardrails.list(limit: 20, page: 1)
437
+ ```
438
+
439
+ #### Update Guardrails
440
+
441
+ ```ruby
442
+ operations = [
443
+ { op: 'replace', path: '/data/0/guardrails_prompt', value: 'Updated guardrails prompt' },
444
+ { op: 'add', path: '/data/0/callback_url', value: 'https://new-server.com/webhook' }
445
+ ]
446
+
447
+ client.guardrails.patch('g12345', operations)
448
+
449
+ # Or update a single field
450
+ client.guardrails.update_field('g12345', '/data/0/guardrails_prompt', 'New prompt')
451
+ ```
452
+
453
+ #### Delete Guardrails
454
+
455
+ ```ruby
456
+ client.guardrails.delete('g12345')
457
+ ```
458
+
459
+ ### Documents (Knowledge Base)
460
+
461
+ #### Create a Document
462
+
463
+ ```ruby
464
+ # Upload from URL
465
+ document = client.documents.create(
466
+ document_url: 'https://example.com/document.pdf',
467
+ document_name: 'Product Documentation',
468
+ callback_url: 'https://your-server.com/webhook',
469
+ tags: ['product', 'documentation'],
470
+ properties: { department: 'sales', priority: 'high' }
471
+ )
472
+
473
+ # Supported formats: .pdf, .txt, .docx, .doc, .png, .jpg, .pptx, .csv, .xlsx
474
+ # Also supports website URLs for snapshots
475
+ ```
476
+
477
+ #### Get a Document
478
+
479
+ ```ruby
480
+ document = client.documents.get('d290f1ee-6c54-4b01-90e6-d701748f0851')
481
+ ```
482
+
483
+ #### List Documents
484
+
485
+ ```ruby
486
+ # List all documents
487
+ documents = client.documents.list
488
+
489
+ # List with filters
490
+ documents = client.documents.list(
491
+ limit: 20,
492
+ page: 0,
493
+ sort: 'descending',
494
+ status: 'ready',
495
+ tags: 'product,documentation'
496
+ )
497
+ ```
498
+
499
+ #### Update a Document
500
+
501
+ ```ruby
502
+ client.documents.update(
503
+ 'd290f1ee-6c54-4b01-90e6-d701748f0851',
504
+ document_name: 'Updated Document Name',
505
+ tags: ['updated', 'important']
506
+ )
507
+ ```
508
+
509
+ #### Delete a Document
510
+
511
+ ```ruby
512
+ client.documents.delete('d290f1ee-6c54-4b01-90e6-d701748f0851')
513
+ ```
514
+
515
+ ### Videos
516
+
517
+ #### Generate a Video
518
+
519
+ ```ruby
520
+ # Generate video from text script
521
+ video = client.videos.create(
522
+ replica_id: 'r783537ef5',
523
+ script: 'Hello from Tavus! Enjoy your new replica',
524
+ video_name: 'My First Video',
525
+ background_url: 'https://yourwebsite.com/',
526
+ callback_url: 'https://yourwebsite.com/webhook'
527
+ )
528
+
529
+ # Or use the convenient method
530
+ video = client.videos.generate_from_text(
531
+ replica_id: 'r783537ef5',
532
+ script: 'Hello from Tavus!',
533
+ video_name: 'My Video'
534
+ )
535
+
536
+ # Generate video from audio file
537
+ video = client.videos.generate_from_audio(
538
+ replica_id: 'r783537ef5',
539
+ audio_url: 'https://my-bucket.s3.amazonaws.com/audio.mp3',
540
+ video_name: 'Audio Video'
541
+ )
542
+
543
+ # Advanced options
544
+ video = client.videos.create(
545
+ replica_id: 'r783537ef5',
546
+ script: 'Hello!',
547
+ fast: true, # Use fast rendering
548
+ transparent_background: true, # Requires fast: true
549
+ watermark_image_url: 'https://s3.amazonaws.com/watermark.png',
550
+ background_source_url: 'https://my-bucket.s3.amazonaws.com/background.mp4'
551
+ )
552
+
553
+ # Response includes:
554
+ # {
555
+ # "video_id" => "abcd123",
556
+ # "status" => "queued",
557
+ # "hosted_url" => "https://tavus.video/abcd123"
558
+ # }
559
+ ```
560
+
561
+ #### Get a Video
562
+
563
+ ```ruby
564
+ # Get basic video info
565
+ video = client.videos.get('abcd123')
566
+
567
+ # Get detailed info with thumbnails
568
+ video = client.videos.get('abcd123', verbose: true)
569
+
570
+ # Response includes download_url, stream_url, hosted_url when ready
571
+ ```
572
+
573
+ #### List Videos
574
+
575
+ ```ruby
576
+ videos = client.videos.list(limit: 20, page: 1)
577
+ ```
578
+
579
+ #### Rename a Video
580
+
581
+ ```ruby
582
+ client.videos.rename('abcd123', 'New Video Name')
583
+ ```
584
+
585
+ #### Delete a Video
586
+
587
+ ```ruby
588
+ # Soft delete
589
+ client.videos.delete('abcd123')
590
+
591
+ # Hard delete (irreversible - deletes all assets)
592
+ client.videos.delete('abcd123', hard: true)
593
+ ```
594
+
595
+ ## Error Handling
596
+
597
+ The gem provides specific error classes for different API responses:
598
+
599
+ ```ruby
600
+ begin
601
+ conversation = client.conversations.create(replica_id: 'invalid')
602
+ rescue Tavus::AuthenticationError => e
603
+ # Handle authentication errors (401)
604
+ puts "Authentication failed: #{e.message}"
605
+ rescue Tavus::BadRequestError => e
606
+ # Handle bad request errors (400)
607
+ puts "Bad request: #{e.message}"
608
+ rescue Tavus::NotFoundError => e
609
+ # Handle not found errors (404)
610
+ puts "Resource not found: #{e.message}"
611
+ rescue Tavus::ValidationError => e
612
+ # Handle validation errors (422)
613
+ puts "Validation failed: #{e.message}"
614
+ rescue Tavus::RateLimitError => e
615
+ # Handle rate limit errors (429)
616
+ puts "Rate limit exceeded: #{e.message}"
617
+ rescue Tavus::ServerError => e
618
+ # Handle server errors (5xx)
619
+ puts "Server error: #{e.message}"
620
+ rescue Tavus::ApiError => e
621
+ # Handle any other API errors
622
+ puts "API error: #{e.message}"
623
+ rescue Tavus::ConfigurationError => e
624
+ # Handle configuration errors
625
+ puts "Configuration error: #{e.message}"
626
+ end
627
+ ```
628
+
629
+ ## API Reference
630
+
631
+ ### Conversations
632
+
633
+ | Method | Description |
634
+ |--------|-------------|
635
+ | `create(replica_id:, persona_id:, **options)` | Create a new conversation |
636
+ | `get(conversation_id)` | Get a conversation by ID |
637
+ | `list(**options)` | List all conversations |
638
+ | `end(conversation_id)` | End a conversation |
639
+ | `delete(conversation_id)` | Delete a conversation |
640
+
641
+ ### Personas
642
+
643
+ | Method | Description |
644
+ |--------|-------------|
645
+ | `create(system_prompt:, **options)` | Create a new persona |
646
+ | `get(persona_id)` | Get a persona by ID |
647
+ | `list(**options)` | List all personas |
648
+ | `patch(persona_id, operations)` | Update a persona using JSON Patch |
649
+ | `build_patch_operation(field, value, operation:)` | Build a JSON Patch operation |
650
+ | `update_field(persona_id, field, value)` | Update a single field |
651
+ | `delete(persona_id)` | Delete a persona |
652
+
653
+ ### Replicas
654
+
655
+ | Method | Description |
656
+ |--------|-------------|
657
+ | `create(train_video_url:, **options)` | Create a new replica |
658
+ | `get(replica_id, verbose: false)` | Get a replica by ID |
659
+ | `list(**options)` | List all replicas |
660
+ | `rename(replica_id, replica_name)` | Rename a replica |
661
+ | `delete(replica_id, hard: false)` | Delete a replica |
662
+
663
+ ### Objectives
664
+
665
+ | Method | Description |
666
+ |--------|-------------|
667
+ | `create(data:)` | Create new objectives |
668
+ | `get(objectives_id)` | Get an objective by ID |
669
+ | `list(**options)` | List all objectives |
670
+ | `patch(objectives_id, operations)` | Update an objective using JSON Patch |
671
+ | `build_patch_operation(field, value, operation:)` | Build a JSON Patch operation |
672
+ | `update_field(objectives_id, field, value)` | Update a single field |
673
+ | `delete(objectives_id)` | Delete an objective |
674
+
675
+ ### Guardrails
676
+
677
+ | Method | Description |
678
+ |--------|-------------|
679
+ | `create(name:, data:)` | Create new guardrails |
680
+ | `get(guardrails_id)` | Get guardrails by ID |
681
+ | `list(**options)` | List all guardrails |
682
+ | `patch(guardrails_id, operations)` | Update guardrails using JSON Patch |
683
+ | `build_patch_operation(field, value, operation:)` | Build a JSON Patch operation |
684
+ | `update_field(guardrails_id, field, value)` | Update a single field |
685
+ | `delete(guardrails_id)` | Delete guardrails |
686
+
687
+ ### Documents (Knowledge Base)
688
+
689
+ | Method | Description |
690
+ |--------|-------------|
691
+ | `create(document_url:, **options)` | Upload a new document |
692
+ | `get(document_id)` | Get a document by ID |
693
+ | `list(**options)` | List all documents |
694
+ | `update(document_id, **options)` | Update document metadata |
695
+ | `delete(document_id)` | Delete a document |
696
+
697
+ ### Videos
698
+
699
+ | Method | Description |
700
+ |--------|-------------|
701
+ | `create(replica_id:, **options)` | Generate a new video |
702
+ | `generate_from_text(replica_id:, script:, **options)` | Generate video from text |
703
+ | `generate_from_audio(replica_id:, audio_url:, **options)` | Generate video from audio |
704
+ | `get(video_id, verbose: false)` | Get a video by ID |
705
+ | `list(**options)` | List all videos |
706
+ | `rename(video_id, video_name)` | Rename a video |
707
+ | `delete(video_id, hard: false)` | Delete a video |
708
+
709
+ ## Development
710
+
711
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
712
+
713
+ To install this gem onto your local machine, run `bundle exec rake install`.
714
+
715
+ ## CI/CD
716
+
717
+ This project uses GitHub Actions for continuous integration:
718
+
719
+ - **Tests**: Runs on Ruby 2.7, 3.0, 3.1, 3.2, and 3.3
720
+ - **Linting**: RuboCop style checking
721
+ - **Security**: Bundle audit for dependency vulnerabilities
722
+
723
+ [![CI](https://github.com/upriser/tavus/actions/workflows/ci.yml/badge.svg)](https://github.com/upriser/tavus/actions/workflows/ci.yml)
724
+
725
+ ## Contributing
726
+
727
+ Bug reports and pull requests are welcome on GitHub at https://github.com/upriser/tavus.
728
+
729
+ ## License
730
+
731
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
732
+
733
+ ## Resources
734
+
735
+ - [Tavus API Documentation](https://docs.tavus.io)
736
+ - [Tavus Developer Portal](https://platform.tavus.io)