Skip to content

Commit

Permalink
Merge pull request #1 from Kr1sh1/dev1
Browse files Browse the repository at this point in the history
Migrate codebase to TypeScript
  • Loading branch information
Kr1sh1 authored Mar 1, 2024
2 parents c13c0b6 + 4af9b32 commit b62b4e8
Show file tree
Hide file tree
Showing 15 changed files with 459 additions and 2,530 deletions.
13 changes: 0 additions & 13 deletions .babelrc

This file was deleted.

12 changes: 6 additions & 6 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: 18
- name: Install dependencies for the twilio function
- name: Install dependencies
run: npm install
- name: twilio-cli
- name: Compile TypeScript files to JavaScript
run: npm run build
- name: Install Twilio CLI
run: npm install -g twilio-cli
- name: twilio serverless
- name: Install Twilio serverless plugin
run: twilio plugins:install @twilio-labs/plugin-serverless@v3
- name: Transpile with Babel ESM syntax to CJS
run: npm run transpile
- name: twilio serverless:deploy
- name: Deploy
env:
TWILIO_ACCOUNT_SID: ${{ secrets.TWILIO_ACCOUNT_SID }}
TWILIO_API_KEY: ${{ secrets.TWILIO_API_KEY }}
Expand Down
22 changes: 22 additions & 0 deletions .github/workflows/pr-workflow.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: PR Workflow

on:
pull_request:
branches:
- main
types: [opened, synchronize, reopened, edited]

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 18
- name: Install dependencies
run: npm install
- name: Run Tests
run: npm run build
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Twilio Serverless
.twiliodeployinfo
compiled/**/*

# Created by https://www.toptal.com/developers/gitignore/api/node,git,macos,linux,windows,intellij
# Edit at https://www.toptal.com/developers/gitignore?templates=node,git,macos,linux,windows,intellij
Expand Down Expand Up @@ -154,7 +155,8 @@ fabric.properties
.LSOverride

# Icon must end with two \r
Icon
Icon


# Thumbnails
._*
Expand Down
3 changes: 2 additions & 1 deletion .twilioserverlessrc
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"loadSystemEnv": true,
"env": ".env.twilio_environment",
"overrideExistingProject": true,
"serviceName": "HillingdonLexTwilio"
"serviceName": "HillingdonLexTwilio",
"functionsFolder": "compiled"
}
}
}
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
## Project Structure
Twilio Functions is a serverless environment.

Every JavaScript function under `/functions` is deployed individually to a unique URL. An extension of `.protected.js` ensures the deployed function is only invokable by Twilio and not externally. Each function can reference each other relatively from under the `/functions` directory.
Every TypeScript function under `/functions` is deployed individually to a unique URL. An extension of `.protected.ts` ensures the deployed function is only invokable by Twilio webhooks. Each function can reference each other relatively from under the `/functions` directory.

The `transcribe` function is invoked when an inbound phone call is received, this behaviour is currently hardcoded in the deploy workflow but can become configurable.

Expand All @@ -21,7 +21,13 @@ user@computer:~$ npm install

### Debugging

The best way to debug right now is to deploy your code and see if it fails. Then make a phone call for the respective branch. If any errors occur, inspect the logs on the Twilio Console.
TypeScript can save you from the most common bugs. Run the following command to see if your files compile successfully. The GitHub Actions deploy workflow runs this anyway and will fail to deploy if there are errors, but you can run it locally too and save yourself time.

```console
user@computer:~$ npm run build
```

For runtime errors, the best way to debug right now is to deploy your code and see if it fails. Then make a phone call for the respective branch. If any errors occur, inspect the logs on the Twilio Console.

Eventually we'll add a testing framework you can run locally.

Expand Down
85 changes: 43 additions & 42 deletions functions/respond.protected.js → functions/respond.protected.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,26 @@
import OpenAI from "openai";
import { twiml, Response } from 'twilio';

import { S3Client, PutObjectCommand, GetObjectCommand } from "@aws-sdk/client-s3";
import { ServerlessFunctionSignature } from '@twilio-labs/serverless-runtime-types/types';
import { RespondServerlessEventObject, TwilioEnvironmentVariables } from './types/interfaces';

enum Role {
SYSTEM = "system",
ASSISTANT = "assistant",
USER = "user",
}

interface Message {
role: Role;
content: string;
}

export async function handler (context, event, callback) {
type Conversation = Message[];

export const handler: ServerlessFunctionSignature<TwilioEnvironmentVariables, RespondServerlessEventObject> = async function(
context,
event,
callback
) {
const openai = new OpenAI({ apiKey: context.OPENAI_API_KEY });
const s3Client = new S3Client(
{
Expand All @@ -14,38 +31,39 @@ export async function handler (context, event, callback) {
}
}
);
const twiml_response = new twiml.VoiceResponse();
const response = new Response();
const twiml_response = new Twilio.twiml.VoiceResponse();
const response = new Twilio.Response();

const cookieValue = event.request.cookies.convo;
const conversation = cookieValue ?
JSON.parse(decodeURIComponent(cookieValue)) :
const cookies = event.request.cookies;
const conversation: Conversation = cookies.convo ?
JSON.parse(decodeURIComponent(cookies.convo)) :
[];

const userLog = {
role: "user",
role: Role.USER,
content: event.SpeechResult,
};

conversation.push(userLog);

const aiResponse = await generateAIResponse(conversation);
if (!aiResponse) return;
const cleanedAiResponse = aiResponse.replace(/^\w+:\s*/i, "").trim();

const assistantLog = {
role: "assistant",
role: Role.ASSISTANT,
content: cleanedAiResponse,
};

conversation.push(assistantLog);

let logFileName;
if (!event.request.cookies.logFileName) {
const callStartTimestamp = decodeURIComponent(event.request.cookies.callStartTimestamp)
logFileName = `${event.From || event.phoneNumber}_${callStartTimestamp}.json`
if (!cookies.logFileName) {
const callStartTimestamp = decodeURIComponent(cookies.callStartTimestamp)
logFileName = `${event.From}_${callStartTimestamp}.json`
response.setCookie('logFileName', encodeURIComponent(logFileName), ['Path=/']);
} else {
logFileName = decodeURIComponent(event.request.cookies.logFileName)
logFileName = decodeURIComponent(cookies.logFileName)
}

await uploadToS3([userLog, assistantLog], logFileName);
Expand Down Expand Up @@ -74,28 +92,28 @@ export async function handler (context, event, callback) {
);
response.setCookie("convo", newCookieValue, ["Path=/"]);

callback(null, response);
return callback(null, response);

async function generateAIResponse(conversation) {
async function generateAIResponse(conversation: Conversation) {
const messages = formatConversation(conversation);
return await createChatCompletion(messages);
}

function formatConversation(conversation) {
function formatConversation(conversation: Conversation) {
const messages = [{
role: "system",
role: Role.SYSTEM,
content: "You are a creative, funny, friendly and amusing AI assistant named Joanna. Please provide engaging but concise responses.",
},
{
role: "user",
role: Role.USER,
content: "We are having a casual conversation over the telephone so please provide engaging but concise responses.",
},
];

return messages.concat(conversation);
}

async function createChatCompletion(messages) {
async function createChatCompletion(messages: Conversation) {
try {
const completion = await openai.chat.completions.create({
model: "gpt-3.5-turbo",
Expand All @@ -104,31 +122,14 @@ export async function handler (context, event, callback) {
max_tokens: 100,
});

if (completion.status === 500) {
console.error("Error: OpenAI API returned a 500 status code.");
twiml_response.say({
voice: "Polly.Joanna-Neural",
},
"Oops, looks like I got an error from the OpenAI API on that request. Let's try that again."
);
twiml_response.redirect({
method: "POST",
},
`/transcribe`
);
response.appendHeader("Content-Type", "application/xml");
response.setBody(twiml_response.toString());
callback(null, response);
}

return completion.choices[0].message.content;
} catch (error) {
if (error.code === "ETIMEDOUT" || error.code === "ESOCKETTIMEDOUT") {
console.error("Error: OpenAI API request timed out.");
if (error instanceof OpenAI.APIError) {
console.error("Error: OpenAI API request errored out.");
twiml_response.say({
voice: "Polly.Joanna-Neural",
},
"I'm sorry, but it's taking me a little bit too long to respond. Let's try that again, one more time."
"I'm sorry, something went wrong. Let's try that again, one more time."
);
twiml_response.redirect({
method: "POST",
Expand All @@ -137,15 +138,15 @@ export async function handler (context, event, callback) {
);
response.appendHeader("Content-Type", "application/xml");
response.setBody(twiml_response.toString());
callback(null, response);
return callback(null, response);
} else {
console.error("Error during OpenAI API request:", error);
throw error;
}
}
}

async function uploadToS3(logs, logFileName) {
async function uploadToS3(logs: Conversation, logFileName: string) {
const bucketName = 'engelbartchatlogs1';

const existingContent = await s3Client.send(
Expand Down
12 changes: 0 additions & 12 deletions functions/statusCallback.protected.js

This file was deleted.

19 changes: 19 additions & 0 deletions functions/statusCallback.protected.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { ServerlessFunctionSignature } from '@twilio-labs/serverless-runtime-types/types';
import { StatusCallbackServerlessEventObject, TwilioEnvironmentVariables } from './types/interfaces';

export const handler: ServerlessFunctionSignature<TwilioEnvironmentVariables, StatusCallbackServerlessEventObject> = function(
context,
event,
callback,
) {
if (event.CallStatus === "completed") {
const callerNumber = event.From
const logFileName = decodeURIComponent(event.request.cookies.logFileName) // Could be 'null' if the call was terminated too early
const callStartTimestamp = decodeURIComponent(event.request.cookies.callStartTimestamp)
const callEndTimestamp = event.Timestamp
const callDurationInSeconds = event.CallDuration
// Send into AWS RDS
}
const response = new Response();
return callback(null, response)
}
42 changes: 0 additions & 42 deletions functions/transcribe.protected.js

This file was deleted.

Loading

0 comments on commit b62b4e8

Please sign in to comment.