{"id":4238,"date":"2024-10-22T11:50:11","date_gmt":"2024-10-22T18:50:11","guid":{"rendered":"https:\/\/www.couchbase.com\/blog\/part2-ai-in-action-enhancing-and-not-replacing-jobs\/"},"modified":"2024-10-22T11:50:11","modified_gmt":"2024-10-22T18:50:11","slug":"part2-ai-in-action-enhancing-and-not-replacing-jobs","status":"publish","type":"post","link":"https:\/\/www.couchbase.com\/blog\/es\/part2-ai-in-action-enhancing-and-not-replacing-jobs\/","title":{"rendered":"Part 2 &#8211; AI in Action: Enhancing and Not Replacing Jobs"},"content":{"rendered":"\n<p><span>Welcome back to the second part of our two-part series on building an interactive customer service support application that empowers support agents with the help of AI. The goal is to enhance their important work by leveraging previously resolved answers for current open questions quickly and intuitively using vector search.<\/span><\/p>\n\n\n\n<p><b><i>tl;dr In case you want to skip right to the implementation, you can find a fully working example of this application on <\/i><\/b><a href=\"https:\/\/github.com\/hummusonrails\/whatsapp_support_app\"><b><i>GitHub<\/i><\/b><\/a><b><i> along with detailed README instructions.<\/i><\/b><\/p>\n\n\n\n<p><span><a href=\"https:\/\/www.couchbase.com\/blog\/ai-in-action-enhancing-and-not-replacing-jobs\/\">In part one,<\/a> we set up all of our services we need for our application, including:<\/span><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><span>Couchbase Capella<\/span><\/li>\n\n\n<li><span>Vonage Messages API<\/span><\/li>\n\n\n<li><span>OpenAI Embeddings API<\/span><\/li>\n\n<\/ul>\n\n\n\n<p><blockquote class=\"wp-embedded-content\" data-secret=\"MwsEwlacnj\"><a href=\"https:\/\/www.couchbase.com\/blog\/ai-in-action-enhancing-and-not-replacing-jobs\/\">AI in Action: Enhancing and Not Replacing Jobs<\/a><\/blockquote><iframe loading=\"lazy\" class=\"wp-embedded-content\" sandbox=\"allow-scripts\" security=\"restricted\" style=\"position: absolute; visibility: hidden;\" title=\"&#8220;AI in Action: Enhancing and Not Replacing Jobs&#8221; &#8212; The Couchbase Blog\" src=\"https:\/\/www.couchbase.com\/blog\/ai-in-action-enhancing-and-not-replacing-jobs\/embed\/#?secret=B53qJ37HnR#?secret=MwsEwlacnj\" data-secret=\"MwsEwlacnj\" width=\"500\" height=\"282\" frameborder=\"0\" marginwidth=\"0\" marginheight=\"0\" scrolling=\"no\"><\/iframe><\/p>\n\n\n\n<p><span>We also scaffolded the backend of our application with Ruby on Rails. Now, we are going to write the code that is going to connect these three services and bring them together to support the work of customer service agents responding to user inquiries via WhatsApp.<\/span><\/p>\n\n\n\n<p><span>First, let\u2019s go ahead and define the business logic of our application.<\/span><\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span>Defining application business logic<\/span><\/h2>\n\n\n\n<p><span>One of the beautiful things about Ruby on Rails is its notion of convention over configuration. This means that we do not need to reinvent the wheel for where to put the business logic of our application. What we now build will be a simplified version. All of the application code will not be shown in this tutorial, but as mentioned at the beginning, the entire codebase is available to clone locally on <\/span><a href=\"https:\/\/github.com\/hummusonrails\/whatsapp_support_app\"><span>GitHub<\/span><\/a><span>.<\/span><\/p>\n\n\n\n<p><span>In short, we will be defining our logic in model and controller files.\u00a0<\/span><\/p>\n\n\n\n<p><span>The application will have three models:<\/span><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><b>Ticket<\/b><\/li>\n\n\n<li><b>User<\/b><\/li>\n\n\n<li><b>Agent<\/b><\/li>\n\n<\/ul>\n\n\n\n<p><span>The application will also have two controllers:<\/span><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><b>Messages Controller<\/b><\/li>\n\n\n<li><b>Dashboard Controller<\/b><\/li>\n\n<\/ul>\n\n\n\n<p><span>Other parts of the application that will need to be configured include the Vonage, OpenAI and Couchbase initializers in <\/span><b><code>config\/initializers<\/code><\/b><span>, and the routes for the application in <\/span><b><code>config\/routes.rb<\/code><\/b><span>. All of those can be viewed on GitHub and copied directly from there to your own codebase.<\/span><\/p>\n\n\n\n<p><span>Let\u2019s start with the models.<\/span><\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><span>Create models<\/span><\/h3>\n\n\n\n<p><span>It is true that classically Rails works with SQL databases right out of the box. What if you want to combine the best of Rails with the best of a JSON document database like Couchbase? That\u2019s now possible to do so in a streamlined way thanks to the Couchbase Ruby ORM that makes available a lot of the ActiveRecord functionality in Rails for working with Couchbase. Each of our models will inherit from <\/span><i><span><code>CouchbaseOrm::Base<\/code><\/span><\/i><span> to make the ORM\u2019s methods available to it.<\/span><\/p>\n\n\n\n<h4 class=\"wp-block-heading\"><span>The Ticket model<\/span><\/h4>\n\n\n\n<p><span>A ticket in the application needs to hold information about the query, its current status, who asked it, the summary of the eventual resolution, and the vector embedding of that resolution. We can define each of those as attributes:<\/span><\/p>\n\n\n<p>[crayon nums=&#8221;false&#8221; lang=&#8221;default&#8221; decode=&#8221;true&#8221;]OPEN = &#8216;open&#8217;<br \/>\nRESOLVED = &#8216;resolved&#8217;<\/p>\n<p>belongs_to :user<br \/>\nbelongs_to :agent, optional: true<\/p>\n<p>attribute :query, :string<br \/>\nattribute :status, :string, default: OPEN<br \/>\nattribute :summary, :string<br \/>\nattribute :embedding, :array, type: :float, default: []<br \/>\nattribute :created_at, :datetime, default: -&gt; { Time.now }<br \/>\nattribute :updated_at, :datetime, default: -&gt; { Time.now }[\/crayon]<\/p>\n\n\n\n<p><span>As you can see above, we also define two constants, <\/span><b>OPEN<\/b><span> and <\/span><b>RESOLVED, <\/b><span>which are the two possible status states a ticket can be in. We also create the relationship of a ticket to a user and to an agent, optionally.<\/span><\/p>\n\n\n\n<p><span>In addition to defining the attributes, we also want to create some methods on the ticket that can be accessed in our application. We want to create helper methods that can be used to check on the status of a ticket like such:<\/span><\/p>\n\n\n<p>[crayon nums=&#8221;false&#8221; lang=&#8221;default&#8221; decode=&#8221;true&#8221;]  def self.open_tickets<br \/>\n\u00a0\u00a0\u00a0\u00a0where(status: OPEN).to_a<br \/>\n\u00a0\u00a0end<\/p>\n<p>\u00a0\u00a0def self.resolved_tickets<br \/>\n\u00a0\u00a0\u00a0\u00a0where(status: RESOLVED).to_a<br \/>\n\u00a0\u00a0end<\/p>\n<p>\u00a0\u00a0def open?<br \/>\n\u00a0\u00a0\u00a0\u00a0status == OPEN<br \/>\n\u00a0\u00a0end<\/p>\n<p>\u00a0\u00a0def resolved?<br \/>\n\u00a0\u00a0\u00a0\u00a0status == RESOLVED<br \/>\n\u00a0\u00a0end<\/p>\n<p>\u00a0\u00a0def mark_as_resolved!<br \/>\n\u00a0\u00a0\u00a0\u00a0update!(status: RESOLVED)<br \/>\n\u00a0\u00a0end[\/crayon]<\/p>\n\n\n\n<p><span>All together, the Ticket model will look like the following, including additional validations:<\/span><\/p>\n\n\n<p>[crayon nums=&#8221;false&#8221; lang=&#8221;default&#8221; decode=&#8221;true&#8221;]class Ticket &lt; CouchbaseOrm::Base<br \/>\n\u00a0\u00a0OPEN = &#8216;open&#8217;<br \/>\n\u00a0\u00a0RESOLVED = &#8216;resolved&#8217;<\/p>\n<p>\u00a0\u00a0belongs_to :user<br \/>\n\u00a0\u00a0belongs_to :agent, optional: true<\/p>\n<p>\u00a0\u00a0attribute :query, :string<br \/>\n\u00a0\u00a0attribute :status, :string, default: OPEN<br \/>\n\u00a0\u00a0attribute :summary, :string<br \/>\n\u00a0\u00a0attribute :embedding, :array, type: :float, default: []<br \/>\n\u00a0\u00a0attribute :created_at, :datetime, default: -&gt; { Time.now }<br \/>\n\u00a0\u00a0attribute :updated_at, :datetime, default: -&gt; { Time.now }<\/p>\n<p>\u00a0\u00a0ensure_design_document!<\/p>\n<p>\u00a0\u00a0validates :query, presence: true<\/p>\n<p>\u00a0\u00a0before_save :set_timestamps<br \/>\n\u00a0\u00a0def self.open_tickets<br \/>\n\u00a0\u00a0\u00a0\u00a0where(status: OPEN).to_a<br \/>\n\u00a0\u00a0end<\/p>\n<p>\u00a0\u00a0def self.resolved_tickets<br \/>\n\u00a0\u00a0\u00a0\u00a0where(status: RESOLVED).to_a<br \/>\n\u00a0\u00a0end<\/p>\n<p>\u00a0\u00a0def open?<br \/>\n\u00a0\u00a0\u00a0\u00a0status == OPEN<br \/>\n\u00a0\u00a0end<\/p>\n<p>\u00a0\u00a0def resolved?<br \/>\n\u00a0\u00a0\u00a0\u00a0status == RESOLVED<br \/>\n\u00a0\u00a0end<\/p>\n<p>\u00a0\u00a0def mark_as_resolved!<br \/>\n\u00a0\u00a0\u00a0\u00a0update!(status: RESOLVED)<br \/>\n\u00a0\u00a0end<\/p>\n<p>\u00a0\u00a0private<\/p>\n<p>\u00a0\u00a0def set_timestamps<br \/>\n\u00a0\u00a0\u00a0\u00a0self.updated_at = Time.now<br \/>\n\u00a0\u00a0end<br \/>\nend[\/crayon]<\/p>\n\n\n\n<p><span>Let\u2019s go down a similar road for the User model.<\/span><\/p>\n\n\n\n<h4 class=\"wp-block-heading\"><span>The User model<\/span><\/h4>\n\n\n\n<p><span>A user in the application needs to be able to have many tickets since it&#8217;s possible a customer may be dealing with multiple issues they are seeking support for.\u00a0<\/span><\/p>\n\n\n\n<p><span>We also want to ensure that we can easily find a user by their WhatsApp number, so we\u2019ll create a helper method on the model for that as well. In this helper method, a user will either be found by their number, or if not, then a new user will be created and a portion of their number will be used to populate the name field.<\/span><\/p>\n\n\n<p>[crayon nums=&#8221;false&#8221; lang=&#8221;default&#8221; decode=&#8221;true&#8221;]class User &lt; CouchbaseOrm::Base<br \/>\n\u00a0\u00a0has_many :tickets<\/p>\n<p>\u00a0\u00a0attribute :whatsapp_number, :string<br \/>\n\u00a0\u00a0attribute :name, :string<br \/>\n\u00a0\u00a0attribute :created_at, :datetime, default: -&gt; { Time.now }<br \/>\n\u00a0\u00a0attribute :updated_at, :datetime, default: -&gt; { Time.now }<\/p>\n<p>\u00a0\u00a0validates :whatsapp_number, presence: true<br \/>\n\u00a0\u00a0validates :name, presence: true<\/p>\n<p>\u00a0\u00a0before_save :set_timestamps<\/p>\n<p>\u00a0\u00a0private<\/p>\n<p>\u00a0\u00a0def set_timestamps<br \/>\n\u00a0\u00a0\u00a0\u00a0self.updated_at = Time.now<br \/>\n\u00a0\u00a0end<\/p>\n<p>\u00a0\u00a0def self.find_or_create_user_by_whatsapp_number(whatsapp_number)<br \/>\n\u00a0\u00a0\u00a0\u00a0user = User.find_by(whatsapp_number: whatsapp_number)<br \/>\n\u00a0\u00a0\u00a0\u00a0unless user<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0user = User.create!(whatsapp_number: whatsapp_number, name: &#8220;User #{whatsapp_number[-4..-1]}&#8221;)<br \/>\n\u00a0\u00a0\u00a0\u00a0end<br \/>\n\u00a0\u00a0\u00a0\u00a0user<br \/>\n\u00a0\u00a0end<br \/>\nend[\/crayon]<\/p>\n\n\n\n<p><span>The last model we will create is the Agent model, let\u2019s do it.<\/span><\/p>\n\n\n\n<h4 class=\"wp-block-heading\"><span>The Agent Model<\/span><\/h4>\n\n\n\n<p><span>In many ways, the Agent model parallels the User model in that they both have many tickets. One key distinction is the agent does not use their own WhatsApp number to communicate in the application. Rather, they communicate with users through the application dashboard and the Vonage Messages API programmatically sends their messages to the user\u2019s WhatsApp inbox.<\/span><\/p>\n\n\n<p>[crayon nums=&#8221;false&#8221; lang=&#8221;default&#8221; decode=&#8221;true&#8221;]require &#8216;couchbase-orm&#8217;<\/p>\n<p>class Agent &lt; CouchbaseOrm::Base<br \/>\n\u00a0\u00a0has_many :tickets<\/p>\n<p>\u00a0\u00a0attribute :name, :string<br \/>\n\u00a0\u00a0attribute :email, :string<br \/>\n\u00a0\u00a0attribute :created_at, :datetime, default: -&gt; { Time.now }<br \/>\n\u00a0\u00a0attribute :updated_at, :datetime, default: -&gt; { Time.now }<\/p>\n<p>\u00a0\u00a0validates :email, presence: true, uniqueness: true<br \/>\n\u00a0\u00a0validates :name, presence: true<\/p>\n<p>\u00a0\u00a0before_save :set_timestamps<\/p>\n<p>\u00a0\u00a0private<\/p>\n<p>\u00a0\u00a0def set_timestamps<br \/>\n\u00a0\u00a0\u00a0\u00a0self.updated_at = Time.now<br \/>\n\u00a0\u00a0end<br \/>\nend[\/crayon]<\/p>\n\n\n\n<p><span>At this point, we have created our models. It\u2019s not time to define the controllers.<\/span><\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><span>Create controllers<\/span><\/h3>\n\n\n\n<p><span>The application will have two controllers that define what happens inside each route of the site. Namely, a <\/span><i><span><code>dashboard_controller<\/code><\/span><\/i><span> that oversees the functionality of the Dashboard, and a <\/span><i><span><code>messages_controller<\/code><\/span><\/i> <span>that oversees the messaging functionality.<\/span><\/p>\n\n\n\n<p><span>We\u2019ll start with the Dashboard first.<\/span><\/p>\n\n\n\n<h4 class=\"wp-block-heading\"><span>The Dashboard controller<\/span><\/h4>\n\n\n\n<p><span>The <\/span><b>index<\/b><span> and <\/span><b>show<\/b><span> views will both be defined in the dashboard controller, along with helper methods to fetch the vector similarity of previously resolved tickets to support agents in their work with current tickets.\u00a0<\/span><\/p>\n\n\n\n<p><span>The Couchbase Ruby SDK includes many useful abstractions to make interacting with the functionality of Couchbase more streamlined. This includes creating a new vector search using <\/span><b><code>Couchbase::VectorSearch.new<\/code><\/b><span> defining the arguments inside the instantiation of a new <\/span><b><code>VectorSearch<\/code> <\/b><span>object like such:<\/span><\/p>\n\n\n<p>[crayon nums=&#8221;false&#8221; lang=&#8221;default&#8221; decode=&#8221;true&#8221;]  \u00a0 request = Couchbase::SearchRequest.new(<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Couchbase::VectorSearch.new(<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0[<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Couchbase::VectorQuery.new(&#8217;embedding&#8217;, embedding) do |q|<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0q.num_candidates = 2<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0q.boost = 0.3<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0end<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0],<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Couchbase::Options::VectorSearch.new(vector_query_combination: :and)<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0)<br \/>\n\u00a0\u00a0\u00a0\u00a0)[\/crayon]<\/p>\n\n\n\n<p><span>First, the <\/span><b><code>VectorSearch<\/code><\/b><span> is wrapped inside a new instance of <\/span><b><code>Couchbase::SearchRequest<\/code><\/b><span>, as the <\/span><b><code>VectorSearch<\/code><\/b><span> is a type of a search request. Then a new <\/span><b><code>Couchbase::VectorQuery<\/code><\/b><span> instance is passed to the <\/span><b><code>VectorSearch<\/code> <\/b><span>object specifying the field to search in (i.e., <\/span><i><span>embedding<\/span><\/i><span>) and the customer query converted into its own embedding as the second argument.<\/span><\/p>\n\n\n\n<p><span>With this in mind on how to create a vector search using the Ruby SDK let\u2019s take a look at the complete dashboard controller code:<\/span><\/p>\n\n\n<p>[crayon nums=&#8221;false&#8221; lang=&#8221;default&#8221; decode=&#8221;true&#8221;]class DashboardController &lt; ApplicationController<br \/>\n\u00a0\u00a0def index<br \/>\n\u00a0\u00a0\u00a0\u00a0@open_tickets = Ticket.where(status: &#8216;open&#8217;).to_a.sort_by { |ticket| ticket.created_at }.reverse<br \/>\n\u00a0\u00a0\u00a0\u00a0@resolved_tickets = Ticket.all.to_a.select { |ticket| ticket.status == Ticket::RESOLVED }.to_a.sort_by { |ticket| ticket.updated_at }.reverse<br \/>\n\u00a0\u00a0end<\/p>\n<p>\u00a0\u00a0def show<br \/>\n\u00a0\u00a0\u00a0\u00a0@ticket = find_ticket<br \/>\n\u00a0\u00a0\u00a0\u00a0@user = find_user<br \/>\n\u00a0\u00a0\u00a0\u00a0@suggestions = find_suggestions<br \/>\n\u00a0\u00a0end<\/p>\n<p>\u00a0\u00a0private<br \/>\n\u00a0\u00a0def find_ticket<br \/>\n\u00a0\u00a0\u00a0\u00a0Ticket.find(params[:id])<br \/>\n\u00a0\u00a0end<\/p>\n<p>\u00a0\u00a0def find_user<br \/>\n\u00a0\u00a0\u00a0\u00a0find_ticket.user<br \/>\n\u00a0\u00a0end<\/p>\n<p>\u00a0\u00a0def find_suggestions<br \/>\n\u00a0\u00a0\u00a0\u00a0search_similar_tickets(find_ticket.query)<br \/>\n\u00a0\u00a0end<\/p>\n<p>\u00a0\u00a0def search_similar_tickets(query)<br \/>\n\u00a0\u00a0\u00a0\u00a0embedding = OPENAI_CLIENT.embeddings(<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0parameters: {<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0model: &#8220;text-embedding-ada-002&#8221;,<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0input: query<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0}<br \/>\n\u00a0\u00a0\u00a0\u00a0)[&#8216;data&#8217;][0][&#8217;embedding&#8217;]<\/p>\n<p>\u00a0\u00a0\u00a0\u00a0cluster = Couchbase::Cluster.connect(<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0ENV[&#8216;COUCHBASE_CONNECTION_STRING&#8217;],<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0ENV[&#8216;COUCHBASE_USERNAME&#8217;],<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0ENV[&#8216;COUCHBASE_PASSWORD&#8217;]<br \/>\n\u00a0\u00a0\u00a0\u00a0)<\/p>\n<p>\u00a0\u00a0\u00a0\u00a0bucket = cluster.bucket(ENV[&#8216;COUCHBASE_BUCKET&#8217;])<\/p>\n<p>\u00a0\u00a0\u00a0\u00a0scope = bucket.scope(&#8216;_default&#8217;)<\/p>\n<p>\u00a0\u00a0\u00a0\u00a0request = Couchbase::SearchRequest.new(<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Couchbase::VectorSearch.new(<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0[<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Couchbase::VectorQuery.new(&#8217;embedding&#8217;, embedding) do |q|<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0q.num_candidates = 2<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0q.boost = 0.3<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0end<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0],<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Couchbase::Options::VectorSearch.new(vector_query_combination: :and)<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0)<br \/>\n\u00a0\u00a0\u00a0\u00a0)<\/p>\n<p>\u00a0\u00a0\u00a0\u00a0result = scope.search(&#8216;whatsapp_support_index&#8217;, request)<\/p>\n<p>\u00a0\u00a0\u00a0\u00a0result.rows.map do |row|<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0document = bucket.default_collection.get(row.id)<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0{<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0id: row.id,<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0score: row.score,<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0summary: document.content[&#8216;summary&#8217;]<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0}<br \/>\n\u00a0\u00a0\u00a0\u00a0end<br \/>\n\u00a0\u00a0end<br \/>\nend[\/crayon]<\/p>\n\n\n\n<p><span>With that code, we\u2019ve defined the functional logic for both the main dashboard view and the view of each individual ticket in the dashboard.\u00a0<\/span><\/p>\n\n\n\n<p><span>The frontend code can be found on <\/span><a href=\"https:\/\/github.com\/hummusonrails\/whatsapp_support_app\/tree\/main\/app\/views\/dashboard\"><span>GitHub<\/span><\/a><span> for both of these views and can be copied directly into your codebase or edited for your own specific needs.<\/span><\/p>\n\n\n\n<p><a href=\"https:\/\/github.com\/hummusonrails\/whatsapp_support_app\/tree\/main\/app\/views\/dashboard\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-16482 size-large\" src=\"https:\/\/www.couchbase.com\/wp-content\/uploads\/sites\/5\/2026\/05\/Screenshot-2024-10-22-at-12.33.05-PM-1024x652-1.png\" alt=\"\" width=\"900\" height=\"573\"><\/a><\/p>\n\n\n\n<h4 class=\"wp-block-heading\"><span>The Messages Controller<\/span><\/h4>\n\n\n\n<p><span>Now we need to define the logic for all of the messages that will be received by and sent through the application via WhatsApp. The messages controller will also be responsible for ensuring that new tickets and users are created when a user messages for the first time.\u00a0<\/span><\/p>\n\n\n\n<p><span>It\u2019s possible in a later iteration of the application that some of this functionality can be moved and separated out to different areas in the codebase, but for simplicity sake we will be keeping it all in the messages controller for now.<\/span><\/p>\n\n\n\n<p><span>One of the great aspects of Rails is the ability to dynamically inject new content on the browser window without needing to use JavaScript. This is accomplished with <\/span><a href=\"https:\/\/api.rubyonrails.org\/v7.1.3.4\/classes\/ActionCable\/Server\/Broadcasting.html\"><span>ActionCable<\/span><\/a><span>, a core feature of Rails. We will be also using ActionCable to update the dashboard individual ticket view with the latest messages sent and received with the Vonage Messages API.<\/span><\/p>\n\n\n\n<p><span>As mentioned in the <\/span><a href=\"https:\/\/www.couchbase.com\/blog\/ai-in-action-enhancing-and-not-replacing-jobs\/\"><em><span>Using the Vonage Messages API <\/span><\/em><\/a><span>section, for development purposes, we are building with the API sandbox, so as a result, our API call to send messages will be built manually as an HTTP POST request. Once you have completed the necessary steps outlined in the Vonage documentation to obtain a Meta WhatsApp business account, you can then use the Vonage Ruby SDK to abstract these HTTP requests for you.<\/span><\/p>\n\n\n\n<p><span>Here is the code for the controller:<\/span><\/p>\n\n\n<p>[crayon nums=&#8221;false&#8221; lang=&#8221;default&#8221; decode=&#8221;true&#8221;]class MessagesController &lt; ApplicationController<br \/>\n\u00a0\u00a0skip_before_action :verify_authenticity_token<\/p>\n<p>\u00a0\u00a0def inbound<br \/>\n\u00a0\u00a0\u00a0\u00a0whatsapp_number = params[:from]<br \/>\n\u00a0\u00a0\u00a0\u00a0text = params[:text]<br \/>\n\u00a0\u00a0\u00a0\u00a0@reply = { sender: &#8216;User&#8217;, body: text, created_at: Time.current }<br \/>\n\u00a0\u00a0\u00a0\u00a0@user = User.find_by(whatsapp_number: whatsapp_number)<\/p>\n<p>\u00a0\u00a0\u00a0\u00a0if @user<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0open_ticket = Ticket.find_by(user_id: @user.id, status: Ticket::OPEN)<\/p>\n<p>\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0if open_ticket.nil?<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Ticket.create!(user_id: @user.id, query: &#8220;Awaiting query&#8221;, status: Ticket::OPEN)<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0send_message(to: whatsapp_number, text: &#8220;Thank you for your message. Please describe your support query.&#8221;)<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0elsif open_ticket.query == &#8220;Awaiting query&#8221;<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0open_ticket.update!(query: text)<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0ActionCable.server.broadcast &#8220;messages_#{@user.id}&#8221;, { reply: @reply, ticket: open_ticket }<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0else<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0ActionCable.server.broadcast &#8220;messages_#{@user.id}&#8221;, { reply: @reply, ticket: open_ticket }<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0end<br \/>\n\u00a0\u00a0\u00a0\u00a0else<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0@user = User.create!(whatsapp_number: whatsapp_number, name: &#8220;User #{whatsapp_number[-4..-1]}&#8221;)<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0send_message(to: whatsapp_number, text: &#8220;Thank you for your message. Please describe your support query.&#8221;)<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Ticket.create!(user_id: @user.id, query: &#8220;Awaiting query&#8221;, status: Ticket::OPEN)<br \/>\n\u00a0\u00a0\u00a0\u00a0end<\/p>\n<p>\u00a0\u00a0\u00a0\u00a0respond_to do |format|<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0format.turbo_stream { render turbo_stream: turbo_stream.append(&#8220;messages_#{@user.id}&#8221;, partial: &#8220;messages\/reply&#8221;, locals: { reply: @reply }) }<br \/>\n\u00a0\u00a0\u00a0\u00a0end<br \/>\n\u00a0\u00a0end<\/p>\n<p>\u00a0\u00a0def reply<br \/>\n\u00a0\u00a0\u00a0\u00a0@ticket_id = params[:ticket_id]<br \/>\n\u00a0\u00a0\u00a0\u00a0@message = params[:message]<br \/>\n\u00a0\u00a0\u00a0\u00a0@resolved = params[:mark_as_resolved] == &#8220;1&#8221;<\/p>\n<p>\u00a0\u00a0\u00a0\u00a0@ticket = Ticket.find(@ticket_id)<br \/>\n\u00a0\u00a0\u00a0\u00a0@user = @ticket.user<\/p>\n<p>\u00a0\u00a0\u00a0\u00a0@reply = { sender: &#8216;Agent&#8217;, body: @message, created_at: Time.current }<\/p>\n<p>\u00a0\u00a0\u00a0\u00a0ActionCable.server.broadcast &#8220;messages_#{@user.id}&#8221;, { reply: @reply, ticket: @ticket }<\/p>\n<p>\u00a0\u00a0\u00a0\u00a0if @resolved<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0embedding = OPENAI_CLIENT.embeddings(<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0parameters: {<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0model: &#8220;text-embedding-ada-002&#8221;,<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0input: @message<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0}<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0)[&#8216;data&#8217;][0][&#8217;embedding&#8217;]<\/p>\n<p>\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0@ticket.update!(summary: @message, status: Ticket::RESOLVED, embedding: embedding)<\/p>\n<p>\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0send_message(<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0to: @user.whatsapp_number,<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0text: @message<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0)<\/p>\n<p>\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0respond_to do |format|<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0format.html { redirect_to request.referrer, notice: &#8220;Ticket marked as resolved.&#8221; }<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0end<br \/>\n\u00a0\u00a0\u00a0\u00a0else<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0send_message(<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0to: @user.whatsapp_number,<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0text: @message<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0)<\/p>\n<p>\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0respond_to do |format|<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0format.turbo_stream { render turbo_stream: turbo_stream.append(&#8220;messages_#{@user.id}&#8221;, partial: &#8220;messages\/reply&#8221;, locals: { reply: @reply }) }<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0end<br \/>\n\u00a0\u00a0\u00a0\u00a0end<br \/>\n\u00a0\u00a0end<\/p>\n<p>\u00a0\u00a0def status<br \/>\n\u00a0\u00a0\u00a0\u00a0head :ok<br \/>\n\u00a0\u00a0end<\/p>\n<p>\u00a0\u00a0private<\/p>\n<p>\u00a0\u00a0def send_message(to:, text:)<br \/>\n\u00a0\u00a0\u00a0\u00a0require &#8216;net\/http&#8217;<br \/>\n\u00a0\u00a0\u00a0\u00a0require &#8216;uri&#8217;<br \/>\n\u00a0\u00a0\u00a0\u00a0require &#8216;json&#8217;<\/p>\n<p>\u00a0\u00a0\u00a0\u00a0uri = URI.parse(&#8220;https:\/\/messages-sandbox.nexmo.com\/v1\/messages&#8221;)<\/p>\n<p>\u00a0\u00a0\u00a0\u00a0request = Net::HTTP::Post.new(uri)<br \/>\n\u00a0\u00a0\u00a0\u00a0request.basic_auth(ENV[&#8216;VONAGE_API_KEY&#8217;], ENV[&#8216;VONAGE_API_SECRET&#8217;])<br \/>\n\u00a0\u00a0\u00a0\u00a0request.content_type = &#8216;application\/json&#8217;<br \/>\n\u00a0\u00a0\u00a0\u00a0request[&#8216;Accept&#8217;] = &#8216;application\/json&#8217;<\/p>\n<p>\u00a0\u00a0\u00a0\u00a0request.body = {<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0from: ENV[&#8216;VONAGE_FROM_NUMBER&#8217;],<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0to: to,<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0message_type: &#8216;text&#8217;,<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0text: text,<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0channel: &#8216;whatsapp&#8217;<br \/>\n\u00a0\u00a0\u00a0\u00a0}.to_json<\/p>\n<p>\u00a0\u00a0\u00a0\u00a0http = Net::HTTP.new(uri.host, uri.port)<br \/>\n\u00a0\u00a0\u00a0\u00a0http.use_ssl = true<\/p>\n<p>\u00a0\u00a0\u00a0\u00a0response = http.request(request)<\/p>\n<p>\u00a0\u00a0\u00a0\u00a0puts &#8220;Response Code: #{response.code}&#8221;<br \/>\n\u00a0\u00a0\u00a0\u00a0puts &#8220;Response Body: #{response.body}&#8221;<br \/>\n\u00a0\u00a0end<br \/>\nend[\/crayon]<\/p>\n\n\n\n<p><span>With the creation of the messages controller, we have defined just about all of the functionality we need in our application.\u00a0<\/span><\/p>\n\n\n\n<p><span>Anything that you may be still trying to figure out is readily available in the fully complete and working example application on <\/span><a href=\"https:\/\/github.com\/hummusonrails\/whatsapp_support_app\/tree\/main\"><span>GitHub<\/span><\/a><span>.<\/span><\/p>\n\n\n\n<p><a href=\"https:\/\/github.com\/hummusonrails\/whatsapp_support_app\/tree\/main\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-large wp-image-16483\" src=\"https:\/\/www.couchbase.com\/wp-content\/uploads\/sites\/5\/2026\/05\/Screenshot-2024-10-22-at-12.39.14-PM-1024x618-1.png\" alt=\"\" width=\"900\" height=\"543\"><\/a><\/p>\n\n\n\n<p><span>All that is left to do now is to run our application!\u00a0<\/span><\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span>Running the application<\/span><\/h2>\n\n\n\n<p><span>To use the application, go ahead and open a terminal window and run <\/span><i><span><code>bin\/dev<\/code><\/span><\/i><span> inside the folder of the application. This will start up a development environment of the application. Now, in a separate terminal window, start ngrok, if you are using ngrok as described above to make your localhost environment externally accessible, by running <\/span><i><span><code>ngrok http 3000<\/code><\/span><\/i><span>.\u00a0<\/span><\/p>\n\n\n\n<p><span>Once you have done both commands, you can open in your browser <\/span><a href=\"https:\/\/localhost:3000\/\">https:\/\/localhost:3000<\/a><span>. You will see an empty dashboard of no tickets. That\u2019s because you haven\u2019t created one yet!<\/span><\/p>\n\n\n\n<p><span>Go ahead and message the WhatsApp number provided to you in your Vonage Messages API sandbox. The application will reply asking you to explain your support question. Once you do, the dashboard in your browser will populate with the new ticket. You can then open up the ticket by clicking <\/span><b>View<\/b><span> and begin interacting with it.<\/span><\/p>\n\n\n\n<p><a href=\"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2024\/10\/image6-2.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-large wp-image-16481\" src=\"https:\/\/www.couchbase.com\/wp-content\/uploads\/sites\/5\/2026\/05\/image6-2-1024x618-1.png\" alt=\"\" width=\"900\" height=\"543\"><\/a><\/p>\n\n\n\n<p><span>Once you begin to have tickets marked as resolved, future tickets will be populated with a <\/span><b>Suggested Solutions<\/b><span> section as you can see in the above screenshot. Each suggested solution will be ranked by its similarity to the current support question.\u00a0<\/span><\/p>\n\n\n\n<p><span>In this way, we have created an application utilizing AI that empowers team members in their work and doesn\u2019t seek to replace them in their jobs.<\/span><\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span>Wrapping up<\/span><\/h2>\n\n\n\n<p><span>By integrating Couchbase, Vonage, and OpenAI, we&#8217;ve built an application that empowers agents with quick access to relevant information, improving their efficiency and allowing them to focus on delivering excellent customer service. This project showcases how technology can be harnessed to support and elevate human roles in the workplace, leading to better outcomes for both employees and customers. Now that you&#8217;ve seen the potential, it&#8217;s time to apply these concepts to your own projects and continue innovating.<\/span><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Part 1 &#8211; <a class=\"row-title\" href=\"https:\/\/www.couchbase.com\/blog\/wp-admin\/post.php?post=16427&amp;action=edit\" aria-label=\"\u201cAI in Action: Enhancing and Not Replacing Jobs\u201d (Edit)\">AI in Action: Enhancing and Not Replacing Jobs<\/a><\/li>\n\n\n<li>Get started using Couchbase Capella today, <a href=\"https:\/\/cloud.couchbase.com\/sign-up?ref=blog\">sign up for free<\/a><\/li>\n\n<\/ul>\n\n\n\n<p><a href=\"https:\/\/cloud.couchbase.com\/sign-up?ref=blog\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-16409 size-large\" src=\"https:\/\/www.couchbase.com\/wp-content\/uploads\/sites\/5\/2026\/05\/capella-cloud-dbaas-couchbase-signup-free-1024x835-1.png\" alt=\"\" width=\"900\" height=\"734\"><\/a><\/p>\n\n\n\n<p><br><br><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Welcome back to the second part of our two-part series on building an interactive customer service support application that empowers support agents with the help of AI. The goal is to enhance their important work by leveraging previously resolved answers for current open questions quickly and intuitively using vector search. tl;dr In case you want [&hellip;]<\/p>\n","protected":false},"author":85356,"featured_media":4235,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"inline_featured_image":false,"_acf":"","footnotes":""},"categories":[598,136,301,727,55,353,715],"tags":[571,839,833,908,912],"ppma_author":[852],"class_list":["post-4238","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-artificial-intelligence-ai","category-best-practices-and-tutorials","category-cloud","category-generative-ai-genai","category-ruby","category-solutions","category-vector-search","tag-customer-service","tag-genai","tag-openai","tag-vonage","tag-whatsapp"],"yoast_head":"<!-- This site is optimized with the Yoast SEO Premium plugin v27.6 (Yoast SEO v27.6) - https:\/\/yoast.com\/product\/yoast-seo-premium-wordpress\/ -->\n<title>Part 2 - AI in Action: Enhancing and Not Replacing Jobs - The Couchbase Blog<\/title>\n<meta name=\"description\" content=\"Couchbase, Vonage, and OpenAI to build an AI-driven customer support app. Part 2 covers coding the business logic and connecting the services.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.couchbase.com\/blog\/es\/part2-ai-in-action-enhancing-and-not-replacing-jobs\/\" \/>\n<meta property=\"og:locale\" content=\"es_MX\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Part 2 - AI in Action: Enhancing and Not Replacing Jobs\" \/>\n<meta property=\"og:description\" content=\"Couchbase, Vonage, and OpenAI to build an AI-driven customer support app. Part 2 covers coding the business logic and connecting the services.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.couchbase.com\/blog\/es\/part2-ai-in-action-enhancing-and-not-replacing-jobs\/\" \/>\n<meta property=\"og:site_name\" content=\"The Couchbase Blog\" \/>\n<meta property=\"article:published_time\" content=\"2024-10-22T18:50:11+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/5\/2026\/05\/blog-vonage-openai-part2.png\" \/>\n\t<meta property=\"og:image:width\" content=\"2400\" \/>\n\t<meta property=\"og:image:height\" content=\"1256\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"author\" content=\"Ben Greenberg, Senior Developer Evangelist\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Ben Greenberg, Senior Developer Evangelist\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"8 minutos\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/part2-ai-in-action-enhancing-and-not-replacing-jobs\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/part2-ai-in-action-enhancing-and-not-replacing-jobs\\\/\"},\"author\":{\"name\":\"Ben Greenberg, Senior Developer Evangelist\",\"@id\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/#\\\/schema\\\/person\\\/48efa1524aec97312d92f65a270c255d\"},\"headline\":\"Part 2 &#8211; AI in Action: Enhancing and Not Replacing Jobs\",\"datePublished\":\"2024-10-22T18:50:11+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/part2-ai-in-action-enhancing-and-not-replacing-jobs\\\/\"},\"wordCount\":2642,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/#organization\"},\"image\":{\"@id\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/part2-ai-in-action-enhancing-and-not-replacing-jobs\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/wp-content\\\/uploads\\\/sites\\\/5\\\/2026\\\/05\\\/blog-vonage-openai-part2.png\",\"keywords\":[\"customer service\",\"GenAI\",\"openai\",\"vonage\",\"WhatsApp\"],\"articleSection\":[\"Artificial Intelligence (AI)\",\"Best Practices and Tutorials\",\"Couchbase Capella\",\"Generative AI (GenAI)\",\"Ruby\",\"Solutions\",\"Vector Search\"],\"inLanguage\":\"es\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/part2-ai-in-action-enhancing-and-not-replacing-jobs\\\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/part2-ai-in-action-enhancing-and-not-replacing-jobs\\\/\",\"url\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/part2-ai-in-action-enhancing-and-not-replacing-jobs\\\/\",\"name\":\"Part 2 - AI in Action: Enhancing and Not Replacing Jobs - The Couchbase Blog\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/part2-ai-in-action-enhancing-and-not-replacing-jobs\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/part2-ai-in-action-enhancing-and-not-replacing-jobs\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/wp-content\\\/uploads\\\/sites\\\/5\\\/2026\\\/05\\\/blog-vonage-openai-part2.png\",\"datePublished\":\"2024-10-22T18:50:11+00:00\",\"description\":\"Couchbase, Vonage, and OpenAI to build an AI-driven customer support app. Part 2 covers coding the business logic and connecting the services.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/part2-ai-in-action-enhancing-and-not-replacing-jobs\\\/#breadcrumb\"},\"inLanguage\":\"es\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/part2-ai-in-action-enhancing-and-not-replacing-jobs\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"es\",\"@id\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/part2-ai-in-action-enhancing-and-not-replacing-jobs\\\/#primaryimage\",\"url\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/wp-content\\\/uploads\\\/sites\\\/5\\\/2026\\\/05\\\/blog-vonage-openai-part2.png\",\"contentUrl\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/wp-content\\\/uploads\\\/sites\\\/5\\\/2026\\\/05\\\/blog-vonage-openai-part2.png\",\"width\":2400,\"height\":1256},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/part2-ai-in-action-enhancing-and-not-replacing-jobs\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Part 2 &#8211; AI in Action: Enhancing and Not Replacing Jobs\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/#website\",\"url\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/\",\"name\":\"The Couchbase Blog\",\"description\":\"Couchbase, the NoSQL Database\",\"publisher\":{\"@id\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"es\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/#organization\",\"name\":\"The Couchbase Blog\",\"url\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"es\",\"@id\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/wp-content\\\/uploads\\\/sites\\\/5\\\/2026\\\/06\\\/logo.svg\",\"contentUrl\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/wp-content\\\/uploads\\\/sites\\\/5\\\/2026\\\/06\\\/logo.svg\",\"width\":\"1024\",\"height\":\"1024\",\"caption\":\"The Couchbase Blog\"},\"image\":{\"@id\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/#\\\/schema\\\/logo\\\/image\\\/\"}},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/#\\\/schema\\\/person\\\/48efa1524aec97312d92f65a270c255d\",\"name\":\"Ben Greenberg, Senior Developer Evangelist\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"es\",\"@id\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/365b64ca9dda70fb57c32645f0ae9d61350022d36a9b7d264e2668ad1741e0d3?s=96&d=mm&r=g8058b8c8566f325c9af22e5182088ccb\",\"url\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/365b64ca9dda70fb57c32645f0ae9d61350022d36a9b7d264e2668ad1741e0d3?s=96&d=mm&r=g\",\"contentUrl\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/365b64ca9dda70fb57c32645f0ae9d61350022d36a9b7d264e2668ad1741e0d3?s=96&d=mm&r=g\",\"caption\":\"Ben Greenberg, Senior Developer Evangelist\"},\"url\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/es\\\/author\\\/bengreenberg\\\/\"}]}<\/script>\n<!-- \/ Yoast SEO Premium plugin. -->","yoast_head_json":{"title":"Part 2 - AI in Action: Enhancing and Not Replacing Jobs - The Couchbase Blog","description":"Couchbase, Vonage, and OpenAI to build an AI-driven customer support app. Part 2 covers coding the business logic and connecting the services.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.couchbase.com\/blog\/es\/part2-ai-in-action-enhancing-and-not-replacing-jobs\/","og_locale":"es_MX","og_type":"article","og_title":"Part 2 - AI in Action: Enhancing and Not Replacing Jobs","og_description":"Couchbase, Vonage, and OpenAI to build an AI-driven customer support app. Part 2 covers coding the business logic and connecting the services.","og_url":"https:\/\/www.couchbase.com\/blog\/es\/part2-ai-in-action-enhancing-and-not-replacing-jobs\/","og_site_name":"The Couchbase Blog","article_published_time":"2024-10-22T18:50:11+00:00","og_image":[{"width":2400,"height":1256,"url":"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/5\/2026\/05\/blog-vonage-openai-part2.png","type":"image\/png"}],"author":"Ben Greenberg, Senior Developer Evangelist","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Ben Greenberg, Senior Developer Evangelist","Est. reading time":"8 minutos"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/www.couchbase.com\/blog\/part2-ai-in-action-enhancing-and-not-replacing-jobs\/#article","isPartOf":{"@id":"https:\/\/www.couchbase.com\/blog\/part2-ai-in-action-enhancing-and-not-replacing-jobs\/"},"author":{"name":"Ben Greenberg, Senior Developer Evangelist","@id":"https:\/\/www.couchbase.com\/blog\/#\/schema\/person\/48efa1524aec97312d92f65a270c255d"},"headline":"Part 2 &#8211; AI in Action: Enhancing and Not Replacing Jobs","datePublished":"2024-10-22T18:50:11+00:00","mainEntityOfPage":{"@id":"https:\/\/www.couchbase.com\/blog\/part2-ai-in-action-enhancing-and-not-replacing-jobs\/"},"wordCount":2642,"commentCount":0,"publisher":{"@id":"https:\/\/www.couchbase.com\/blog\/#organization"},"image":{"@id":"https:\/\/www.couchbase.com\/blog\/part2-ai-in-action-enhancing-and-not-replacing-jobs\/#primaryimage"},"thumbnailUrl":"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/5\/2026\/05\/blog-vonage-openai-part2.png","keywords":["customer service","GenAI","openai","vonage","WhatsApp"],"articleSection":["Artificial Intelligence (AI)","Best Practices and Tutorials","Couchbase Capella","Generative AI (GenAI)","Ruby","Solutions","Vector Search"],"inLanguage":"es","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/www.couchbase.com\/blog\/part2-ai-in-action-enhancing-and-not-replacing-jobs\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/www.couchbase.com\/blog\/part2-ai-in-action-enhancing-and-not-replacing-jobs\/","url":"https:\/\/www.couchbase.com\/blog\/part2-ai-in-action-enhancing-and-not-replacing-jobs\/","name":"Part 2 - AI in Action: Enhancing and Not Replacing Jobs - The Couchbase Blog","isPartOf":{"@id":"https:\/\/www.couchbase.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/www.couchbase.com\/blog\/part2-ai-in-action-enhancing-and-not-replacing-jobs\/#primaryimage"},"image":{"@id":"https:\/\/www.couchbase.com\/blog\/part2-ai-in-action-enhancing-and-not-replacing-jobs\/#primaryimage"},"thumbnailUrl":"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/5\/2026\/05\/blog-vonage-openai-part2.png","datePublished":"2024-10-22T18:50:11+00:00","description":"Couchbase, Vonage, and OpenAI to build an AI-driven customer support app. Part 2 covers coding the business logic and connecting the services.","breadcrumb":{"@id":"https:\/\/www.couchbase.com\/blog\/part2-ai-in-action-enhancing-and-not-replacing-jobs\/#breadcrumb"},"inLanguage":"es","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.couchbase.com\/blog\/part2-ai-in-action-enhancing-and-not-replacing-jobs\/"]}]},{"@type":"ImageObject","inLanguage":"es","@id":"https:\/\/www.couchbase.com\/blog\/part2-ai-in-action-enhancing-and-not-replacing-jobs\/#primaryimage","url":"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/5\/2026\/05\/blog-vonage-openai-part2.png","contentUrl":"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/5\/2026\/05\/blog-vonage-openai-part2.png","width":2400,"height":1256},{"@type":"BreadcrumbList","@id":"https:\/\/www.couchbase.com\/blog\/part2-ai-in-action-enhancing-and-not-replacing-jobs\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/www.couchbase.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Part 2 &#8211; AI in Action: Enhancing and Not Replacing Jobs"}]},{"@type":"WebSite","@id":"https:\/\/www.couchbase.com\/blog\/#website","url":"https:\/\/www.couchbase.com\/blog\/","name":"The Couchbase Blog","description":"Couchbase, the NoSQL Database","publisher":{"@id":"https:\/\/www.couchbase.com\/blog\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.couchbase.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"es"},{"@type":"Organization","@id":"https:\/\/www.couchbase.com\/blog\/#organization","name":"The Couchbase Blog","url":"https:\/\/www.couchbase.com\/blog\/","logo":{"@type":"ImageObject","inLanguage":"es","@id":"https:\/\/www.couchbase.com\/blog\/#\/schema\/logo\/image\/","url":"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/5\/2026\/06\/logo.svg","contentUrl":"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/5\/2026\/06\/logo.svg","width":"1024","height":"1024","caption":"The Couchbase Blog"},"image":{"@id":"https:\/\/www.couchbase.com\/blog\/#\/schema\/logo\/image\/"}},{"@type":"Person","@id":"https:\/\/www.couchbase.com\/blog\/#\/schema\/person\/48efa1524aec97312d92f65a270c255d","name":"Ben Greenberg, Senior Developer Evangelist","image":{"@type":"ImageObject","inLanguage":"es","@id":"https:\/\/secure.gravatar.com\/avatar\/365b64ca9dda70fb57c32645f0ae9d61350022d36a9b7d264e2668ad1741e0d3?s=96&d=mm&r=g8058b8c8566f325c9af22e5182088ccb","url":"https:\/\/secure.gravatar.com\/avatar\/365b64ca9dda70fb57c32645f0ae9d61350022d36a9b7d264e2668ad1741e0d3?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/365b64ca9dda70fb57c32645f0ae9d61350022d36a9b7d264e2668ad1741e0d3?s=96&d=mm&r=g","caption":"Ben Greenberg, Senior Developer Evangelist"},"url":"https:\/\/www.couchbase.com\/blog\/es\/author\/bengreenberg\/"}]}},"acf":[],"authors":[{"term_id":852,"user_id":85356,"is_guest":0,"slug":"bengreenberg","display_name":"Ben Greenberg, Senior Developer Evangelist","avatar_url":{"url":"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/5\/2026\/05\/T024FJS4M-U075H3NTJUR-b4c321d902e2-512-23.jpeg","url2x":"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/5\/2026\/05\/T024FJS4M-U075H3NTJUR-b4c321d902e2-512-23.jpeg"},"author_category":"","first_name":"Ben","last_name":"Greenberg, Senior Developer Evangelist","user_url":"","job_title":"","description":""}],"_links":{"self":[{"href":"https:\/\/www.couchbase.com\/blog\/es\/wp-json\/wp\/v2\/posts\/4238","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.couchbase.com\/blog\/es\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.couchbase.com\/blog\/es\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.couchbase.com\/blog\/es\/wp-json\/wp\/v2\/users\/85356"}],"replies":[{"embeddable":true,"href":"https:\/\/www.couchbase.com\/blog\/es\/wp-json\/wp\/v2\/comments?post=4238"}],"version-history":[{"count":0,"href":"https:\/\/www.couchbase.com\/blog\/es\/wp-json\/wp\/v2\/posts\/4238\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.couchbase.com\/blog\/es\/wp-json\/wp\/v2\/media\/4235"}],"wp:attachment":[{"href":"https:\/\/www.couchbase.com\/blog\/es\/wp-json\/wp\/v2\/media?parent=4238"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.couchbase.com\/blog\/es\/wp-json\/wp\/v2\/categories?post=4238"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.couchbase.com\/blog\/es\/wp-json\/wp\/v2\/tags?post=4238"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.couchbase.com\/blog\/es\/wp-json\/wp\/v2\/ppma_author?post=4238"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}