Skip to content

Commit

Permalink
chore(api): enable webhook for whatsapp
Browse files Browse the repository at this point in the history
  • Loading branch information
nicobytes committed Oct 27, 2024
1 parent 90984cc commit 452f12b
Show file tree
Hide file tree
Showing 22 changed files with 579 additions and 123 deletions.
1 change: 1 addition & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"@langchain/core": "^0.3.15",
"@langchain/langgraph": "^0.2.18",
"@langchain/mistralai": "^0.1.1",
"@langchain/openai": "^0.3.11",
"hono": "^4.6.7",
"zod": "^3.23.8"
},
Expand Down
3 changes: 3 additions & 0 deletions apps/api/src/bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ export type Bindings = {
CLOUDFLARE_API_TOKEN: string;
CLOUDFLARE_AI_GATEWAY_URL: string;
OPENAI_API_KEY: string;
MISTRAL_API_KEY: string;
AI: any;
BUCKET: R2Bucket;
R2_URL: string;
FB_TOKEN: string;
FB_VERIFY_TOKEN: string;
};
21 changes: 21 additions & 0 deletions apps/api/src/chatbot/getModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ChatMistralAI } from "@langchain/mistralai";
import { ChatOpenAI } from "@langchain/openai";

export const models = {
gpt4: (apiKey: string) => {
return new ChatOpenAI({
model: "gpt-4",
temperature: 0,
maxRetries: 2,
apiKey,
});
},
mistral: (apiKey: string) => {
return new ChatMistralAI({
model: "mistral-large-latest",
temperature: 0,
maxRetries: 2,
apiKey,
});
},
};
9 changes: 9 additions & 0 deletions apps/api/src/chatbot/graph.state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Annotation, MessagesAnnotation } from "@langchain/langgraph";
import { MyNodes } from "./nodes";

export const GraphState = Annotation.Root({
...MessagesAnnotation.spec,
lastAgent: Annotation<MyNodes>,
});

export type GraphState = typeof GraphState.State;
60 changes: 60 additions & 0 deletions apps/api/src/chatbot/graph.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { END, START, StateGraph } from "@langchain/langgraph";
import { MemorySaver } from "@langchain/langgraph";

import { GraphState } from "./graph.state";

import { MyNodes } from "./nodes";
import { toolNode } from "./nodes/tool.node";
import { availabilityNode } from "./nodes/availability.node";
import { bookingNode } from "./nodes/booking.node";
import { conversationalNode } from "./nodes/conversation.node";

import { checkerToolRouter } from "./routers/checkerTool.router";
import { availabilityRouter } from "./routers/availability.router";
import { toolToNodeRouter } from "./routers/toolToNode.router";
import { intentRouter } from "./routers/intent.router";
import { models } from "./getModel";

interface Props {
openAIKey: string;
mistralKey: string;
}

export const createGraph = (data: Props) => {
const memory = new MemorySaver();

const llmGpt4 = models.gpt4(data.openAIKey);
const llmMistral = models.mistral(data.mistralKey);

const workflow = new StateGraph(GraphState)
// nodes
.addNode(MyNodes.TOOLS, toolNode)
.addNode(MyNodes.AVAILABILITY, availabilityNode(llmGpt4))
.addNode(MyNodes.BOOKING, bookingNode(llmGpt4))
.addNode(MyNodes.CONVERSATION, conversationalNode(llmMistral))
// edges
.addEdge(MyNodes.CONVERSATION, END)
.addEdge(MyNodes.BOOKING, END)
// routers
.addConditionalEdges(MyNodes.TOOLS, toolToNodeRouter, [
MyNodes.AVAILABILITY,
MyNodes.BOOKING,
END,
])
.addConditionalEdges(MyNodes.AVAILABILITY, availabilityRouter(llmMistral), [
MyNodes.TOOLS,
MyNodes.BOOKING,
END,
])
.addConditionalEdges(MyNodes.BOOKING, checkerToolRouter, [
MyNodes.TOOLS,
END,
])
.addConditionalEdges(START, intentRouter(llmMistral), [
MyNodes.AVAILABILITY,
MyNodes.BOOKING,
MyNodes.CONVERSATION,
]);

return workflow.compile({ checkpointer: memory });
};
50 changes: 0 additions & 50 deletions apps/api/src/chatbot/graphs/react.ts

This file was deleted.

41 changes: 0 additions & 41 deletions apps/api/src/chatbot/graphs/simple.ts

This file was deleted.

47 changes: 45 additions & 2 deletions apps/api/src/chatbot/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,50 @@ import type { App } from "@src/types";
const app = new Hono<App>();

app.get("/", (c) => c.text("hello from chatbot"));
app.get("/webhook", (c) => c.json("list webhook"));
app.post("/webhook", (c) => c.json("proxy", 201));

app.get("/webhook", (c) => {
const mode = c.req.query("hub.mode");
const token = c.req.query("hub.verify_token");
const challenge = c.req.query("hub.challenge");
if (mode === "subscribe" && token === c.env.FB_VERIFY_TOKEN) {
c.status(200);
return c.text(challenge || "ok");
} else {
c.status(403);
return c.text("Forbidden");
}
});

app.post("/webhook", async (c) => {
const body = await c.req.json();
const message = body.entry?.[0]?.changes[0]?.value?.messages?.[0];

if (message?.type === "text") {
const business_phone_number_id =
body.entry?.[0].changes?.[0].value?.metadata?.phone_number_id;

const url = `https://graph.facebook.com/v20.0/${business_phone_number_id}/messages`;

const headers = new Headers();
headers.append("Authorization", `Bearer ${c.env.FB_TOKEN}`);
headers.append("Content-Type", "application/json");

const response = await fetch(url, {
method: "POST",
headers,
body: JSON.stringify({
messaging_product: "whatsapp",
to: message.from,
text: { body: "Echo: " + message.text.body },
}),
});

if (response.status !== 200) {
console.error(response.statusText);
}
}

c.status(200);
});

export default app;
40 changes: 40 additions & 0 deletions apps/api/src/chatbot/nodes/availability.node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { formatDate } from "date-fns";
import { checkAvailabilityTool } from "../tools/checkAvailability.tool";

import { GraphState } from "../graph.state";
import { SystemMessage } from "@langchain/core/messages";
import { MyNodes } from ".";
import { ChatOpenAI } from "@langchain/openai";

const SYSTEM_PROMPT = (formattedDate: string) => {
return `You are expert assistant for checking the availability of Sander's, a hairdressing salon in Cochabamba (Bolivia). To help customers check availability for appointments, you ask them for the date they would like to book an appointment.
# RULES
- As reference, today is ${formattedDate}.
- Don't assume parameters in call functions that it didnt say.
- MUST NOT force users how to write. Let them write in the way they want.
- The conversation should be very natural like a secretary talking with a client.
- Call only ONE tool at a time.
- Your responses must be in spanish.`;
};

export const availabilityNode = (llm: ChatOpenAI) => {
return async (state: GraphState) => {
const { messages } = state;

const formattedDate = formatDate(new Date(), "yyyy-MM-dd");
const systemPrompt = SYSTEM_PROMPT(formattedDate);
llm.bindTools([checkAvailabilityTool]);

let trimmedHistory = messages;
if (trimmedHistory.at(-1)?.getType() === "ai") {
trimmedHistory = trimmedHistory.slice(0, -1);
}

const response = await llm.invoke([
new SystemMessage(systemPrompt),
...trimmedHistory.filter((m) => m.getType() !== "system"),
]);
return { messages: response, lastAgent: MyNodes.AVAILABILITY };
};
};
43 changes: 43 additions & 0 deletions apps/api/src/chatbot/nodes/booking.node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { SystemMessage } from "@langchain/core/messages";

import { MyNodes } from ".";
import { GraphState } from "../graph.state";
import { models } from "../getModel";
import { bookAppointmentTool } from "../tools/bookAppointment.tool";
import { ChatOpenAI } from "@langchain/openai";

const SYSTEM_PROMPT = () => {
return `You are Expert Assistant for booking an appointment at Sander's, a hairdressing salon in Cochabamba (Bolivia).
To book an appointment, you ask for the date and the name of the client
# RULES
- Recognize previously mentioned information
- Do not assume parameters in calling functions that it does not say.
- MUST NOT force users how to write. Let them write the way they want.
- The conversation should be very natural, like a secretary talking to a client.
- Only call ONE tool at a time.
- Your answers must be in Spanish.`;
};

export const bookingNode = (llm: ChatOpenAI) => {
return async (state: GraphState) => {
const { messages } = state;

const systemPrompt = SYSTEM_PROMPT();
llm.bindTools([bookAppointmentTool]);

let trimmedHistory = messages;
if (trimmedHistory.at(-1)?.getType() === "ai") {
trimmedHistory = trimmedHistory.slice(0, -1);
}

const response = await llm.invoke([
new SystemMessage(systemPrompt),
...trimmedHistory.filter((m) => m.getType() !== "system"),
]);
return {
messages: response,
lastAgent: MyNodes.BOOKING,
};
};
};
36 changes: 36 additions & 0 deletions apps/api/src/chatbot/nodes/conversation.node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { ChatMistralAI } from "@langchain/mistralai";
import { MyNodes } from ".";
import { GraphState } from "../graph.state";

const SUPPORT_PROMPT = `You are frontline support for Sander's, a hair salon in Cochabamba (Bolivia).
You can chat with customers and help them with basic questions, do not try to answer the question directly or gather information.
# RULES
- Be concise in your responses.
- Don't assume parameters in call functions that it didnt say.
- MUST NOT force users how to write. Let them write in the way they want.
- The conversation should be very natural like a secretary talking with a client.
- Call only ONE tool at a time.
- Your responses must be in spanish.
- Keep a friendly, professional tone.
- Avoid verbosity.
- Don't mention the name of the team you are transferring the user to.
# INTRODUCE YOURSELF
Hola, soy SanderBot, tu asistente virtual. ¿En qué puedo ayudarte hoy?
`;

export const conversationalNode = (llm: ChatMistralAI) => {
return async (state: GraphState) => {
const { messages } = state;
const response = await llm.invoke([
{ role: "system", content: SUPPORT_PROMPT },
...messages,
]);

return {
messages: response,
lastAgent: MyNodes.CONVERSATION,
};
};
};
6 changes: 6 additions & 0 deletions apps/api/src/chatbot/nodes/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export enum MyNodes {
CONVERSATION = "conversation",
BOOKING = "booking",
AVAILABILITY = "availability",
TOOLS = "tools",
}
9 changes: 9 additions & 0 deletions apps/api/src/chatbot/nodes/tool.node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ToolNode } from "@langchain/langgraph/prebuilt";

import { checkAvailabilityTool } from "../tools/checkAvailability.tool";
import { bookAppointmentTool } from "../tools/bookAppointment.tool";

export const toolNode = new ToolNode([
checkAvailabilityTool,
bookAppointmentTool,
]);
Loading

0 comments on commit 452f12b

Please sign in to comment.