{"id":16480,"date":"2024-10-22T11:50:11","date_gmt":"2024-10-22T18:50:11","guid":{"rendered":"https:\/\/www.couchbase.com\/blog\/?p=16480"},"modified":"2025-06-13T16:36:37","modified_gmt":"2025-06-13T23:36:37","slug":"part2-ai-in-action-enhancing-and-not-replacing-jobs","status":"publish","type":"post","link":"https:\/\/www.couchbase.com\/blog\/part2-ai-in-action-enhancing-and-not-replacing-jobs\/","title":{"rendered":"Part 2 &#8211; AI in Action: Enhancing and Not Replacing Jobs"},"content":{"rendered":"<p><span style=\"font-weight: 400;\">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<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<p><span style=\"font-weight: 400;\"><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<ul>\n<li style=\"list-style-type: none;\">\n<ul>\n<li style=\"font-weight: 400;\" aria-level=\"1\"><span style=\"font-weight: 400;\">Couchbase Capella<\/span><\/li>\n<li style=\"font-weight: 400;\" aria-level=\"1\"><span style=\"font-weight: 400;\">Vonage Messages API<\/span><\/li>\n<li style=\"font-weight: 400;\" aria-level=\"1\"><span style=\"font-weight: 400;\">OpenAI Embeddings API<\/span><\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<blockquote class=\"wp-embedded-content\" data-secret=\"FgkBWZlmbu\"><p><a href=\"https:\/\/www.couchbase.com\/blog\/ai-in-action-enhancing-and-not-replacing-jobs\/\">AI in Action: Enhancing and Not Replacing Jobs<\/a><\/p><\/blockquote>\n<p><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=sCe5GHoTyn#?secret=FgkBWZlmbu\" data-secret=\"FgkBWZlmbu\" width=\"600\" height=\"338\" frameborder=\"0\" marginwidth=\"0\" marginheight=\"0\" scrolling=\"no\"><\/iframe><\/p>\n<p><span style=\"font-weight: 400;\">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<p><span style=\"font-weight: 400;\">First, let\u2019s go ahead and define the business logic of our application.<\/span><\/p>\n<h2><span style=\"font-weight: 400;\">Defining application business logic<\/span><\/h2>\n<p><span style=\"font-weight: 400;\">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 style=\"font-weight: 400;\">GitHub<\/span><\/a><span style=\"font-weight: 400;\">.<\/span><\/p>\n<p><span style=\"font-weight: 400;\">In short, we will be defining our logic in model and controller files.\u00a0<\/span><\/p>\n<p><span style=\"font-weight: 400;\">The application will have three models:<\/span><\/p>\n<ul>\n<li style=\"list-style-type: none;\">\n<ul>\n<li aria-level=\"1\"><b>Ticket<\/b><\/li>\n<li aria-level=\"1\"><b>User<\/b><\/li>\n<li aria-level=\"1\"><b>Agent<\/b><\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p><span style=\"font-weight: 400;\">The application will also have two controllers:<\/span><\/p>\n<ul>\n<li style=\"list-style-type: none;\">\n<ul>\n<li aria-level=\"1\"><b>Messages Controller<\/b><\/li>\n<li aria-level=\"1\"><b>Dashboard Controller<\/b><\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p><span style=\"font-weight: 400;\">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 style=\"font-weight: 400;\">, and the routes for the application in <\/span><b><code>config\/routes.rb<\/code><\/b><span style=\"font-weight: 400;\">. All of those can be viewed on GitHub and copied directly from there to your own codebase.<\/span><\/p>\n<p><span style=\"font-weight: 400;\">Let\u2019s start with the models.<\/span><\/p>\n<h3><span style=\"font-weight: 400;\">Create models<\/span><\/h3>\n<p><span style=\"font-weight: 400;\">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 style=\"font-weight: 400;\"><code>CouchbaseOrm::Base<\/code><\/span><\/i><span style=\"font-weight: 400;\"> to make the ORM\u2019s methods available to it.<\/span><\/p>\n<h4><span style=\"font-weight: 400;\">The Ticket model<\/span><\/h4>\n<p><span style=\"font-weight: 400;\">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<pre class=\"nums:false lang:default decode:true\">OPEN = 'open'\r\nRESOLVED = 'resolved'\r\n\r\nbelongs_to :user\r\nbelongs_to :agent, optional: true\r\n\r\nattribute :query, :string\r\nattribute :status, :string, default: OPEN\r\nattribute :summary, :string\r\nattribute :embedding, :array, type: :float, default: []\r\nattribute :created_at, :datetime, default: -&gt; { Time.now }\r\nattribute :updated_at, :datetime, default: -&gt; { Time.now }<\/pre>\n<p><span style=\"font-weight: 400;\">As you can see above, we also define two constants, <\/span><b>OPEN<\/b><span style=\"font-weight: 400;\"> and <\/span><b>RESOLVED, <\/b><span style=\"font-weight: 400;\">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<p><span style=\"font-weight: 400;\">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<pre class=\"nums:false lang:default decode:true\">  def self.open_tickets\r\n\u00a0\u00a0\u00a0\u00a0where(status: OPEN).to_a\r\n\u00a0\u00a0end\r\n\r\n\u00a0\u00a0def self.resolved_tickets\r\n\u00a0\u00a0\u00a0\u00a0where(status: RESOLVED).to_a\r\n\u00a0\u00a0end\r\n\r\n\u00a0\u00a0def open?\r\n\u00a0\u00a0\u00a0\u00a0status == OPEN\r\n\u00a0\u00a0end\r\n\r\n\u00a0\u00a0def resolved?\r\n\u00a0\u00a0\u00a0\u00a0status == RESOLVED\r\n\u00a0\u00a0end\r\n\r\n\u00a0\u00a0def mark_as_resolved!\r\n\u00a0\u00a0\u00a0\u00a0update!(status: RESOLVED)\r\n\u00a0\u00a0end<\/pre>\n<p><span style=\"font-weight: 400;\">All together, the Ticket model will look like the following, including additional validations:<\/span><\/p>\n<pre class=\"nums:false lang:default decode:true\">class Ticket &lt; CouchbaseOrm::Base\r\n\u00a0\u00a0OPEN = 'open'\r\n\u00a0\u00a0RESOLVED = 'resolved'\r\n\r\n\u00a0\u00a0belongs_to :user\r\n\u00a0\u00a0belongs_to :agent, optional: true\r\n\r\n\u00a0\u00a0attribute :query, :string\r\n\u00a0\u00a0attribute :status, :string, default: OPEN\r\n\u00a0\u00a0attribute :summary, :string\r\n\u00a0\u00a0attribute :embedding, :array, type: :float, default: []\r\n\u00a0\u00a0attribute :created_at, :datetime, default: -&gt; { Time.now }\r\n\u00a0\u00a0attribute :updated_at, :datetime, default: -&gt; { Time.now }\r\n\r\n\u00a0\u00a0ensure_design_document!\r\n\r\n\u00a0\u00a0validates :query, presence: true\r\n\r\n\u00a0\u00a0before_save :set_timestamps\r\n\u00a0\u00a0def self.open_tickets\r\n\u00a0\u00a0\u00a0\u00a0where(status: OPEN).to_a\r\n\u00a0\u00a0end\r\n\r\n\u00a0\u00a0def self.resolved_tickets\r\n\u00a0\u00a0\u00a0\u00a0where(status: RESOLVED).to_a\r\n\u00a0\u00a0end\r\n\r\n\u00a0\u00a0def open?\r\n\u00a0\u00a0\u00a0\u00a0status == OPEN\r\n\u00a0\u00a0end\r\n\r\n\u00a0\u00a0def resolved?\r\n\u00a0\u00a0\u00a0\u00a0status == RESOLVED\r\n\u00a0\u00a0end\r\n\r\n\u00a0\u00a0def mark_as_resolved!\r\n\u00a0\u00a0\u00a0\u00a0update!(status: RESOLVED)\r\n\u00a0\u00a0end\r\n\r\n\u00a0\u00a0private\r\n\r\n\u00a0\u00a0def set_timestamps\r\n\u00a0\u00a0\u00a0\u00a0self.updated_at = Time.now\r\n\u00a0\u00a0end\r\nend<\/pre>\n<p><span style=\"font-weight: 400;\">Let\u2019s go down a similar road for the User model.<\/span><\/p>\n<h4><span style=\"font-weight: 400;\">The User model<\/span><\/h4>\n<p><span style=\"font-weight: 400;\">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<p><span style=\"font-weight: 400;\">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<pre class=\"nums:false lang:default decode:true\">class User &lt; CouchbaseOrm::Base\r\n\u00a0\u00a0has_many :tickets\r\n\r\n\u00a0\u00a0attribute :whatsapp_number, :string\r\n\u00a0\u00a0attribute :name, :string\r\n\u00a0\u00a0attribute :created_at, :datetime, default: -&gt; { Time.now }\r\n\u00a0\u00a0attribute :updated_at, :datetime, default: -&gt; { Time.now }\r\n\r\n\u00a0\u00a0validates :whatsapp_number, presence: true\r\n\u00a0\u00a0validates :name, presence: true\r\n\r\n\u00a0\u00a0before_save :set_timestamps\r\n\r\n\u00a0\u00a0private\r\n\r\n\u00a0\u00a0def set_timestamps\r\n\u00a0\u00a0\u00a0\u00a0self.updated_at = Time.now\r\n\u00a0\u00a0end\r\n\r\n\u00a0\u00a0def self.find_or_create_user_by_whatsapp_number(whatsapp_number)\r\n\u00a0\u00a0\u00a0\u00a0user = User.find_by(whatsapp_number: whatsapp_number)\r\n\u00a0\u00a0\u00a0\u00a0unless user\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0user = User.create!(whatsapp_number: whatsapp_number, name: \"User #{whatsapp_number[-4..-1]}\")\r\n\u00a0\u00a0\u00a0\u00a0end\r\n\u00a0\u00a0\u00a0\u00a0user\r\n\u00a0\u00a0end\r\nend<\/pre>\n<p><span style=\"font-weight: 400;\">The last model we will create is the Agent model, let\u2019s do it.<\/span><\/p>\n<h4><span style=\"font-weight: 400;\">The Agent Model<\/span><\/h4>\n<p><span style=\"font-weight: 400;\">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<pre class=\"nums:false lang:default decode:true\">require 'couchbase-orm'\r\n\r\nclass Agent &lt; CouchbaseOrm::Base\r\n\u00a0\u00a0has_many :tickets\r\n\r\n\u00a0\u00a0attribute :name, :string\r\n\u00a0\u00a0attribute :email, :string\r\n\u00a0\u00a0attribute :created_at, :datetime, default: -&gt; { Time.now }\r\n\u00a0\u00a0attribute :updated_at, :datetime, default: -&gt; { Time.now }\r\n\r\n\u00a0\u00a0validates :email, presence: true, uniqueness: true\r\n\u00a0\u00a0validates :name, presence: true\r\n\r\n\u00a0\u00a0before_save :set_timestamps\r\n\r\n\u00a0\u00a0private\r\n\r\n\u00a0\u00a0def set_timestamps\r\n\u00a0\u00a0\u00a0\u00a0self.updated_at = Time.now\r\n\u00a0\u00a0end\r\nend<\/pre>\n<p><span style=\"font-weight: 400;\">At this point, we have created our models. It\u2019s not time to define the controllers.<\/span><\/p>\n<h3><span style=\"font-weight: 400;\">Create controllers<\/span><\/h3>\n<p><span style=\"font-weight: 400;\">The application will have two controllers that define what happens inside each route of the site. Namely, a <\/span><i><span style=\"font-weight: 400;\"><code>dashboard_controller<\/code><\/span><\/i><span style=\"font-weight: 400;\"> that oversees the functionality of the Dashboard, and a <\/span><i><span style=\"font-weight: 400;\"><code>messages_controller<\/code><\/span><\/i> <span style=\"font-weight: 400;\">that oversees the messaging functionality.<\/span><\/p>\n<p><span style=\"font-weight: 400;\">We\u2019ll start with the Dashboard first.<\/span><\/p>\n<h4><span style=\"font-weight: 400;\">The Dashboard controller<\/span><\/h4>\n<p><span style=\"font-weight: 400;\">The <\/span><b>index<\/b><span style=\"font-weight: 400;\"> and <\/span><b>show<\/b><span style=\"font-weight: 400;\"> 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<p><span style=\"font-weight: 400;\">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 style=\"font-weight: 400;\"> defining the arguments inside the instantiation of a new <\/span><b><code>VectorSearch<\/code> <\/b><span style=\"font-weight: 400;\">object like such:<\/span><\/p>\n<pre class=\"nums:false lang:default decode:true\">  \u00a0 request = Couchbase::SearchRequest.new(\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Couchbase::VectorSearch.new(\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0[\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Couchbase::VectorQuery.new('embedding', embedding) do |q|\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0q.num_candidates = 2\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0q.boost = 0.3\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0end\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0],\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Couchbase::Options::VectorSearch.new(vector_query_combination: :and)\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0)\r\n\u00a0\u00a0\u00a0\u00a0)<\/pre>\n<p><span style=\"font-weight: 400;\">First, the <\/span><b><code>VectorSearch<\/code><\/b><span style=\"font-weight: 400;\"> is wrapped inside a new instance of <\/span><b><code>Couchbase::SearchRequest<\/code><\/b><span style=\"font-weight: 400;\">, as the <\/span><b><code>VectorSearch<\/code><\/b><span style=\"font-weight: 400;\"> is a type of a search request. Then a new <\/span><b><code>Couchbase::VectorQuery<\/code><\/b><span style=\"font-weight: 400;\"> instance is passed to the <\/span><b><code>VectorSearch<\/code> <\/b><span style=\"font-weight: 400;\">object specifying the field to search in (i.e., <\/span><i><span style=\"font-weight: 400;\">embedding<\/span><\/i><span style=\"font-weight: 400;\">) and the customer query converted into its own embedding as the second argument.<\/span><\/p>\n<p><span style=\"font-weight: 400;\">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<pre class=\"nums:false lang:default decode:true\">class DashboardController &lt; ApplicationController\r\n\u00a0\u00a0def index\r\n\u00a0\u00a0\u00a0\u00a0@open_tickets = Ticket.where(status: 'open').to_a.sort_by { |ticket| ticket.created_at }.reverse\r\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\r\n\u00a0\u00a0end\r\n\r\n\u00a0\u00a0def show\r\n\u00a0\u00a0\u00a0\u00a0@ticket = find_ticket\r\n\u00a0\u00a0\u00a0\u00a0@user = find_user\r\n\u00a0\u00a0\u00a0\u00a0@suggestions = find_suggestions\r\n\u00a0\u00a0end\r\n\r\n\u00a0\u00a0private\r\n\u00a0\u00a0def find_ticket\r\n\u00a0\u00a0\u00a0\u00a0Ticket.find(params[:id])\r\n\u00a0\u00a0end\r\n\r\n\u00a0\u00a0def find_user\r\n\u00a0\u00a0\u00a0\u00a0find_ticket.user\r\n\u00a0\u00a0end\r\n\r\n\u00a0\u00a0def find_suggestions\r\n\u00a0\u00a0\u00a0\u00a0search_similar_tickets(find_ticket.query)\r\n\u00a0\u00a0end\r\n\r\n\u00a0\u00a0def search_similar_tickets(query)\r\n\u00a0\u00a0\u00a0\u00a0embedding = OPENAI_CLIENT.embeddings(\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0parameters: {\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0model: \"text-embedding-ada-002\",\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0input: query\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0}\r\n\u00a0\u00a0\u00a0\u00a0)['data'][0]['embedding']\r\n\r\n\u00a0\u00a0\u00a0\u00a0cluster = Couchbase::Cluster.connect(\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0ENV['COUCHBASE_CONNECTION_STRING'],\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0ENV['COUCHBASE_USERNAME'],\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0ENV['COUCHBASE_PASSWORD']\r\n\u00a0\u00a0\u00a0\u00a0)\r\n\r\n\u00a0\u00a0\u00a0\u00a0bucket = cluster.bucket(ENV['COUCHBASE_BUCKET'])\r\n\r\n\u00a0\u00a0\u00a0\u00a0scope = bucket.scope('_default')\r\n\r\n\u00a0\u00a0\u00a0\u00a0request = Couchbase::SearchRequest.new(\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Couchbase::VectorSearch.new(\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0[\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Couchbase::VectorQuery.new('embedding', embedding) do |q|\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0q.num_candidates = 2\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0q.boost = 0.3\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0end\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0],\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Couchbase::Options::VectorSearch.new(vector_query_combination: :and)\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0)\r\n\u00a0\u00a0\u00a0\u00a0)\r\n\r\n\u00a0\u00a0\u00a0\u00a0result = scope.search('whatsapp_support_index', request)\r\n\r\n\u00a0\u00a0\u00a0\u00a0result.rows.map do |row|\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0document = bucket.default_collection.get(row.id)\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0{\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0id: row.id,\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0score: row.score,\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0summary: document.content['summary']\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0}\r\n\u00a0\u00a0\u00a0\u00a0end\r\n\u00a0\u00a0end\r\nend<\/pre>\n<p><span style=\"font-weight: 400;\">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<p><span style=\"font-weight: 400;\">The frontend code can be found on <\/span><a href=\"https:\/\/github.com\/hummusonrails\/whatsapp_support_app\/tree\/main\/app\/views\/dashboard\"><span style=\"font-weight: 400;\">GitHub<\/span><\/a><span style=\"font-weight: 400;\"> for both of these views and can be copied directly into your codebase or edited for your own specific needs.<\/span><\/p>\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\/blog\/wp-content\/uploads\/sites\/1\/2024\/10\/Screenshot-2024-10-22-at-12.33.05\u202fPM-1024x652.png\" alt=\"\" width=\"900\" height=\"573\" srcset=\"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2024\/10\/Screenshot-2024-10-22-at-12.33.05\u202fPM-1024x652.png 1024w, https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2024\/10\/Screenshot-2024-10-22-at-12.33.05\u202fPM-300x191.png 300w, https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2024\/10\/Screenshot-2024-10-22-at-12.33.05\u202fPM-768x489.png 768w, https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2024\/10\/Screenshot-2024-10-22-at-12.33.05\u202fPM-1536x978.png 1536w, https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2024\/10\/Screenshot-2024-10-22-at-12.33.05\u202fPM-2048x1304.png 2048w, https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2024\/10\/Screenshot-2024-10-22-at-12.33.05\u202fPM-1320x841.png 1320w\" sizes=\"auto, (max-width: 900px) 100vw, 900px\" \/><\/a><\/p>\n<h4><span style=\"font-weight: 400;\">The Messages Controller<\/span><\/h4>\n<p><span style=\"font-weight: 400;\">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<p><span style=\"font-weight: 400;\">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<p><span style=\"font-weight: 400;\">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 style=\"font-weight: 400;\">ActionCable<\/span><\/a><span style=\"font-weight: 400;\">, 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<p><span style=\"font-weight: 400;\">As mentioned in the <\/span><a href=\"https:\/\/www.couchbase.com\/blog\/ai-in-action-enhancing-and-not-replacing-jobs\/\"><em><span style=\"font-weight: 400;\">Using the Vonage Messages API <\/span><\/em><\/a><span style=\"font-weight: 400;\">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<p><span style=\"font-weight: 400;\">Here is the code for the controller:<\/span><\/p>\n<pre class=\"nums:false lang:default decode:true\">class MessagesController &lt; ApplicationController\r\n\u00a0\u00a0skip_before_action :verify_authenticity_token\r\n\r\n\u00a0\u00a0def inbound\r\n\u00a0\u00a0\u00a0\u00a0whatsapp_number = params[:from]\r\n\u00a0\u00a0\u00a0\u00a0text = params[:text]\r\n\u00a0\u00a0\u00a0\u00a0@reply = { sender: 'User', body: text, created_at: Time.current }\r\n\u00a0\u00a0\u00a0\u00a0@user = User.find_by(whatsapp_number: whatsapp_number)\r\n\r\n\u00a0\u00a0\u00a0\u00a0if @user\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0open_ticket = Ticket.find_by(user_id: @user.id, status: Ticket::OPEN)\r\n\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0if open_ticket.nil?\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Ticket.create!(user_id: @user.id, query: \"Awaiting query\", status: Ticket::OPEN)\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0send_message(to: whatsapp_number, text: \"Thank you for your message. Please describe your support query.\")\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0elsif open_ticket.query == \"Awaiting query\"\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0open_ticket.update!(query: text)\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0ActionCable.server.broadcast \"messages_#{@user.id}\", { reply: @reply, ticket: open_ticket }\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0else\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0ActionCable.server.broadcast \"messages_#{@user.id}\", { reply: @reply, ticket: open_ticket }\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0end\r\n\u00a0\u00a0\u00a0\u00a0else\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0@user = User.create!(whatsapp_number: whatsapp_number, name: \"User #{whatsapp_number[-4..-1]}\")\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0send_message(to: whatsapp_number, text: \"Thank you for your message. Please describe your support query.\")\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Ticket.create!(user_id: @user.id, query: \"Awaiting query\", status: Ticket::OPEN)\r\n\u00a0\u00a0\u00a0\u00a0end\r\n\r\n\u00a0\u00a0\u00a0\u00a0respond_to do |format|\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0format.turbo_stream { render turbo_stream: turbo_stream.append(\"messages_#{@user.id}\", partial: \"messages\/reply\", locals: { reply: @reply }) }\r\n\u00a0\u00a0\u00a0\u00a0end\r\n\u00a0\u00a0end\r\n\r\n\u00a0\u00a0def reply\r\n\u00a0\u00a0\u00a0\u00a0@ticket_id = params[:ticket_id]\r\n\u00a0\u00a0\u00a0\u00a0@message = params[:message]\r\n\u00a0\u00a0\u00a0\u00a0@resolved = params[:mark_as_resolved] == \"1\"\r\n\r\n\u00a0\u00a0\u00a0\u00a0@ticket = Ticket.find(@ticket_id)\r\n\u00a0\u00a0\u00a0\u00a0@user = @ticket.user\r\n\r\n\u00a0\u00a0\u00a0\u00a0@reply = { sender: 'Agent', body: @message, created_at: Time.current }\r\n\r\n\u00a0\u00a0\u00a0\u00a0ActionCable.server.broadcast \"messages_#{@user.id}\", { reply: @reply, ticket: @ticket }\r\n\r\n\u00a0\u00a0\u00a0\u00a0if @resolved\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0embedding = OPENAI_CLIENT.embeddings(\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0parameters: {\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0model: \"text-embedding-ada-002\",\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0input: @message\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0}\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0)['data'][0]['embedding']\r\n\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0@ticket.update!(summary: @message, status: Ticket::RESOLVED, embedding: embedding)\r\n\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0send_message(\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0to: @user.whatsapp_number,\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0text: @message\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0)\r\n\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0respond_to do |format|\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0format.html { redirect_to request.referrer, notice: \"Ticket marked as resolved.\" }\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0end\r\n\u00a0\u00a0\u00a0\u00a0else\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0send_message(\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0to: @user.whatsapp_number,\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0text: @message\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0)\r\n\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0respond_to do |format|\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0format.turbo_stream { render turbo_stream: turbo_stream.append(\"messages_#{@user.id}\", partial: \"messages\/reply\", locals: { reply: @reply }) }\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0end\r\n\u00a0\u00a0\u00a0\u00a0end\r\n\u00a0\u00a0end\r\n\r\n\u00a0\u00a0def status\r\n\u00a0\u00a0\u00a0\u00a0head :ok\r\n\u00a0\u00a0end\r\n\r\n\u00a0\u00a0private\r\n\r\n\u00a0\u00a0def send_message(to:, text:)\r\n\u00a0\u00a0\u00a0\u00a0require 'net\/http'\r\n\u00a0\u00a0\u00a0\u00a0require 'uri'\r\n\u00a0\u00a0\u00a0\u00a0require 'json'\r\n\r\n\u00a0\u00a0\u00a0\u00a0uri = URI.parse(\"https:\/\/messages-sandbox.nexmo.com\/v1\/messages\")\r\n\r\n\u00a0\u00a0\u00a0\u00a0request = Net::HTTP::Post.new(uri)\r\n\u00a0\u00a0\u00a0\u00a0request.basic_auth(ENV['VONAGE_API_KEY'], ENV['VONAGE_API_SECRET'])\r\n\u00a0\u00a0\u00a0\u00a0request.content_type = 'application\/json'\r\n\u00a0\u00a0\u00a0\u00a0request['Accept'] = 'application\/json'\r\n\r\n\u00a0\u00a0\u00a0\u00a0request.body = {\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0from: ENV['VONAGE_FROM_NUMBER'],\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0to: to,\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0message_type: 'text',\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0text: text,\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0channel: 'whatsapp'\r\n\u00a0\u00a0\u00a0\u00a0}.to_json\r\n\r\n\u00a0\u00a0\u00a0\u00a0http = Net::HTTP.new(uri.host, uri.port)\r\n\u00a0\u00a0\u00a0\u00a0http.use_ssl = true\r\n\r\n\u00a0\u00a0\u00a0\u00a0response = http.request(request)\r\n\r\n\u00a0\u00a0\u00a0\u00a0puts \"Response Code: #{response.code}\"\r\n\u00a0\u00a0\u00a0\u00a0puts \"Response Body: #{response.body}\"\r\n\u00a0\u00a0end\r\nend<\/pre>\n<p><span style=\"font-weight: 400;\">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<p><span style=\"font-weight: 400;\">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 style=\"font-weight: 400;\">GitHub<\/span><\/a><span style=\"font-weight: 400;\">.<\/span><\/p>\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\/blog\/wp-content\/uploads\/sites\/1\/2024\/10\/Screenshot-2024-10-22-at-12.39.14\u202fPM-1024x618.png\" alt=\"\" width=\"900\" height=\"543\" srcset=\"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2024\/10\/Screenshot-2024-10-22-at-12.39.14\u202fPM-1024x618.png 1024w, https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2024\/10\/Screenshot-2024-10-22-at-12.39.14\u202fPM-300x181.png 300w, https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2024\/10\/Screenshot-2024-10-22-at-12.39.14\u202fPM-768x464.png 768w, https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2024\/10\/Screenshot-2024-10-22-at-12.39.14\u202fPM-1536x927.png 1536w, https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2024\/10\/Screenshot-2024-10-22-at-12.39.14\u202fPM-2048x1236.png 2048w, https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2024\/10\/Screenshot-2024-10-22-at-12.39.14\u202fPM-1320x797.png 1320w\" sizes=\"auto, (max-width: 900px) 100vw, 900px\" \/><\/a><\/p>\n<p><span style=\"font-weight: 400;\">All that is left to do now is to run our application!\u00a0<\/span><\/p>\n<h2><span style=\"font-weight: 400;\">Running the application<\/span><\/h2>\n<p><span style=\"font-weight: 400;\">To use the application, go ahead and open a terminal window and run <\/span><i><span style=\"font-weight: 400;\"><code>bin\/dev<\/code><\/span><\/i><span style=\"font-weight: 400;\"> 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 style=\"font-weight: 400;\"><code>ngrok http 3000<\/code><\/span><\/i><span style=\"font-weight: 400;\">.\u00a0<\/span><\/p>\n<p><span style=\"font-weight: 400;\">Once you have done both commands, you can open in your browser <\/span><a href=\"https:\/\/localhost:3000\/\">https:\/\/localhost:3000<\/a><span style=\"font-weight: 400;\">. You will see an empty dashboard of no tickets. That\u2019s because you haven\u2019t created one yet!<\/span><\/p>\n<p><span style=\"font-weight: 400;\">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 style=\"font-weight: 400;\"> and begin interacting with it.<\/span><\/p>\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\" style=\"border: 1px black solid;\" src=\"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2024\/10\/image6-2-1024x618.png\" alt=\"\" width=\"900\" height=\"543\" srcset=\"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2024\/10\/image6-2-1024x618.png 1024w, https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2024\/10\/image6-2-300x181.png 300w, https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2024\/10\/image6-2-768x463.png 768w, https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2024\/10\/image6-2-1536x927.png 1536w, https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2024\/10\/image6-2-1320x796.png 1320w, https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2024\/10\/image6-2.png 1999w\" sizes=\"auto, (max-width: 900px) 100vw, 900px\" \/><\/a><\/p>\n<p><span style=\"font-weight: 400;\">Once you begin to have tickets marked as resolved, future tickets will be populated with a <\/span><b>Suggested Solutions<\/b><span style=\"font-weight: 400;\"> 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<p><span style=\"font-weight: 400;\">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<h2><span style=\"font-weight: 400;\">Wrapping up<\/span><\/h2>\n<p><span style=\"font-weight: 400;\">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<ul>\n<li style=\"list-style-type: none;\">\n<ul>\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<li>Get started using Couchbase Capella today, <a href=\"https:\/\/cloud.couchbase.com\/sign-up?ref=blog\">sign up for free<\/a><\/li>\n<\/ul>\n<\/li>\n<\/ul>\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\/blog\/wp-content\/uploads\/sites\/1\/2024\/10\/capella-cloud-dbaas-couchbase-signup-free-1024x835.png\" alt=\"\" width=\"900\" height=\"734\" srcset=\"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2024\/10\/capella-cloud-dbaas-couchbase-signup-free-1024x835.png 1024w, https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2024\/10\/capella-cloud-dbaas-couchbase-signup-free-300x245.png 300w, https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2024\/10\/capella-cloud-dbaas-couchbase-signup-free-768x626.png 768w, https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2024\/10\/capella-cloud-dbaas-couchbase-signup-free-1536x1252.png 1536w, https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2024\/10\/capella-cloud-dbaas-couchbase-signup-free-2048x1670.png 2048w, https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2024\/10\/capella-cloud-dbaas-couchbase-signup-free-1320x1076.png 1320w\" sizes=\"auto, (max-width: 900px) 100vw, 900px\" \/><\/a><\/p>\n<p><br style=\"font-weight: 400;\" \/><br style=\"font-weight: 400;\" \/><\/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 [&hellip;]<\/p>\n","protected":false},"author":85356,"featured_media":16484,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"inline_featured_image":false,"footnotes":""},"categories":[10122,1815,2225,9973,9407,2389,9937],"tags":[9560,9974,9964,10039,10044],"ppma_author":[9985],"class_list":["post-16480","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"],"acf":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO Premium plugin v25.8 (Yoast SEO v25.8) - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\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\/part2-ai-in-action-enhancing-and-not-replacing-jobs\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\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\/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=\"article:modified_time\" content=\"2025-06-13T23:36:37+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2024\/10\/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 minutes\" \/>\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\",\"dateModified\":\"2025-06-13T23:36:37+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/www.couchbase.com\/blog\/part2-ai-in-action-enhancing-and-not-replacing-jobs\/\"},\"wordCount\":1614,\"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\/1\/2024\/10\/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\":\"en-US\",\"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\/1\/2024\/10\/blog-vonage-openai-part2.png\",\"datePublished\":\"2024-10-22T18:50:11+00:00\",\"dateModified\":\"2025-06-13T23:36:37+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\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/www.couchbase.com\/blog\/part2-ai-in-action-enhancing-and-not-replacing-jobs\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@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\/1\/2024\/10\/blog-vonage-openai-part2.png\",\"contentUrl\":\"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2024\/10\/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\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\/\/www.couchbase.com\/blog\/#organization\",\"name\":\"The Couchbase Blog\",\"url\":\"https:\/\/www.couchbase.com\/blog\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/www.couchbase.com\/blog\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/2023\/04\/admin-logo.png\",\"contentUrl\":\"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/2023\/04\/admin-logo.png\",\"width\":218,\"height\":34,\"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\":\"en-US\",\"@id\":\"https:\/\/www.couchbase.com\/blog\/#\/schema\/person\/image\/c9bda12524045d12a5878a2ef3fbe0de\",\"url\":\"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2024\/06\/T024FJS4M-U075H3NTJUR-b4c321d902e2-512.jpeg\",\"contentUrl\":\"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2024\/06\/T024FJS4M-U075H3NTJUR-b4c321d902e2-512.jpeg\",\"caption\":\"Ben Greenberg, Senior Developer Evangelist\"},\"url\":\"https:\/\/www.couchbase.com\/blog\/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\/part2-ai-in-action-enhancing-and-not-replacing-jobs\/","og_locale":"en_US","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\/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","article_modified_time":"2025-06-13T23:36:37+00:00","og_image":[{"width":2400,"height":1256,"url":"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2024\/10\/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 minutes"},"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","dateModified":"2025-06-13T23:36:37+00:00","mainEntityOfPage":{"@id":"https:\/\/www.couchbase.com\/blog\/part2-ai-in-action-enhancing-and-not-replacing-jobs\/"},"wordCount":1614,"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\/1\/2024\/10\/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":"en-US","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\/1\/2024\/10\/blog-vonage-openai-part2.png","datePublished":"2024-10-22T18:50:11+00:00","dateModified":"2025-06-13T23:36:37+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":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.couchbase.com\/blog\/part2-ai-in-action-enhancing-and-not-replacing-jobs\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@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\/1\/2024\/10\/blog-vonage-openai-part2.png","contentUrl":"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2024\/10\/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":"en-US"},{"@type":"Organization","@id":"https:\/\/www.couchbase.com\/blog\/#organization","name":"The Couchbase Blog","url":"https:\/\/www.couchbase.com\/blog\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.couchbase.com\/blog\/#\/schema\/logo\/image\/","url":"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/2023\/04\/admin-logo.png","contentUrl":"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/2023\/04\/admin-logo.png","width":218,"height":34,"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":"en-US","@id":"https:\/\/www.couchbase.com\/blog\/#\/schema\/person\/image\/c9bda12524045d12a5878a2ef3fbe0de","url":"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2024\/06\/T024FJS4M-U075H3NTJUR-b4c321d902e2-512.jpeg","contentUrl":"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2024\/06\/T024FJS4M-U075H3NTJUR-b4c321d902e2-512.jpeg","caption":"Ben Greenberg, Senior Developer Evangelist"},"url":"https:\/\/www.couchbase.com\/blog\/author\/bengreenberg\/"}]}},"authors":[{"term_id":9985,"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\/1\/2024\/06\/T024FJS4M-U075H3NTJUR-b4c321d902e2-512.jpeg","url2x":"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2024\/06\/T024FJS4M-U075H3NTJUR-b4c321d902e2-512.jpeg"},"author_category":"","last_name":"Greenberg, Senior Developer Evangelist","first_name":"Ben","job_title":"Senior Developer Evangelist","user_url":"","description":""}],"_links":{"self":[{"href":"https:\/\/www.couchbase.com\/blog\/wp-json\/wp\/v2\/posts\/16480","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.couchbase.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.couchbase.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.couchbase.com\/blog\/wp-json\/wp\/v2\/users\/85356"}],"replies":[{"embeddable":true,"href":"https:\/\/www.couchbase.com\/blog\/wp-json\/wp\/v2\/comments?post=16480"}],"version-history":[{"count":0,"href":"https:\/\/www.couchbase.com\/blog\/wp-json\/wp\/v2\/posts\/16480\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.couchbase.com\/blog\/wp-json\/wp\/v2\/media\/16484"}],"wp:attachment":[{"href":"https:\/\/www.couchbase.com\/blog\/wp-json\/wp\/v2\/media?parent=16480"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.couchbase.com\/blog\/wp-json\/wp\/v2\/categories?post=16480"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.couchbase.com\/blog\/wp-json\/wp\/v2\/tags?post=16480"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.couchbase.com\/blog\/wp-json\/wp\/v2\/ppma_author?post=16480"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}