Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: add email responder example #1506

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions samples/js-emailResponder/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# Customer Service Chatbot with Genkit

This project demonstrates how to build a customer service chatbot using Genkit, a powerful AI framework for building conversational applications. The chatbot is designed to handle various customer inquiries related to products, orders, and general catalog questions.

Key features:

- Uses SQLite as the underlying database for easy setup and portability
- Implements complex flow logic with conditional branching based on customer intent:
- Queries distinct data tables depending on the intent / sub-intent
- Includes evaluation examples using Genkit's evaluation framework to measure:
- Response faithfulness to source data
- Answer relevancy to customer questions
- Ready for deployment on Google Cloud Platform with Vertex AI integration

## Prerequisites

Before you begin, make sure you have the following installed:

- Node.js (v20 or later)
- npm (v10.5.0 or later)
- Genkit CLI

## Getting Started

1. Clone this repository:

```
git clone https://github.com/jeffdh5/customer-service-chatbot.git
cd customer-service-chatbot
```

2. Install dependencies:

```
pnpm i
```

3. Set up your database:
a. Set up your environment variables:
Create a `.env` file in the root directory and add the following:

```
PROJECT_ID=[YOUR PROJECT ID]
LOCATION=[YOUR LOCATION]
```

b. Set up the database and seed with sample data:

```
cd src/
npx prisma generate
npx prisma migrate dev --name init
npm run prisma:seed
cd ../
```

4. Test the chatbot with sample data

After seeding the database, you can test the chatbot with these example queries that match our seed data:

```bash
# In a different terminal
npm run genkit:dev

# Test the classify inquiry flow to detect intent
genkit flow:run classifyInquiryFlow '{
"inquiry": "Is the Classic Blue T-Shirt for $19.99 still in stock?"
}'

# Test e2e CS flow (generates a email response to the input inquiry)
genkit flow:run customerServiceFlow '{
"from": "[email protected]",
"to": "[email protected]",
"subject": "Product Catalog",
"body": "What products do you have under $50?",
"sentAt": "2024-03-14T12:00:00Z",
"threadHistory": []
}'
```

### Seeded Data Reference

The SQLite database comes pre-seeded with some data so that you can
easily test out your queries.

#### Products:

- Classic Blue T-Shirt ($19.99, SKU: BLU-TSHIRT-M)
- Running Shoes ($89.99, SKU: RUN-SHOE-42)
- Denim Jeans ($49.99, SKU: DEN-JEAN-32)
- Leather Wallet ($29.99, SKU: LEA-WALL-01)
- Wireless Headphones ($149.99, SKU: WIR-HEAD-BK)

#### Customers and Their Orders:

- John Doe ([email protected])
- Order TRACK123456: 2 Blue T-Shirts, 1 Running Shoes (DELIVERED)
- Jane Smith ([email protected])
- Order TRACK789012: 1 Denim Jeans (PROCESSING)
- Bob Wilson ([email protected])
- Order TRACK345678: 1 Leather Wallet, 1 Wireless Headphones (PENDING)

5. Run evals
```
genkit eval:flow classifyInquiryFlow --input evals/classifyInquiryTestInputs.json
genkit eval:flow generateDraftFlow --input evals/generateDraftTestInputs.json
```

## Project Structure

The project is structured as follows:

- `src/`: Contains the main application code
- `prisma/`: Contains the Prisma schema and migrations (for PostgreSQL)
- `prompts/`: Contains the prompt templates for Genkit
- `scripts/`: Contains utility scripts

## Handler Concept

The chatbot uses a handler-based architecture to process inquiries:

1. Inquiry Classification: Categorizes user inquiries
2. Response Generation: Creates draft responses based on classification
3. Human Review (optional): Routes complex inquiries for human review

This allows you to configure the responder to only handle specific intents.
If there is no handler, then the flow will escalate the conversation.

It also gives you the flexibility to control the logic for each handler.

Handlers are modular and extensible, located in `src/handlers/`.
16 changes: 16 additions & 0 deletions samples/js-emailResponder/evals/classifyInquiryTestInputs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[
{ "inquiry": "I want to return a product I bought last week" },
{ "inquiry": "What's the status of my order?" },
{ "inquiry": "Do you have this shirt in blue?" },
{ "inquiry": "I need help setting up my account" },
{ "inquiry": "How can I track my package?" },
{ "inquiry": "Is there a warranty on this product?" },
{ "inquiry": "Can I change my shipping address?" },
{ "inquiry": "What are your store hours?" },
{ "inquiry": "Do you offer gift wrapping?" },
{ "inquiry": "How do I apply a coupon code?" },
{ "inquiry": "Are there any ongoing sales or promotions?" },
{ "inquiry": "Can I cancel my subscription?" },
{ "inquiry": "What's your return policy?" },
{ "inquiry": "How long does shipping usually take?" }
]
165 changes: 165 additions & 0 deletions samples/js-emailResponder/evals/generateDraftTestInputs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
[
{
"intent": "Product",
"subintent": "StockAvailability",
"inquiry": "Do you have the blue t-shirt in size medium?",
"context": {
"product": {
"id": "PROD123",
"name": "Classic Blue T-Shirt",
"description": "Comfortable cotton t-shirt in classic blue",
"price": 19.99,
"sku": "BLU-TSHIRT-M",
"stockLevel": 5
}
},
"handlerResult": {
"needsUserInput": false,
"nextAction": "DONE",
"actionsTaken": ["Retrieved product details"],
"data": {
"product": {
"id": "PROD123",
"name": "Classic Blue T-Shirt",
"description": "Comfortable cotton t-shirt in classic blue",
"price": 19.99,
"sku": "BLU-TSHIRT-M",
"stockLevel": 5
}
},
"handlerCompleted": true
}
},
{
"intent": "Order",
"subintent": "TrackingStatus",
"inquiry": "Where is my order #ORD456? It's been a week since I ordered.",
"context": {
"customerEmail": "[email protected]"
},
"handlerResult": {
"needsUserInput": false,
"nextAction": "DONE",
"actionsTaken": ["Retrieved customer details", "Fetched recent orders"],
"data": {
"customer": {
"id": "CUST789",
"email": "[email protected]",
"name": "John Doe"
},
"recentOrders": [
{
"id": "ORD456",
"orderDate": "2023-05-01T00:00:00Z",
"status": "SHIPPED",
"trackingNumber": "TRACK123456",
"orderItems": [
{
"id": "ITEM1",
"product": {
"id": "PROD123",
"name": "Classic Blue T-Shirt"
},
"quantity": 2
}
]
}
]
},
"handlerCompleted": true
}
},
{
"intent": "Returns",
"subintent": "ProcessInquiry",
"inquiry": "How do I return the shoes I bought last week? They don't fit.",
"context": {
"customerEmail": "[email protected]"
},
"handlerResult": {
"needsUserInput": false,
"nextAction": "DONE",
"actionsTaken": ["Retrieved customer details", "Fetched recent orders"],
"data": {
"customer": {
"id": "CUST789",
"email": "[email protected]",
"name": "John Doe"
},
"recentOrders": [
{
"id": "ORD789",
"orderDate": "2023-04-25T00:00:00Z",
"status": "DELIVERED",
"orderItems": [
{
"id": "ITEM2",
"product": {
"id": "PROD456",
"name": "Running Shoes"
},
"quantity": 1
}
]
}
]
},
"handlerCompleted": true
}
},
{
"intent": "Account",
"subintent": "LoginIssue",
"inquiry": "I can't log into my account. It says my password is incorrect but I'm sure it's right.",
"context": {
"customerEmail": "[email protected]"
},
"handlerResult": {
"needsUserInput": true,
"nextAction": "RESET_PASSWORD",
"actionsTaken": ["Retrieved customer details"],
"data": {
"customer": {
"id": "CUST789",
"email": "[email protected]",
"name": "John Doe"
}
},
"handlerCompleted": false
}
},
{
"intent": "Catalog",
"subintent": "ProductAvailability",
"inquiry": "What products do you have available?",
"context": {},
"handlerResult": {
"needsUserInput": false,
"nextAction": "DONE",
"actionsTaken": ["Listed available products"],
"data": {
"products": [
{
"id": "PROD123",
"name": "Classic Blue T-Shirt",
"price": 19.99,
"stockLevel": 5
},
{
"id": "PROD456",
"name": "Running Shoes",
"price": 89.99,
"stockLevel": 10
},
{
"id": "PROD789",
"name": "Denim Jeans",
"price": 49.99,
"stockLevel": 15
}
]
},
"handlerCompleted": true
}
}
]
54 changes: 54 additions & 0 deletions samples/js-emailResponder/genkit-tools.conf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

module.exports = {
evaluators: [
{
flowName: 'generateDraftFlow',
extractors: {
context: (trace) => {
const rootSpan = Object.values(trace.spans).find(
(s) =>
s.attributes['genkit:type'] === 'flow' &&
s.attributes['genkit:name'] === 'generateDraftFlow'
);
if (!rootSpan) return JSON.stringify([]);

const input = JSON.parse(rootSpan.attributes['genkit:input']);
return JSON.stringify([JSON.stringify(input)]);
},
// Keep the default extractors for input and output
},
},
{
flowName: 'classifyInquiryFlow',
extractors: {
context: (trace) => {
const rootSpan = Object.values(trace.spans).find(
(s) =>
s.attributes['genkit:type'] === 'flow' &&
s.attributes['genkit:name'] === 'classifyInquiryFlow'
);
if (!rootSpan) return JSON.stringify([]);

const input = JSON.parse(rootSpan.attributes['genkit:input']);
return JSON.stringify([JSON.stringify(input)]);
},
// Keep the default extractors for input and output
},
},
],
};
Loading
Loading