From d37f9d04991379f2429ade4291ee0519a6fad1ed Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Thu, 12 Dec 2024 13:48:56 -0600 Subject: [PATCH 1/5] samples: add email responder example --- samples/js-emailResponder/README.md | 122 +++++++ .../evals/classifyInquiryTestInputs.json | 16 + .../evals/generateDraftTestInputs.json | 165 +++++++++ .../js-emailResponder/genkit-tools.conf.js | 35 ++ samples/js-emailResponder/package.json | 44 +++ .../prompts/classify_inquiry.prompt | 40 +++ .../prompts/extract_info.prompt | 47 +++ .../prompts/generate_draft.prompt | 42 +++ .../handler_catalog_generalquestion.prompt | 49 +++ .../handler_order_generalquestion.prompt | 49 +++ .../handler_product_generalquestion.prompt | 49 +++ .../handler_product_stockavailability.prompt | 50 +++ samples/js-emailResponder/src/db.ts | 324 ++++++++++++++++++ samples/js-emailResponder/src/handlers.ts | 66 ++++ samples/js-emailResponder/src/index.ts | 302 ++++++++++++++++ .../src/prisma/schema.prisma | 59 ++++ samples/js-emailResponder/src/prisma/seed.ts | 156 +++++++++ samples/js-emailResponder/tsconfig.json | 110 ++++++ 18 files changed, 1725 insertions(+) create mode 100644 samples/js-emailResponder/README.md create mode 100644 samples/js-emailResponder/evals/classifyInquiryTestInputs.json create mode 100644 samples/js-emailResponder/evals/generateDraftTestInputs.json create mode 100644 samples/js-emailResponder/genkit-tools.conf.js create mode 100644 samples/js-emailResponder/package.json create mode 100644 samples/js-emailResponder/prompts/classify_inquiry.prompt create mode 100644 samples/js-emailResponder/prompts/extract_info.prompt create mode 100644 samples/js-emailResponder/prompts/generate_draft.prompt create mode 100644 samples/js-emailResponder/prompts/handler_catalog_generalquestion.prompt create mode 100644 samples/js-emailResponder/prompts/handler_order_generalquestion.prompt create mode 100644 samples/js-emailResponder/prompts/handler_product_generalquestion.prompt create mode 100644 samples/js-emailResponder/prompts/handler_product_stockavailability.prompt create mode 100644 samples/js-emailResponder/src/db.ts create mode 100644 samples/js-emailResponder/src/handlers.ts create mode 100644 samples/js-emailResponder/src/index.ts create mode 100644 samples/js-emailResponder/src/prisma/schema.prisma create mode 100644 samples/js-emailResponder/src/prisma/seed.ts create mode 100644 samples/js-emailResponder/tsconfig.json diff --git a/samples/js-emailResponder/README.md b/samples/js-emailResponder/README.md new file mode 100644 index 000000000..06c7c56bf --- /dev/null +++ b/samples/js-emailResponder/README.md @@ -0,0 +1,122 @@ +# 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 (v14 or later) +- npm (v6 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": "john.doe@example.com", + "to": "support@company.com", + "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 (john.doe@example.com) + - Order TRACK123456: 2 Blue T-Shirts, 1 Running Shoes (DELIVERED) + - Jane Smith (jane.smith@example.com) + - Order TRACK789012: 1 Denim Jeans (PROCESSING) + - Bob Wilson (bob.wilson@example.com) + - 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/`. \ No newline at end of file diff --git a/samples/js-emailResponder/evals/classifyInquiryTestInputs.json b/samples/js-emailResponder/evals/classifyInquiryTestInputs.json new file mode 100644 index 000000000..d60c5e579 --- /dev/null +++ b/samples/js-emailResponder/evals/classifyInquiryTestInputs.json @@ -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?" } +] \ No newline at end of file diff --git a/samples/js-emailResponder/evals/generateDraftTestInputs.json b/samples/js-emailResponder/evals/generateDraftTestInputs.json new file mode 100644 index 000000000..12511b3ba --- /dev/null +++ b/samples/js-emailResponder/evals/generateDraftTestInputs.json @@ -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": "customer@example.com" + }, + "handlerResult": { + "needsUserInput": false, + "nextAction": "DONE", + "actionsTaken": ["Retrieved customer details", "Fetched recent orders"], + "data": { + "customer": { + "id": "CUST789", + "email": "customer@example.com", + "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": "customer@example.com" + }, + "handlerResult": { + "needsUserInput": false, + "nextAction": "DONE", + "actionsTaken": ["Retrieved customer details", "Fetched recent orders"], + "data": { + "customer": { + "id": "CUST789", + "email": "customer@example.com", + "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": "customer@example.com" + }, + "handlerResult": { + "needsUserInput": true, + "nextAction": "RESET_PASSWORD", + "actionsTaken": ["Retrieved customer details"], + "data": { + "customer": { + "id": "CUST789", + "email": "customer@example.com", + "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 + } + } + ] \ No newline at end of file diff --git a/samples/js-emailResponder/genkit-tools.conf.js b/samples/js-emailResponder/genkit-tools.conf.js new file mode 100644 index 000000000..491bb2b3e --- /dev/null +++ b/samples/js-emailResponder/genkit-tools.conf.js @@ -0,0 +1,35 @@ +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 + }, + }, + ], +}; + diff --git a/samples/js-emailResponder/package.json b/samples/js-emailResponder/package.json new file mode 100644 index 000000000..17c2500bd --- /dev/null +++ b/samples/js-emailResponder/package.json @@ -0,0 +1,44 @@ +{ + "name": "customer-service-chatbot", + "version": "1.0.0", + "main": "lib/index.js", + "scripts": { + "start": "node lib/index.js", + "genkit:dev": "genkit start -- tsx --watch src/index.ts", + "compile": "tsc", + "build": "npm run build:clean && npm run compile", + "build:clean": "rimraf ./lib", + "build:watch": "tsc --watch", + "build-and-run": "npm run build && node lib/index.js", + "migrate": "prisma migrate dev", + "prisma:seed": "ts-node src/prisma/seed.ts", + "prisma:studio": "prisma studio" + }, + "dependencies": { + "@faker-js/faker": "^7.5.0", + "@genkit-ai/evaluator": "^0.9.9", + "@genkit-ai/google-cloud": "^0.9.9", + "@genkit-ai/googleai": "^0.9.9", + "@genkit-ai/vertexai": "^0.9.9", + "@prisma/client": "^4.0.0", + "cors": "^2.8.5", + "dotenv": "^16.4.7", + "express": "^4.21.0", + "genkit": "^0.9.9", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/node": "^20.16.5", + "genkit-cli": "^0.9.9", + "nodemon": "^3.1.4", + "prisma": "^4.0.0", + "ts-node": "^10.9.2", + "typescript": "^5.7.2" + }, + "prisma": { + "seed": "ts-node ./prisma/seed.ts" + }, + "packageManager": "pnpm@9.13.2+sha256.ccce81bf7498c5f0f80e31749c1f8f03baba99d168f64590fc7e13fad3ea1938" +} diff --git a/samples/js-emailResponder/prompts/classify_inquiry.prompt b/samples/js-emailResponder/prompts/classify_inquiry.prompt new file mode 100644 index 000000000..bee83e7a6 --- /dev/null +++ b/samples/js-emailResponder/prompts/classify_inquiry.prompt @@ -0,0 +1,40 @@ +--- +model: vertexai/gemini-1.5-pro +input: + schema: + inquiry: string +output: + schema: + type: object + properties: + intent: + type: string + subintent: + type: string +--- +Classify the following customer inquiry into one of these intent/subintent pairs: +- Catalog/GeneralQuestion +- Catalog/ProductAvailability +- Product/GeneralQuestion +- Product/StockAvailability +- Product/PriceInquiry +- Order/GeneralQuestion +- Order/TrackingStatus +- Order/CancellationRequest +- Returns/ProcessInquiry +- Returns/RefundStatus +- Shipping/DeliveryTimeframe +- Shipping/CostInquiry +- Account/LoginIssue +- Account/UpdateInformation +- Payment/MethodInquiry +- Payment/TransactionIssue +- Warranty/CoverageInquiry +- Warranty/ClaimProcess +- Feedback/ProductReview +- Feedback/CustomerService +- Other/Other + +Customer inquiry: {{inquiry}} + +Classification: \ No newline at end of file diff --git a/samples/js-emailResponder/prompts/extract_info.prompt b/samples/js-emailResponder/prompts/extract_info.prompt new file mode 100644 index 000000000..32e819e2a --- /dev/null +++ b/samples/js-emailResponder/prompts/extract_info.prompt @@ -0,0 +1,47 @@ +--- +model: vertexai/gemini-1.5-pro +input: + schema: + inquiry: string + category: string +output: + schema: + type: object + properties: + productId: + type: string + orderId: + type: string + customerId: + type: string + issue: + type: string +--- + +### TASK +Extract key information from the following customer inquiry. + +Please provide the following information: +- Product ID (if mentioned) +- Order ID (if mentioned) +- Customer ID (if mentioned) +- Main issue or question + +Important: If any of the requested information is not available in the inquiry, provide an empty string for that field. Do NOT invent or hallucinate any information. + +### CUSTOMER INQUIRY CONTEXT + +This is the inquiry information. + +Category: {{category}} +Inquiry: {{inquiry}} + +### EXAMPLE OUTPUT +{ + "productId": "PROD123", + "orderId": "ORD456", + "customerId": "", + "issue": "When will my order be delivered?" +} + +Extracted Information: \ No newline at end of file diff --git a/samples/js-emailResponder/prompts/generate_draft.prompt b/samples/js-emailResponder/prompts/generate_draft.prompt new file mode 100644 index 000000000..2eb127307 --- /dev/null +++ b/samples/js-emailResponder/prompts/generate_draft.prompt @@ -0,0 +1,42 @@ +--- +model: vertexai/gemini-1.5-pro +input: + schema: + type: object + properties: + intent: + type: string + subintent: + type: string + inquiry: + type: string + context: + type: string +output: + schema: + type: object + properties: + draftResponse: + type: string +--- +Generate a draft response for the following customer inquiry: + +Intent: {{intent}} +Subintent: {{subintent}} +Customer Inquiry: {{inquiry}} +Context: {{context}} + +{% if escalate %} +This inquiry needs to be escalated to a human representative. +{% endif %} + +Please create a helpful and empathetic draft response addressing the customer's concerns. Include relevant details from the provided information based on the intent and subintent: + +1. For Catalog/GeneralQuestion: Provide an overview of the catalog or answer general questions about products. +2. For Product/GeneralQuestion: Answer general questions about the specific product, using the product information provided. +3. For Product/StockAvailability: Inform the customer about the stock status of the requested product. +4. For Other/Other: Politely inform the customer that their inquiry will be escalated to a specialist for further assistance. + +Ensure the tone is professional, friendly, and tailored to the customer's needs. + +Draft Response: \ No newline at end of file diff --git a/samples/js-emailResponder/prompts/handler_catalog_generalquestion.prompt b/samples/js-emailResponder/prompts/handler_catalog_generalquestion.prompt new file mode 100644 index 000000000..63a5f919d --- /dev/null +++ b/samples/js-emailResponder/prompts/handler_catalog_generalquestion.prompt @@ -0,0 +1,49 @@ +--- +model: vertexai/gemini-1.5-pro +input: + schema: + type: object + properties: + inquiry: + type: string + context: + type: string +output: + schema: + type: object + properties: + needsUserInput: + type: boolean + nextAction: + type: string + actionsTaken: + type: array + items: + type: string + data: + type: object + handlerCompleted: + type: boolean + summary: + type: string +--- +You are a customer service AI assistant handling a general question about a product catalog. Follow these steps: + +1. Analyze the customer's inquiry to identify the product or catalog category in question. +2. If the product or category is ambiguous or not specified, ask the customer to confirm before proceeding. +3. Use the available catalog information to answer the customer's question. +4. Provide a clear and concise answer to the customer's question. + +Execute as many steps as possible before requiring user input. If you complete all steps, set handlerCompleted to true. + +Customer Inquiry: {{inquiry}} +Context: {{context}} + +Respond with a JSON object containing: +- needsUserInput: boolean indicating if user input is required +- nextAction: string describing the next action needed from the user. Return DONE if no action. +- actionsTaken: array of strings describing the actions you've taken +- data: object containing any relevant data or answers +- handlerCompleted: boolean indicating if all steps have been completed + +Response: diff --git a/samples/js-emailResponder/prompts/handler_order_generalquestion.prompt b/samples/js-emailResponder/prompts/handler_order_generalquestion.prompt new file mode 100644 index 000000000..54293dba5 --- /dev/null +++ b/samples/js-emailResponder/prompts/handler_order_generalquestion.prompt @@ -0,0 +1,49 @@ +--- +model: vertexai/gemini-1.5-pro +input: + schema: + type: object + properties: + inquiry: + type: string + context: + type: string +output: + schema: + type: object + properties: + needsUserInput: + type: boolean + nextAction: + type: string + actionsTaken: + type: array + items: + type: string + data: + type: object + handlerCompleted: + type: boolean + summary: + type: string +--- +You are a customer service AI assistant handling a general question about a specific order. Follow these steps: + +1. Analyze the customer's inquiry to identify the order in question. +2. If the order is ambiguous or not specified, ask the customer to confirm before proceeding. +3. Use the available information about the order to answer the customer's question. +4. Provide a clear and concise answer to the customer's question. + +Execute as many steps as possible before requiring user input. If you complete all steps, set handlerCompleted to true. + +Customer Inquiry: {{inquiry}} +Context: {{context}} + +Respond with a JSON object containing: +- needsUserInput: boolean indicating if user input is required +- nextAction: string describing the next action needed from the user. Return DONE if no action. +- actionsTaken: array of strings describing the actions you've taken +- data: object containing any relevant data or answers +- handlerCompleted: boolean indicating if all steps have been completed + +Response: \ No newline at end of file diff --git a/samples/js-emailResponder/prompts/handler_product_generalquestion.prompt b/samples/js-emailResponder/prompts/handler_product_generalquestion.prompt new file mode 100644 index 000000000..d27b1e9d2 --- /dev/null +++ b/samples/js-emailResponder/prompts/handler_product_generalquestion.prompt @@ -0,0 +1,49 @@ +--- +model: vertexai/gemini-1.5-pro +input: + schema: + type: object + properties: + inquiry: + type: string + context: + type: string +output: + schema: + type: object + properties: + needsUserInput: + type: boolean + nextAction: + type: string + actionsTaken: + type: array + items: + type: string + data: + type: object + handlerCompleted: + type: boolean + summary: + type: string +--- +You are a customer service AI assistant handling a general question about a specific product. Follow these steps: + +1. Analyze the customer's inquiry to identify the product in question. +2. If the product is ambiguous or not specified, ask the customer to confirm before proceeding. +3. Use the available information about the product to answer the customer's question. +4. Provide a clear and concise answer to the customer's question. + +Execute as many steps as possible before requiring user input. If you complete all steps, set handlerCompleted to true. + +Customer Inquiry: {{inquiry}} +Context: {{context}} + +Respond with a JSON object containing: +- needsUserInput: boolean indicating if user input is required +- nextAction: string describing the next action needed from the user. Return DONE if no action. +- actionsTaken: array of strings describing the actions you've taken +- data: object containing any relevant data or answers +- handlerCompleted: boolean indicating if all steps have been completed + +Response: \ No newline at end of file diff --git a/samples/js-emailResponder/prompts/handler_product_stockavailability.prompt b/samples/js-emailResponder/prompts/handler_product_stockavailability.prompt new file mode 100644 index 000000000..f11b5212c --- /dev/null +++ b/samples/js-emailResponder/prompts/handler_product_stockavailability.prompt @@ -0,0 +1,50 @@ +--- +model: vertexai/gemini-1.5-pro +input: + schema: + type: object + properties: + inquiry: + type: string + context: + type: string +output: + schema: + type: object + properties: + needsUserInput: + type: boolean + nextAction: + type: string + actionsTaken: + type: array + items: + type: string + data: + type: object + handlerCompleted: + type: boolean + summary: + type: string +--- +You are a customer service AI assistant handling a stock availability question about a specific product. Follow these steps: + +1. Analyze the customer's inquiry to identify the product in question. +2. If the product is ambiguous or not specified, ask the customer to confirm before proceeding. +3. Check the available information about the product's stock status. +4. Provide a clear and concise answer about the product's availability. +5. If the product is out of stock, provide information on when it might be restocked, if available. + +Execute as many steps as possible before requiring user input. If you complete all steps, set handlerCompleted to true. + +Customer Inquiry: {{inquiry}} +Context: {{context}} + +Respond with a JSON object containing: +- needsUserInput: boolean indicating if user input is required +- nextAction: string describing the next action needed from the user. Return DONE if no action. +- actionsTaken: array of strings describing the actions you've taken +- data: object containing any relevant data or answers about stock availability +- handlerCompleted: boolean indicating if all steps have been completed + +Response: diff --git a/samples/js-emailResponder/src/db.ts b/samples/js-emailResponder/src/db.ts new file mode 100644 index 000000000..8c147f8ad --- /dev/null +++ b/samples/js-emailResponder/src/db.ts @@ -0,0 +1,324 @@ +import { PrismaClient } from '@prisma/client' + +const prisma = new PrismaClient() + +// Product-related functions +/** + * Retrieves a product by its ID + * @param id The ID of the product + * @returns The product or null if not found + */ +export async function getProductById(id: number) { + try { + return await prisma.product.findUnique({ + where: { id }, + }) + } catch (error) { + console.error('Error fetching product:', error) + throw error + } +} + +/** + * Lists all products + * @returns An array of all products + */ +export async function listProducts() { + try { + return await prisma.product.findMany() + } catch (error) { + console.error('Error listing products:', error) + throw error + } +} + +// Order-related functions +/** + * Retrieves an order by its ID + * @param id The ID of the order + * @returns The order with customer and product details, or null if not found + */ +export async function getOrderById(id: number) { + try { + return await prisma.order.findUnique({ + where: { id }, + include: { + customer: true, + orderItems: { + include: { + product: true, + }, + }, + }, + }) + } catch (error) { + console.error('Error fetching order:', error) + throw error + } +} + +/** + * Lists all orders + * @returns An array of all orders with customer and product details + */ +export async function listOrders() { + try { + return await prisma.order.findMany({ + include: { + customer: true, + orderItems: { + include: { + product: true, + }, + }, + }, + }) + } catch (error) { + console.error('Error listing orders:', error) + throw error + } +} + +/** + * Retrieves recent orders for a customer + * @param customerId The ID of the customer + * @param limit The maximum number of orders to retrieve (default: 5) + * @returns An array of recent orders with product details + */ +export async function getRecentOrders(customerId: number, limit: number = 5) { + try { + return await prisma.order.findMany({ + where: { + customerId: customerId + }, + orderBy: { + orderDate: 'desc' + }, + take: limit, + include: { + orderItems: { + include: { + product: true + } + } + } + }) + } catch (error) { + console.error('Error fetching recent orders:', error) + throw error + } +} + +/** + * Retrieves orders by customer email + * @param email The email of the customer + * @returns An array of orders or null if the customer is not found + */ +export async function getOrdersByCustomerEmail(email: string) { + try { + const customer = await prisma.customer.findUnique({ + where: { + email: email + }, + include: { + orders: { + include: { + orderItems: { + include: { + product: true + } + } + } + } + } + }) + return customer ? customer.orders : null + } catch (error) { + console.error('Error fetching orders by customer email:', error) + throw error + } +} + +/** + * Retrieves recent orders by customer email + * @param email The email of the customer + * @param limit The maximum number of orders to retrieve (default: 5) + * @returns An array of recent orders with product details + */ +export async function getRecentOrdersByEmail(email: string, limit: number = 5) { + try { + return await prisma.order.findMany({ + where: { + customer: { + email: email + } + }, + orderBy: { + orderDate: 'desc' + }, + take: limit, + include: { + orderItems: { + include: { + product: true + } + } + } + }) + } catch (error) { + console.error('Error fetching recent orders by email:', error) + throw error + } +} + +// Customer-related functions +/** + * Retrieves a customer by their ID + * @param id The ID of the customer + * @returns The customer with their orders, or null if not found + */ +export async function getCustomerById(id: number) { + try { + return await prisma.customer.findUnique({ + where: { id }, + include: { + orders: true, + }, + }) + } catch (error) { + console.error('Error fetching customer:', error) + throw error + } +} + +/** + * Lists all customers + * @returns An array of all customers with their orders + */ +export async function listCustomers() { + try { + return await prisma.customer.findMany({ + include: { + orders: true, + }, + }) + } catch (error) { + console.error('Error listing customers:', error) + throw error + } +} + +/** + * Retrieves a customer by their email + * @param email The email of the customer + * @returns The customer with their orders and order details, or null if not found + */ +export async function getCustomerByEmail(email: string) { + try { + return await prisma.customer.findUnique({ + where: { + email: email + }, + include: { + orders: { + include: { + orderItems: { + include: { + product: true + } + } + } + } + } + }) + } catch (error) { + console.error('Error fetching customer by email:', error) + throw error + } +} + +// Escalation-related functions +/** + * Creates a new escalation + * @param customerId The ID of the customer + * @param subject The subject of the escalation + * @param description The description of the escalation + * @param threadId The thread ID associated with the escalation + * @returns The created escalation + */ +export async function createEscalation(customerId: number, subject: string, description: string, threadId: string) { + try { + return await prisma.escalation.create({ + data: { + customerId, + subject, + description, + threadId, + }, + }) + } catch (error) { + console.error('Error creating escalation:', error) + throw error + } +} + +/** + * Retrieves an escalation by its ID + * @param id The ID of the escalation + * @returns The escalation with customer details, or null if not found + */ +export async function getEscalationById(id: number) { + try { + return await prisma.escalation.findUnique({ + where: { id }, + include: { + customer: true, + }, + }) + } catch (error) { + console.error('Error fetching escalation:', error) + throw error + } +} + +/** + * Updates the status of an escalation + * @param id The ID of the escalation + * @param status The new status of the escalation + * @returns The updated escalation + */ +export async function updateEscalationStatus(id: number, status: string) { + try { + return await prisma.escalation.update({ + where: { id }, + data: { status }, + }) + } catch (error) { + console.error('Error updating escalation status:', error) + throw error + } +} + +/** + * Lists all escalations + * @returns An array of all escalations with customer details + */ +export async function listEscalations() { + try { + return await prisma.escalation.findMany({ + include: { + customer: true, + }, + }) + } catch (error) { + console.error('Error listing escalations:', error) + throw error + } +} + +/** + * Disconnects the Prisma client + */ +export async function disconnectPrisma() { + await prisma.$disconnect() +} \ No newline at end of file diff --git a/samples/js-emailResponder/src/handlers.ts b/samples/js-emailResponder/src/handlers.ts new file mode 100644 index 000000000..981248fde --- /dev/null +++ b/samples/js-emailResponder/src/handlers.ts @@ -0,0 +1,66 @@ +import { z } from 'genkit'; +import * as fs from 'fs'; +import * as path from 'path'; +import { ai } from '.'; + +// Handler concept: A handler is responsible for processing specific intents and subintents in the chatbot. +// It takes user input, processes it, and generates an appropriate response or action. + +// Define the structure of a handler's result +const HandlerResult = z.object({ + needsUserInput: z.boolean(), + nextAction: z.string().optional(), + actionsTaken: z.array(z.string()), + data: z.record(z.unknown()), + handlerCompleted: z.boolean(), +}); + +// Define the input structure for a handler +type HandlerInput = { + intent: string; + subintent: string; + inquiry: string; + context: Record; +}; + +// Main function to execute a handler based on the given input +export async function executeHandler(input: HandlerInput): Promise> { + // Get the appropriate prompt for the handler based on intent and subintent + const handlerPrompt = getHandlerPrompt(input.intent, input.subintent); + + // Generate a response using the handler's prompt + const handlerResult = await handlerPrompt({ + input: { + inquiry: input.inquiry, + context: JSON.stringify(input.context, null, 2), + }, + }); + + // Process the output from the handler + const output = handlerResult.output; + + // Return the structured result of the handler's execution + return { + needsUserInput: output.needsUserInput || false, + nextAction: output.nextAction, + actionsTaken: output.actionsTaken || [], + data: output.data || {}, + handlerCompleted: output.handlerCompleted || false, + }; +} + +// Function to retrieve the appropriate prompt for a handler +function getHandlerPrompt(intent: string, subintent: string) { + // Construct the prompt key based on intent and subintent + const promptKey = `handler_${intent.toLowerCase()}_${subintent.toLowerCase()}`; + + // Determine the file path for the handler's prompt + const promptPath = path.join(__dirname, '..', 'prompts', `${promptKey}.prompt`); + + // Check if the prompt file exists and return it, or throw an error if not found + if (fs.existsSync(promptPath)) { + return ai.prompt(promptKey); + } else { + throw new Error(`NoHandlerPromptError: No handler prompt found for intent '${intent}' and subintent '${subintent}'`); + } +} \ No newline at end of file diff --git a/samples/js-emailResponder/src/index.ts b/samples/js-emailResponder/src/index.ts new file mode 100644 index 000000000..03a1d2dab --- /dev/null +++ b/samples/js-emailResponder/src/index.ts @@ -0,0 +1,302 @@ +import { genkit, z } from 'genkit'; +import { executeHandler } from './handlers'; +import { createEscalation, getCustomerByEmail, getOrderById, getProductById, getRecentOrdersByEmail, listProducts } from './db'; +import vertexAI, { gemini10Pro, gemini15Pro, textEmbedding004 } from '@genkit-ai/vertexai'; +import genkitEval, { GenkitMetric } from '@genkit-ai/evaluator'; + + +// Configure Genkit with necessary plugins +export const ai = genkit({ + plugins: [ + vertexAI({projectId: process.env.PROJECT_ID, location: process.env.LOCATION || "us-central1"}), + genkitEval({ + judge: gemini15Pro, + metrics: [GenkitMetric.FAITHFULNESS, GenkitMetric.ANSWER_RELEVANCY], + embedder: textEmbedding004, + }), + ], + model: gemini10Pro +}); + +// Define prompts +const classifyInquiryPrompt = ai.prompt('classify_inquiry'); +const generateDraftPrompt = ai.prompt('generate_draft'); +const extractInfoPrompt = ai.prompt('extract_info'); + +export const classifyInquiryFlow = ai.defineFlow( + { + name: 'classifyInquiryFlow', + inputSchema: z.object({ + inquiry: z.string(), + }), + outputSchema: z.object({ + intent: z.string(), + subintent: z.string(), + }), + }, + async (input) => { + try { + console.log('Classifying inquiry:', input.inquiry); + const classificationResult = await classifyInquiryPrompt({ inquiry: input.inquiry }); + return classificationResult.output; + } catch (error) { + console.error('Error in classifyInquiryFlow:', error); + throw error; + } + } +); + +export const customerServiceFlow = ai.defineFlow( + { + name: 'customerServiceFlow', + inputSchema: z.object({ + from: z.string(), + to: z.string(), + subject: z.string(), + body: z.string(), + sentAt: z.string(), // Changed from timestamp to sentAt + threadHistory: z.array(z.object({ + from: z.string(), + to: z.string(), + body: z.string(), + sentAt: z.string(), // Changed from timestamp to sentAt + })), + }), + outputSchema: z.object({ + intent: z.string(), + subintent: z.string(), + response: z.string(), + needsUserInput: z.boolean(), + nextAction: z.string().optional(), + }), + }, + async (input) => { + console.log('Starting customerServiceFlow with input:', { + from: input.from, + to: input.to, + subject: input.subject, + body: input.body, + threadHistoryLength: input.threadHistory.length + }); + + // Step 1: Classify the inquiry + console.log('Step 1: Classifying inquiry...'); + const classificationResult = await classifyInquiryFlow({inquiry: input.body}); + console.log('Classification result:', classificationResult); + const { intent, subintent } = classificationResult; + + // Step 2: Augment data + console.log('Step 2: Augmenting data...'); + const augmentedData = await augmentInfo({ + intent, + customerInquiry: input.body, + email: input.from, + }); + console.log('Augmented data:', augmentedData); + + // Step 3: Execute Handler + console.log('Step 3: Executing handler...'); + let handlerResult; + try { + handlerResult = await executeHandlerFlow({ + intent, + subintent, + inquiry: input.body, + context: { + ...augmentedData.responseData, + subject: input.subject, + threadHistory: input.threadHistory, + }, + }); + console.log('Handler result:', handlerResult); + } catch (error) { + console.error('Error executing handler:', error); + // Escalate if no handler + if (error instanceof Error && error.message.startsWith('NoHandlerPromptError')) { + console.log('No handler found, escalating to human...'); + const escalationResult = await escalateToHuman(input.body, input.from, 'No handler found'); + console.log('Escalation result:', escalationResult); + return { + intent, + subintent, + response: escalationResult.message, + needsUserInput: false, + nextAction: 'wait_for_human', + escalated: true, + escalationReason: 'No handler found', + }; + } else { + throw error; // Re-throw other errors + } + } + + // Step 4: Generate response + console.log('Step 4: Generating response...'); + const responseResult = await generateDraftFlow({ + intent, + subintent, + inquiry: input.body, + context: { + ...augmentedData.responseData, + subject: input.subject, + threadHistory: input.threadHistory, + }, + handlerResult: handlerResult.data, + }); + console.log('Generated response:', responseResult); + + const result = { + intent, + subintent, + response: responseResult.draftResponse, + needsUserInput: handlerResult.needsUserInput ?? false, + nextAction: handlerResult.nextAction, + escalated: false, + }; + console.log('Final result:', result); + return result; + } +); + +async function escalateToHuman(inquiry: string, email: string, reason: string) { + const customer = await getCustomerByEmail(email); + if (!customer) { + throw new Error('Customer not found'); + } + + const escalation = await createEscalation( + customer.id, + 'Customer Inquiry Escalation', + `Inquiry: ${inquiry}\n\nReason for escalation: ${reason}`, + inquiry + ); + + return { + message: "Your inquiry has been escalated to our customer service team. We'll get back to you as soon as possible.", + escalationId: escalation.id + }; +} + +export const augmentInfo = ai.defineFlow( + { + name: 'augmentInfoFlow', + inputSchema: z.object({ + intent: z.string(), + customerInquiry: z.string(), + email: z.string(), + }), + outputSchema: z.object({ + responseData: z.record(z.unknown()), + }), + }, + async (input) => { + let responseData = {}; + switch (input.intent) { + case 'Catalog': + const products = await listProducts(); + responseData = { catalog: products }; + break; + case 'Product': + const productInfo = await extractInfoFlow({ inquiry: input.customerInquiry }); + if (productInfo.productId) { + const product = await getProductById(productInfo.productId); + responseData = { product }; + } else { + const products = await listProducts(); + responseData = { products }; + } + break; + case 'Order': + const orderInfo = await extractInfoFlow({ inquiry: input.customerInquiry }); + console.log(orderInfo); + console.log('Extracted order info:', orderInfo); + if (orderInfo.orderId) { + const order = await getOrderById(orderInfo.orderId); + console.log('Retrieved order:', order); + responseData = { order }; + } else { + const recentOrders = await getRecentOrdersByEmail(input.email); + responseData = { recentOrders }; + } + break; + case 'Other': + const customer = await getCustomerByEmail(input.email); + responseData = { customer }; + break; + } + return { responseData }; + }) + +export const extractInfoFlow = ai.defineFlow( + { + name: 'extractInfoFlow', + inputSchema: z.object({ + inquiry: z.string(), + }), + outputSchema: z.object({ + productId: z.number(), + orderId: z.number(), + customerId: z.number(), + issue: z.string(), + }), + }, + async (input) => { + const extractionResult = await extractInfoPrompt({ + inquiry: input.inquiry, + category: 'Customer Service' + }); + const output = extractionResult.output; + return { + productId: output.productId ? parseInt(output.productId, 10) : 0, + orderId: output.orderId ? parseInt(output.orderId, 10) : 0, + customerId: output.customerId ? parseInt(output.customerId, 10) : 0, + issue: output.issue || "", + }; + } +); + +export const executeHandlerFlow = ai.defineFlow( + { + name: 'executeHandlerFlow', + inputSchema: z.object({ + intent: z.string(), + subintent: z.string(), + inquiry: z.string(), + context: z.record(z.unknown()), + }), + outputSchema: z.object({ + data: z.unknown(), + needsUserInput: z.boolean().optional(), + nextAction: z.string().optional(), + }), + }, + async (input) => { + return executeHandler(input); + } +); + +export const generateDraftFlow = ai.defineFlow( + { + name: 'generateDraftFlow', + inputSchema: z.object({ + intent: z.string(), + subintent: z.string(), + inquiry: z.string(), + context: z.record(z.unknown()), + handlerResult: z.unknown(), + }), + outputSchema: z.object({ + draftResponse: z.string(), + }), + }, + async (input) => { + const responseResult = await generateDraftPrompt({ + intent: input.intent, + subintent: input.subintent, + inquiry: input.inquiry, + context: JSON.stringify(input.context, null, 2), + handlerResult: input.handlerResult, + }); + return { draftResponse: responseResult.output.draftResponse }; + } +); \ No newline at end of file diff --git a/samples/js-emailResponder/src/prisma/schema.prisma b/samples/js-emailResponder/src/prisma/schema.prisma new file mode 100644 index 000000000..90c4e3539 --- /dev/null +++ b/samples/js-emailResponder/src/prisma/schema.prisma @@ -0,0 +1,59 @@ +datasource db { + provider = "sqlite" + url = "file:./dev.db" +} + +generator client { + provider = "prisma-client-js" +} + +model Customer { + id Int @id @default(autoincrement()) + name String + email String @unique + orders Order[] + escalations Escalation[] +} + +model Product { + id Int @id @default(autoincrement()) + name String + description String + price Float + sku String @unique + stockLevel Int + orderItems OrderItem[] +} + +model Order { + id Int @id @default(autoincrement()) + customer Customer @relation(fields: [customerId], references: [id]) + customerId Int + orderDate DateTime @default(now()) + status String @default("PENDING") + trackingNumber String? + orderItems OrderItem[] +} + +model OrderItem { + id Int @id @default(autoincrement()) + order Order @relation(fields: [orderId], references: [id]) + orderId Int + product Product @relation(fields: [productId], references: [id]) + productId Int + quantity Int + + @@unique([orderId, productId]) +} + +model Escalation { + id Int @id @default(autoincrement()) + customer Customer @relation(fields: [customerId], references: [id]) + customerId Int + subject String + description String + status String @default("OPEN") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + threadId String +} diff --git a/samples/js-emailResponder/src/prisma/seed.ts b/samples/js-emailResponder/src/prisma/seed.ts new file mode 100644 index 000000000..3e7709c96 --- /dev/null +++ b/samples/js-emailResponder/src/prisma/seed.ts @@ -0,0 +1,156 @@ +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +async function main() { + + // Seed Products with hardcoded values + const products = []; + const productData = [ + { + id: 1, + name: 'Classic Blue T-Shirt', + description: 'Comfortable cotton t-shirt in classic blue', + stockLevel: 50, + price: 19.99, + sku: 'BLU-TSHIRT-M', + }, + { + id: 2, + name: 'Running Shoes', + description: 'Lightweight running shoes with cushioned sole', + stockLevel: 25, + price: 89.99, + sku: 'RUN-SHOE-42', + }, + { + id: 3, + name: 'Denim Jeans', + description: 'Classic fit denim jeans in dark wash', + stockLevel: 75, + price: 49.99, + sku: 'DEN-JEAN-32', + }, + { + id: 4, + name: 'Leather Wallet', + description: 'Genuine leather bifold wallet', + stockLevel: 100, + price: 29.99, + sku: 'LEA-WALL-01', + }, + { + id: 5, + name: 'Wireless Headphones', + description: 'Noise-cancelling wireless headphones', + stockLevel: 30, + price: 149.99, + sku: 'WIR-HEAD-BK', + } + ]; + + for (const data of productData) { + products.push( + await prisma.product.create({ + data + }) + ); + } + + // Seed Customers with hardcoded values + const customers = []; + const customerData = [ + { + id: 1, + name: 'John Doe', + email: 'john.doe@example.com', + }, + { + id: 2, + name: 'Jane Smith', + email: 'jane.smith@example.com', + }, + { + id: 3, + name: 'Bob Wilson', + email: 'bob.wilson@example.com', + } + ]; + + for (const data of customerData) { + customers.push( + await prisma.customer.create({ + data + }) + ); + } + + // Seed Orders with hardcoded values + const orderData = [ + { + customerId: customers[0].id, + status: 'DELIVERED', + trackingNumber: 'TRACK123456', + orderItems: { + create: [ + { + productId: products[0].id, + quantity: 2, + }, + { + productId: products[1].id, + quantity: 1, + } + ], + }, + }, + { + customerId: customers[1].id, + status: 'PROCESSING', + trackingNumber: 'TRACK789012', + orderItems: { + create: [ + { + productId: products[2].id, + quantity: 1, + } + ], + }, + }, + { + customerId: customers[2].id, + status: 'PENDING', + trackingNumber: 'TRACK345678', + orderItems: { + create: [ + { + productId: products[3].id, + quantity: 1, + }, + { + productId: products[4].id, + quantity: 1, + } + ], + }, + } + ]; + + for (const data of orderData) { + await prisma.order.create({ + data + }); + } + + console.log('Database has been seeded with hardcoded values.'); +} + +main() + .catch((e) => { + console.error(e); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); + \ No newline at end of file diff --git a/samples/js-emailResponder/tsconfig.json b/samples/js-emailResponder/tsconfig.json new file mode 100644 index 000000000..cf2e01b49 --- /dev/null +++ b/samples/js-emailResponder/tsconfig.json @@ -0,0 +1,110 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "commonjs", /* Specify what module code is generated. */ + "rootDir": "./src", /* Specify the root folder within your source files. */ + // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./lib", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +} From 80d5561a7090f8c61413a36fef11787dd18a28f7 Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Thu, 12 Dec 2024 13:53:50 -0600 Subject: [PATCH 2/5] fix format --- samples/js-emailResponder/README.md | 39 ++- .../evals/classifyInquiryTestInputs.json | 2 +- .../evals/generateDraftTestInputs.json | 294 +++++++++--------- .../js-emailResponder/genkit-tools.conf.js | 9 +- samples/js-emailResponder/src/db.ts | 155 ++++----- samples/js-emailResponder/src/handlers.ts | 23 +- samples/js-emailResponder/src/index.ts | 88 ++++-- samples/js-emailResponder/src/prisma/seed.ts | 20 +- samples/js-emailResponder/tsconfig.json | 16 +- 9 files changed, 351 insertions(+), 295 deletions(-) diff --git a/samples/js-emailResponder/README.md b/samples/js-emailResponder/README.md index 06c7c56bf..d54a2e793 100644 --- a/samples/js-emailResponder/README.md +++ b/samples/js-emailResponder/README.md @@ -1,7 +1,9 @@ # 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 @@ -21,35 +23,39 @@ Before you begin, make sure you have the following installed: ## 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] - ``` + 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 ../ - ``` + + ``` + 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 @@ -73,10 +79,12 @@ Before you begin, make sure you have the following installed: ``` ### 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) @@ -84,6 +92,7 @@ Before you begin, make sure you have the following installed: - Wireless Headphones ($149.99, SKU: WIR-HEAD-BK) #### Customers and Their Orders: + - John Doe (john.doe@example.com) - Order TRACK123456: 2 Blue T-Shirts, 1 Running Shoes (DELIVERED) - Jane Smith (jane.smith@example.com) @@ -93,7 +102,7 @@ Before you begin, make sure you have the following installed: 5. Run evals ``` - genkit eval:flow classifyInquiryFlow --input evals/classifyInquiryTestInputs.json + genkit eval:flow classifyInquiryFlow --input evals/classifyInquiryTestInputs.json genkit eval:flow generateDraftFlow --input evals/generateDraftTestInputs.json ``` @@ -119,4 +128,4 @@ 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/`. \ No newline at end of file +Handlers are modular and extensible, located in `src/handlers/`. diff --git a/samples/js-emailResponder/evals/classifyInquiryTestInputs.json b/samples/js-emailResponder/evals/classifyInquiryTestInputs.json index d60c5e579..257531a25 100644 --- a/samples/js-emailResponder/evals/classifyInquiryTestInputs.json +++ b/samples/js-emailResponder/evals/classifyInquiryTestInputs.json @@ -13,4 +13,4 @@ { "inquiry": "Can I cancel my subscription?" }, { "inquiry": "What's your return policy?" }, { "inquiry": "How long does shipping usually take?" } -] \ No newline at end of file +] diff --git a/samples/js-emailResponder/evals/generateDraftTestInputs.json b/samples/js-emailResponder/evals/generateDraftTestInputs.json index 12511b3ba..1e5a65347 100644 --- a/samples/js-emailResponder/evals/generateDraftTestInputs.json +++ b/samples/js-emailResponder/evals/generateDraftTestInputs.json @@ -1,9 +1,23 @@ [ - { - "intent": "Product", - "subintent": "StockAvailability", - "inquiry": "Do you have the blue t-shirt in size medium?", - "context": { + { + "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", @@ -13,153 +27,139 @@ "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 - } + "handlerCompleted": true + } + }, + { + "intent": "Order", + "subintent": "TrackingStatus", + "inquiry": "Where is my order #ORD456? It's been a week since I ordered.", + "context": { + "customerEmail": "customer@example.com" }, - { - "intent": "Order", - "subintent": "TrackingStatus", - "inquiry": "Where is my order #ORD456? It's been a week since I ordered.", - "context": { - "customerEmail": "customer@example.com" - }, - "handlerResult": { - "needsUserInput": false, - "nextAction": "DONE", - "actionsTaken": ["Retrieved customer details", "Fetched recent orders"], - "data": { - "customer": { - "id": "CUST789", - "email": "customer@example.com", - "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 - } - ] - } - ] + "handlerResult": { + "needsUserInput": false, + "nextAction": "DONE", + "actionsTaken": ["Retrieved customer details", "Fetched recent orders"], + "data": { + "customer": { + "id": "CUST789", + "email": "customer@example.com", + "name": "John Doe" }, - "handlerCompleted": true - } - }, - { - "intent": "Returns", - "subintent": "ProcessInquiry", - "inquiry": "How do I return the shoes I bought last week? They don't fit.", - "context": { - "customerEmail": "customer@example.com" + "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 + } + ] + } + ] }, - "handlerResult": { - "needsUserInput": false, - "nextAction": "DONE", - "actionsTaken": ["Retrieved customer details", "Fetched recent orders"], - "data": { - "customer": { - "id": "CUST789", - "email": "customer@example.com", - "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": "Returns", + "subintent": "ProcessInquiry", + "inquiry": "How do I return the shoes I bought last week? They don't fit.", + "context": { + "customerEmail": "customer@example.com" + }, + "handlerResult": { + "needsUserInput": false, + "nextAction": "DONE", + "actionsTaken": ["Retrieved customer details", "Fetched recent orders"], + "data": { + "customer": { + "id": "CUST789", + "email": "customer@example.com", + "name": "John Doe" }, - "handlerCompleted": true - } + "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": "customer@example.com" }, - { - "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": "customer@example.com" + "handlerResult": { + "needsUserInput": true, + "nextAction": "RESET_PASSWORD", + "actionsTaken": ["Retrieved customer details"], + "data": { + "customer": { + "id": "CUST789", + "email": "customer@example.com", + "name": "John Doe" + } }, - "handlerResult": { - "needsUserInput": true, - "nextAction": "RESET_PASSWORD", - "actionsTaken": ["Retrieved customer details"], - "data": { - "customer": { - "id": "CUST789", - "email": "customer@example.com", - "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": 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 - } + ] + }, + "handlerCompleted": true } - ] \ No newline at end of file + } +] diff --git a/samples/js-emailResponder/genkit-tools.conf.js b/samples/js-emailResponder/genkit-tools.conf.js index 491bb2b3e..cd00b3f8d 100644 --- a/samples/js-emailResponder/genkit-tools.conf.js +++ b/samples/js-emailResponder/genkit-tools.conf.js @@ -5,7 +5,9 @@ module.exports = { extractors: { context: (trace) => { const rootSpan = Object.values(trace.spans).find( - (s) => s.attributes['genkit:type'] === 'flow' && s.attributes['genkit:name'] === 'generateDraftFlow' + (s) => + s.attributes['genkit:type'] === 'flow' && + s.attributes['genkit:name'] === 'generateDraftFlow' ); if (!rootSpan) return JSON.stringify([]); @@ -20,7 +22,9 @@ module.exports = { extractors: { context: (trace) => { const rootSpan = Object.values(trace.spans).find( - (s) => s.attributes['genkit:type'] === 'flow' && s.attributes['genkit:name'] === 'classifyInquiryFlow' + (s) => + s.attributes['genkit:type'] === 'flow' && + s.attributes['genkit:name'] === 'classifyInquiryFlow' ); if (!rootSpan) return JSON.stringify([]); @@ -32,4 +36,3 @@ module.exports = { }, ], }; - diff --git a/samples/js-emailResponder/src/db.ts b/samples/js-emailResponder/src/db.ts index 8c147f8ad..b28471634 100644 --- a/samples/js-emailResponder/src/db.ts +++ b/samples/js-emailResponder/src/db.ts @@ -1,6 +1,6 @@ -import { PrismaClient } from '@prisma/client' +import { PrismaClient } from '@prisma/client'; -const prisma = new PrismaClient() +const prisma = new PrismaClient(); // Product-related functions /** @@ -12,10 +12,10 @@ export async function getProductById(id: number) { try { return await prisma.product.findUnique({ where: { id }, - }) + }); } catch (error) { - console.error('Error fetching product:', error) - throw error + console.error('Error fetching product:', error); + throw error; } } @@ -25,10 +25,10 @@ export async function getProductById(id: number) { */ export async function listProducts() { try { - return await prisma.product.findMany() + return await prisma.product.findMany(); } catch (error) { - console.error('Error listing products:', error) - throw error + console.error('Error listing products:', error); + throw error; } } @@ -50,10 +50,10 @@ export async function getOrderById(id: number) { }, }, }, - }) + }); } catch (error) { - console.error('Error fetching order:', error) - throw error + console.error('Error fetching order:', error); + throw error; } } @@ -72,10 +72,10 @@ export async function listOrders() { }, }, }, - }) + }); } catch (error) { - console.error('Error listing orders:', error) - throw error + console.error('Error listing orders:', error); + throw error; } } @@ -89,23 +89,23 @@ export async function getRecentOrders(customerId: number, limit: number = 5) { try { return await prisma.order.findMany({ where: { - customerId: customerId + customerId: customerId, }, orderBy: { - orderDate: 'desc' + orderDate: 'desc', }, take: limit, include: { orderItems: { include: { - product: true - } - } - } - }) + product: true, + }, + }, + }, + }); } catch (error) { - console.error('Error fetching recent orders:', error) - throw error + console.error('Error fetching recent orders:', error); + throw error; } } @@ -118,24 +118,24 @@ export async function getOrdersByCustomerEmail(email: string) { try { const customer = await prisma.customer.findUnique({ where: { - email: email + email: email, }, include: { orders: { include: { orderItems: { include: { - product: true - } - } - } - } - } - }) - return customer ? customer.orders : null + product: true, + }, + }, + }, + }, + }, + }); + return customer ? customer.orders : null; } catch (error) { - console.error('Error fetching orders by customer email:', error) - throw error + console.error('Error fetching orders by customer email:', error); + throw error; } } @@ -150,24 +150,24 @@ export async function getRecentOrdersByEmail(email: string, limit: number = 5) { return await prisma.order.findMany({ where: { customer: { - email: email - } + email: email, + }, }, orderBy: { - orderDate: 'desc' + orderDate: 'desc', }, take: limit, include: { orderItems: { include: { - product: true - } - } - } - }) + product: true, + }, + }, + }, + }); } catch (error) { - console.error('Error fetching recent orders by email:', error) - throw error + console.error('Error fetching recent orders by email:', error); + throw error; } } @@ -184,10 +184,10 @@ export async function getCustomerById(id: number) { include: { orders: true, }, - }) + }); } catch (error) { - console.error('Error fetching customer:', error) - throw error + console.error('Error fetching customer:', error); + throw error; } } @@ -201,10 +201,10 @@ export async function listCustomers() { include: { orders: true, }, - }) + }); } catch (error) { - console.error('Error listing customers:', error) - throw error + console.error('Error listing customers:', error); + throw error; } } @@ -217,23 +217,23 @@ export async function getCustomerByEmail(email: string) { try { return await prisma.customer.findUnique({ where: { - email: email + email: email, }, include: { orders: { include: { orderItems: { include: { - product: true - } - } - } - } - } - }) + product: true, + }, + }, + }, + }, + }, + }); } catch (error) { - console.error('Error fetching customer by email:', error) - throw error + console.error('Error fetching customer by email:', error); + throw error; } } @@ -246,7 +246,12 @@ export async function getCustomerByEmail(email: string) { * @param threadId The thread ID associated with the escalation * @returns The created escalation */ -export async function createEscalation(customerId: number, subject: string, description: string, threadId: string) { +export async function createEscalation( + customerId: number, + subject: string, + description: string, + threadId: string +) { try { return await prisma.escalation.create({ data: { @@ -255,10 +260,10 @@ export async function createEscalation(customerId: number, subject: string, desc description, threadId, }, - }) + }); } catch (error) { - console.error('Error creating escalation:', error) - throw error + console.error('Error creating escalation:', error); + throw error; } } @@ -274,10 +279,10 @@ export async function getEscalationById(id: number) { include: { customer: true, }, - }) + }); } catch (error) { - console.error('Error fetching escalation:', error) - throw error + console.error('Error fetching escalation:', error); + throw error; } } @@ -292,10 +297,10 @@ export async function updateEscalationStatus(id: number, status: string) { return await prisma.escalation.update({ where: { id }, data: { status }, - }) + }); } catch (error) { - console.error('Error updating escalation status:', error) - throw error + console.error('Error updating escalation status:', error); + throw error; } } @@ -309,10 +314,10 @@ export async function listEscalations() { include: { customer: true, }, - }) + }); } catch (error) { - console.error('Error listing escalations:', error) - throw error + console.error('Error listing escalations:', error); + throw error; } } @@ -320,5 +325,5 @@ export async function listEscalations() { * Disconnects the Prisma client */ export async function disconnectPrisma() { - await prisma.$disconnect() -} \ No newline at end of file + await prisma.$disconnect(); +} diff --git a/samples/js-emailResponder/src/handlers.ts b/samples/js-emailResponder/src/handlers.ts index 981248fde..fbbea0654 100644 --- a/samples/js-emailResponder/src/handlers.ts +++ b/samples/js-emailResponder/src/handlers.ts @@ -1,5 +1,5 @@ -import { z } from 'genkit'; import * as fs from 'fs'; +import { z } from 'genkit'; import * as path from 'path'; import { ai } from '.'; @@ -24,10 +24,12 @@ type HandlerInput = { }; // Main function to execute a handler based on the given input -export async function executeHandler(input: HandlerInput): Promise> { +export async function executeHandler( + input: HandlerInput +): Promise> { // Get the appropriate prompt for the handler based on intent and subintent const handlerPrompt = getHandlerPrompt(input.intent, input.subintent); - + // Generate a response using the handler's prompt const handlerResult = await handlerPrompt({ input: { @@ -55,12 +57,19 @@ function getHandlerPrompt(intent: string, subintent: string) { const promptKey = `handler_${intent.toLowerCase()}_${subintent.toLowerCase()}`; // Determine the file path for the handler's prompt - const promptPath = path.join(__dirname, '..', 'prompts', `${promptKey}.prompt`); - + const promptPath = path.join( + __dirname, + '..', + 'prompts', + `${promptKey}.prompt` + ); + // Check if the prompt file exists and return it, or throw an error if not found if (fs.existsSync(promptPath)) { return ai.prompt(promptKey); } else { - throw new Error(`NoHandlerPromptError: No handler prompt found for intent '${intent}' and subintent '${subintent}'`); + throw new Error( + `NoHandlerPromptError: No handler prompt found for intent '${intent}' and subintent '${subintent}'` + ); } -} \ No newline at end of file +} diff --git a/samples/js-emailResponder/src/index.ts b/samples/js-emailResponder/src/index.ts index 03a1d2dab..947dbab47 100644 --- a/samples/js-emailResponder/src/index.ts +++ b/samples/js-emailResponder/src/index.ts @@ -1,21 +1,34 @@ +import genkitEval, { GenkitMetric } from '@genkit-ai/evaluator'; +import vertexAI, { + gemini10Pro, + gemini15Pro, + textEmbedding004, +} from '@genkit-ai/vertexai'; import { genkit, z } from 'genkit'; +import { + createEscalation, + getCustomerByEmail, + getOrderById, + getProductById, + getRecentOrdersByEmail, + listProducts, +} from './db'; import { executeHandler } from './handlers'; -import { createEscalation, getCustomerByEmail, getOrderById, getProductById, getRecentOrdersByEmail, listProducts } from './db'; -import vertexAI, { gemini10Pro, gemini15Pro, textEmbedding004 } from '@genkit-ai/vertexai'; -import genkitEval, { GenkitMetric } from '@genkit-ai/evaluator'; - // Configure Genkit with necessary plugins export const ai = genkit({ plugins: [ - vertexAI({projectId: process.env.PROJECT_ID, location: process.env.LOCATION || "us-central1"}), + vertexAI({ + projectId: process.env.PROJECT_ID, + location: process.env.LOCATION || 'us-central1', + }), genkitEval({ judge: gemini15Pro, metrics: [GenkitMetric.FAITHFULNESS, GenkitMetric.ANSWER_RELEVANCY], embedder: textEmbedding004, }), ], - model: gemini10Pro + model: gemini10Pro, }); // Define prompts @@ -37,7 +50,9 @@ export const classifyInquiryFlow = ai.defineFlow( async (input) => { try { console.log('Classifying inquiry:', input.inquiry); - const classificationResult = await classifyInquiryPrompt({ inquiry: input.inquiry }); + const classificationResult = await classifyInquiryPrompt({ + inquiry: input.inquiry, + }); return classificationResult.output; } catch (error) { console.error('Error in classifyInquiryFlow:', error); @@ -51,16 +66,18 @@ export const customerServiceFlow = ai.defineFlow( name: 'customerServiceFlow', inputSchema: z.object({ from: z.string(), - to: z.string(), + to: z.string(), subject: z.string(), body: z.string(), sentAt: z.string(), // Changed from timestamp to sentAt - threadHistory: z.array(z.object({ - from: z.string(), - to: z.string(), - body: z.string(), - sentAt: z.string(), // Changed from timestamp to sentAt - })), + threadHistory: z.array( + z.object({ + from: z.string(), + to: z.string(), + body: z.string(), + sentAt: z.string(), // Changed from timestamp to sentAt + }) + ), }), outputSchema: z.object({ intent: z.string(), @@ -76,12 +93,14 @@ export const customerServiceFlow = ai.defineFlow( to: input.to, subject: input.subject, body: input.body, - threadHistoryLength: input.threadHistory.length + threadHistoryLength: input.threadHistory.length, }); // Step 1: Classify the inquiry console.log('Step 1: Classifying inquiry...'); - const classificationResult = await classifyInquiryFlow({inquiry: input.body}); + const classificationResult = await classifyInquiryFlow({ + inquiry: input.body, + }); console.log('Classification result:', classificationResult); const { intent, subintent } = classificationResult; @@ -112,9 +131,16 @@ export const customerServiceFlow = ai.defineFlow( } catch (error) { console.error('Error executing handler:', error); // Escalate if no handler - if (error instanceof Error && error.message.startsWith('NoHandlerPromptError')) { + if ( + error instanceof Error && + error.message.startsWith('NoHandlerPromptError') + ) { console.log('No handler found, escalating to human...'); - const escalationResult = await escalateToHuman(input.body, input.from, 'No handler found'); + const escalationResult = await escalateToHuman( + input.body, + input.from, + 'No handler found' + ); console.log('Escalation result:', escalationResult); return { intent, @@ -172,8 +198,9 @@ async function escalateToHuman(inquiry: string, email: string, reason: string) { ); return { - message: "Your inquiry has been escalated to our customer service team. We'll get back to you as soon as possible.", - escalationId: escalation.id + message: + "Your inquiry has been escalated to our customer service team. We'll get back to you as soon as possible.", + escalationId: escalation.id, }; } @@ -197,7 +224,9 @@ export const augmentInfo = ai.defineFlow( responseData = { catalog: products }; break; case 'Product': - const productInfo = await extractInfoFlow({ inquiry: input.customerInquiry }); + const productInfo = await extractInfoFlow({ + inquiry: input.customerInquiry, + }); if (productInfo.productId) { const product = await getProductById(productInfo.productId); responseData = { product }; @@ -207,7 +236,9 @@ export const augmentInfo = ai.defineFlow( } break; case 'Order': - const orderInfo = await extractInfoFlow({ inquiry: input.customerInquiry }); + const orderInfo = await extractInfoFlow({ + inquiry: input.customerInquiry, + }); console.log(orderInfo); console.log('Extracted order info:', orderInfo); if (orderInfo.orderId) { @@ -225,7 +256,8 @@ export const augmentInfo = ai.defineFlow( break; } return { responseData }; - }) + } +); export const extractInfoFlow = ai.defineFlow( { @@ -241,16 +273,16 @@ export const extractInfoFlow = ai.defineFlow( }), }, async (input) => { - const extractionResult = await extractInfoPrompt({ - inquiry: input.inquiry, - category: 'Customer Service' + const extractionResult = await extractInfoPrompt({ + inquiry: input.inquiry, + category: 'Customer Service', }); const output = extractionResult.output; return { productId: output.productId ? parseInt(output.productId, 10) : 0, orderId: output.orderId ? parseInt(output.orderId, 10) : 0, customerId: output.customerId ? parseInt(output.customerId, 10) : 0, - issue: output.issue || "", + issue: output.issue || '', }; } ); @@ -299,4 +331,4 @@ export const generateDraftFlow = ai.defineFlow( }); return { draftResponse: responseResult.output.draftResponse }; } -); \ No newline at end of file +); diff --git a/samples/js-emailResponder/src/prisma/seed.ts b/samples/js-emailResponder/src/prisma/seed.ts index 3e7709c96..cfa8bf1e2 100644 --- a/samples/js-emailResponder/src/prisma/seed.ts +++ b/samples/js-emailResponder/src/prisma/seed.ts @@ -3,7 +3,6 @@ import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); async function main() { - // Seed Products with hardcoded values const products = []; const productData = [ @@ -46,13 +45,13 @@ async function main() { stockLevel: 30, price: 149.99, sku: 'WIR-HEAD-BK', - } + }, ]; for (const data of productData) { products.push( await prisma.product.create({ - data + data, }) ); } @@ -74,13 +73,13 @@ async function main() { id: 3, name: 'Bob Wilson', email: 'bob.wilson@example.com', - } + }, ]; for (const data of customerData) { customers.push( await prisma.customer.create({ - data + data, }) ); } @@ -100,7 +99,7 @@ async function main() { { productId: products[1].id, quantity: 1, - } + }, ], }, }, @@ -113,7 +112,7 @@ async function main() { { productId: products[2].id, quantity: 1, - } + }, ], }, }, @@ -130,15 +129,15 @@ async function main() { { productId: products[4].id, quantity: 1, - } + }, ], }, - } + }, ]; for (const data of orderData) { await prisma.order.create({ - data + data, }); } @@ -153,4 +152,3 @@ main() .finally(async () => { await prisma.$disconnect(); }); - \ No newline at end of file diff --git a/samples/js-emailResponder/tsconfig.json b/samples/js-emailResponder/tsconfig.json index cf2e01b49..a4d54289a 100644 --- a/samples/js-emailResponder/tsconfig.json +++ b/samples/js-emailResponder/tsconfig.json @@ -11,7 +11,7 @@ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ - "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ @@ -25,8 +25,8 @@ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ /* Modules */ - "module": "commonjs", /* Specify what module code is generated. */ - "rootDir": "./src", /* Specify the root folder within your source files. */ + "module": "commonjs" /* Specify what module code is generated. */, + "rootDir": "./src" /* Specify the root folder within your source files. */, // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ @@ -57,7 +57,7 @@ // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ // "noEmit": true, /* Disable emitting files from a compilation. */ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - "outDir": "./lib", /* Specify an output folder for all emitted files. */ + "outDir": "./lib" /* Specify an output folder for all emitted files. */, // "removeComments": true, /* Disable emitting comments. */ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ @@ -77,12 +77,12 @@ // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, /* Type Checking */ - "strict": true, /* Enable all strict type-checking options. */ + "strict": true /* Enable all strict type-checking options. */, // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ @@ -105,6 +105,6 @@ /* Completeness */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ } } From 8f979d95e9989556911c99680578e9d4b12b5b51 Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Thu, 12 Dec 2024 15:18:40 -0600 Subject: [PATCH 3/5] add copyright --- samples/js-emailResponder/genkit-tools.conf.js | 16 ++++++++++++++++ samples/js-emailResponder/src/db.ts | 16 ++++++++++++++++ samples/js-emailResponder/src/handlers.ts | 16 ++++++++++++++++ samples/js-emailResponder/src/index.ts | 16 ++++++++++++++++ samples/js-emailResponder/src/prisma/seed.ts | 16 ++++++++++++++++ 5 files changed, 80 insertions(+) diff --git a/samples/js-emailResponder/genkit-tools.conf.js b/samples/js-emailResponder/genkit-tools.conf.js index cd00b3f8d..35fcf0394 100644 --- a/samples/js-emailResponder/genkit-tools.conf.js +++ b/samples/js-emailResponder/genkit-tools.conf.js @@ -1,3 +1,19 @@ +/** + * 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: [ { diff --git a/samples/js-emailResponder/src/db.ts b/samples/js-emailResponder/src/db.ts index b28471634..504b8d1e4 100644 --- a/samples/js-emailResponder/src/db.ts +++ b/samples/js-emailResponder/src/db.ts @@ -1,3 +1,19 @@ +/** + * 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. + */ + import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); diff --git a/samples/js-emailResponder/src/handlers.ts b/samples/js-emailResponder/src/handlers.ts index fbbea0654..69044c039 100644 --- a/samples/js-emailResponder/src/handlers.ts +++ b/samples/js-emailResponder/src/handlers.ts @@ -1,3 +1,19 @@ +/** + * 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. + */ + import * as fs from 'fs'; import { z } from 'genkit'; import * as path from 'path'; diff --git a/samples/js-emailResponder/src/index.ts b/samples/js-emailResponder/src/index.ts index 947dbab47..0810fa8b1 100644 --- a/samples/js-emailResponder/src/index.ts +++ b/samples/js-emailResponder/src/index.ts @@ -1,3 +1,19 @@ +/** + * 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. + */ + import genkitEval, { GenkitMetric } from '@genkit-ai/evaluator'; import vertexAI, { gemini10Pro, diff --git a/samples/js-emailResponder/src/prisma/seed.ts b/samples/js-emailResponder/src/prisma/seed.ts index cfa8bf1e2..ff81571e3 100644 --- a/samples/js-emailResponder/src/prisma/seed.ts +++ b/samples/js-emailResponder/src/prisma/seed.ts @@ -1,3 +1,19 @@ +/** + * 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. + */ + import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); From 6423fc64994fa5c54ad9ea56454081553c184df3 Mon Sep 17 00:00:00 2001 From: huangjeff5 <64040981+huangjeff5@users.noreply.github.com> Date: Mon, 16 Dec 2024 17:22:02 -0600 Subject: [PATCH 4/5] Update README.md --- samples/js-emailResponder/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/js-emailResponder/README.md b/samples/js-emailResponder/README.md index d54a2e793..f34a0382a 100644 --- a/samples/js-emailResponder/README.md +++ b/samples/js-emailResponder/README.md @@ -16,8 +16,8 @@ Key features: Before you begin, make sure you have the following installed: -- Node.js (v14 or later) -- npm (v6 or later) +- Node.js (v20 or later) +- npm (v10.5.0 or later) - Genkit CLI ## Getting Started From dd45e2d63a88b181e524c58bd1c7fb86c785d1ef Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Wed, 18 Dec 2024 15:48:34 -0600 Subject: [PATCH 5/5] fix tsconfig.json --- samples/js-emailResponder/README.md | 12 +-- samples/js-emailResponder/tsconfig.json | 120 +++--------------------- 2 files changed, 18 insertions(+), 114 deletions(-) diff --git a/samples/js-emailResponder/README.md b/samples/js-emailResponder/README.md index f34a0382a..e82d515c7 100644 --- a/samples/js-emailResponder/README.md +++ b/samples/js-emailResponder/README.md @@ -10,7 +10,7 @@ Key features: - 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 +- Ready for deployment on Google Cloud Platform with Google AI integration ## Prerequisites @@ -36,15 +36,13 @@ Before you begin, make sure you have the following installed: ``` 3. Set up your database: - a. Set up your environment variables: - Create a `.env` file in the root directory and add the following: - + a. Export environment variables: ``` - PROJECT_ID=[YOUR PROJECT ID] - LOCATION=[YOUR LOCATION] + export PROJECT_ID=[YOUR PROJECT ID] + export LOCATION=[YOUR LOCATION] ``` - b. Set up the database and seed with sample data: + c. Set up the database and seed with sample data: ``` cd src/ diff --git a/samples/js-emailResponder/tsconfig.json b/samples/js-emailResponder/tsconfig.json index a4d54289a..b73ccd04d 100644 --- a/samples/js-emailResponder/tsconfig.json +++ b/samples/js-emailResponder/tsconfig.json @@ -1,110 +1,16 @@ { "compilerOptions": { - /* Visit https://aka.ms/tsconfig to read more about this file */ - - /* Projects */ - // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ - // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ - // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ - // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ - // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ - // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ - - /* Language and Environment */ - "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, - // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ - // "jsx": "preserve", /* Specify what JSX code is generated. */ - // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ - // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ - // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ - // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ - // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ - // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ - // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ - // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ - // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ - - /* Modules */ - "module": "commonjs" /* Specify what module code is generated. */, - "rootDir": "./src" /* Specify the root folder within your source files. */, - // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ - // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ - // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ - // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ - // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ - // "types": [], /* Specify type package names to be included without being referenced in a source file. */ - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ - // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ - // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ - // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ - // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ - // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ - // "resolveJsonModule": true, /* Enable importing .json files. */ - // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ - // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ - - /* JavaScript Support */ - // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ - // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ - // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ - - /* Emit */ - // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ - // "declarationMap": true, /* Create sourcemaps for d.ts files. */ - // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ - // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - // "noEmit": true, /* Disable emitting files from a compilation. */ - // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - "outDir": "./lib" /* Specify an output folder for all emitted files. */, - // "removeComments": true, /* Disable emitting comments. */ - // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ - // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ - // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ - // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ - // "newLine": "crlf", /* Set the newline character for emitting files. */ - // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ - // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ - // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ - // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ - // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ - - /* Interop Constraints */ - // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ - // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ - // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ - // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, - // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, - - /* Type Checking */ - "strict": true /* Enable all strict type-checking options. */, - // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ - // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ - // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ - // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ - // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ - // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ - // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ - // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ - // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ - // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ - // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ - // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ - // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ - // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ - // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ - // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ - // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ - // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ - // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ - - /* Completeness */ - // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ - } + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noImplicitReturns": true, + "noUnusedLocals": false, + "outDir": "lib", + "sourceMap": true, + "strict": true, + "target": "es2017", + "skipLibCheck": true, + "esModuleInterop": true + }, + "compileOnSave": true, + "include": ["src"] }