From 452f12b8a3515dd4ee0ba84db1140fbde4328de6 Mon Sep 17 00:00:00 2001 From: Nicolas Molina Date: Sat, 26 Oct 2024 23:39:22 -0400 Subject: [PATCH] chore(api): enable webhook for whatsapp --- apps/api/package.json | 1 + apps/api/src/bindings.ts | 3 + apps/api/src/chatbot/getModel.ts | 21 +++ apps/api/src/chatbot/graph.state.ts | 9 ++ apps/api/src/chatbot/graph.ts | 60 ++++++++ apps/api/src/chatbot/graphs/react.ts | 50 ------- apps/api/src/chatbot/graphs/simple.ts | 41 ------ apps/api/src/chatbot/index.ts | 47 ++++++- .../src/chatbot/nodes/availability.node.ts | 40 ++++++ apps/api/src/chatbot/nodes/booking.node.ts | 43 ++++++ .../src/chatbot/nodes/conversation.node.ts | 36 +++++ apps/api/src/chatbot/nodes/index.ts | 6 + apps/api/src/chatbot/nodes/tool.node.ts | 9 ++ .../chatbot/routers/availability.router.ts | 48 +++++++ .../src/chatbot/routers/checkerTool.router.ts | 17 +++ apps/api/src/chatbot/routers/intent.router.ts | 64 +++++++++ .../src/chatbot/routers/toolToNode.router.ts | 10 ++ .../src/chatbot/tools/bookAppointment.tool.ts | 18 +++ .../chatbot/tools/checkAvailability.tool.ts | 18 +++ apps/api/src/chatbot/tools/cities.tool.ts | 13 -- apps/api/src/chatbot/tools/wheater.tool.ts | 16 --- apps/api/yarn.lock | 132 +++++++++++++++++- 22 files changed, 579 insertions(+), 123 deletions(-) create mode 100644 apps/api/src/chatbot/getModel.ts create mode 100644 apps/api/src/chatbot/graph.state.ts create mode 100644 apps/api/src/chatbot/graph.ts delete mode 100644 apps/api/src/chatbot/graphs/react.ts delete mode 100644 apps/api/src/chatbot/graphs/simple.ts create mode 100644 apps/api/src/chatbot/nodes/availability.node.ts create mode 100644 apps/api/src/chatbot/nodes/booking.node.ts create mode 100644 apps/api/src/chatbot/nodes/conversation.node.ts create mode 100644 apps/api/src/chatbot/nodes/index.ts create mode 100644 apps/api/src/chatbot/nodes/tool.node.ts create mode 100644 apps/api/src/chatbot/routers/availability.router.ts create mode 100644 apps/api/src/chatbot/routers/checkerTool.router.ts create mode 100644 apps/api/src/chatbot/routers/intent.router.ts create mode 100644 apps/api/src/chatbot/routers/toolToNode.router.ts create mode 100644 apps/api/src/chatbot/tools/bookAppointment.tool.ts create mode 100644 apps/api/src/chatbot/tools/checkAvailability.tool.ts delete mode 100644 apps/api/src/chatbot/tools/cities.tool.ts delete mode 100644 apps/api/src/chatbot/tools/wheater.tool.ts diff --git a/apps/api/package.json b/apps/api/package.json index 7293ede..52793ec 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -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" }, diff --git a/apps/api/src/bindings.ts b/apps/api/src/bindings.ts index dcc1eba..caed0e6 100644 --- a/apps/api/src/bindings.ts +++ b/apps/api/src/bindings.ts @@ -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; }; diff --git a/apps/api/src/chatbot/getModel.ts b/apps/api/src/chatbot/getModel.ts new file mode 100644 index 0000000..4eddfd8 --- /dev/null +++ b/apps/api/src/chatbot/getModel.ts @@ -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, + }); + }, +}; diff --git a/apps/api/src/chatbot/graph.state.ts b/apps/api/src/chatbot/graph.state.ts new file mode 100644 index 0000000..1f3ca0b --- /dev/null +++ b/apps/api/src/chatbot/graph.state.ts @@ -0,0 +1,9 @@ +import { Annotation, MessagesAnnotation } from "@langchain/langgraph"; +import { MyNodes } from "./nodes"; + +export const GraphState = Annotation.Root({ + ...MessagesAnnotation.spec, + lastAgent: Annotation, +}); + +export type GraphState = typeof GraphState.State; diff --git a/apps/api/src/chatbot/graph.ts b/apps/api/src/chatbot/graph.ts new file mode 100644 index 0000000..52638be --- /dev/null +++ b/apps/api/src/chatbot/graph.ts @@ -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 }); +}; diff --git a/apps/api/src/chatbot/graphs/react.ts b/apps/api/src/chatbot/graphs/react.ts deleted file mode 100644 index 322c819..0000000 --- a/apps/api/src/chatbot/graphs/react.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { END, START, StateGraph, Annotation, MessagesAnnotation } from "@langchain/langgraph"; -import { ToolNode } from '@langchain/langgraph/prebuilt'; - -import { getWeather } from '@chatbot/tools/wheater.tool'; -import { getCoolestCities } from '@chatbot/tools/cities.tool'; - -import { ChatMistralAI } from "@langchain/mistralai"; - -console.log() - -const tools = [getWeather, getCoolestCities]; - -const llm = new ChatMistralAI({ - model: "mistral-large-latest", - temperature: 0, - maxRetries: 2, -}).bindTools(tools); - -const toolNode = new ToolNode(tools); - -const GraphState = Annotation.Root({ - ...MessagesAnnotation.spec, -}); - -type GraphState = typeof GraphState.State; - -const shouldContinue = (state: GraphState) => { - const { messages } = state; - const lastMessage = messages[messages.length - 1]; - if ("tool_calls" in lastMessage && Array.isArray(lastMessage.tool_calls) && lastMessage.tool_calls?.length) { - return "tools"; - } - return END; - } - - const callModel = async (state: GraphState) => { - const { messages } = state; - const response = await llm.invoke(messages); - return { messages: response }; - } - -// Define a new graph -const workflow = new StateGraph(MessagesAnnotation) - .addNode("agent", callModel) - .addNode("tools", toolNode) - .addEdge(START, "agent") - .addConditionalEdges("agent", shouldContinue, ["tools", END]) - .addEdge("tools", "agent"); - -export const graph = workflow.compile(); diff --git a/apps/api/src/chatbot/graphs/simple.ts b/apps/api/src/chatbot/graphs/simple.ts deleted file mode 100644 index 71dc5f2..0000000 --- a/apps/api/src/chatbot/graphs/simple.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { END, START, StateGraph, Annotation } from "@langchain/langgraph" - -const GraphState = Annotation.Root({ - message: Annotation(), -}); - -type GraphState = typeof GraphState.State; - -const node1 = async (state: GraphState) => { - const currentMessage = state.message; - return { message: `${currentMessage} I am` }; -}; - -const node2 = async (state: GraphState) => { - const currentMessage = state.message; - return { message: `${currentMessage} happy!` }; -}; - -const node3 = async (state: GraphState) => { - const currentMessage = state.message; - return { message: `${currentMessage} sad!` }; -}; - -const decideMood = (state: GraphState): string => { - if (Math.random() < 0.5) { - return "node3"; - } - return "node2"; -}; - -// Define a new graph -const workflow = new StateGraph(GraphState) - .addNode("node1", node1) - .addNode("node2", node2) - .addNode("node3", node3) - .addEdge(START, "node1") - .addConditionalEdges("node1", decideMood) - .addEdge("node2", END) - .addEdge("node3", END); - -export const graph = workflow.compile(); diff --git a/apps/api/src/chatbot/index.ts b/apps/api/src/chatbot/index.ts index 1a5d655..e9b4adb 100644 --- a/apps/api/src/chatbot/index.ts +++ b/apps/api/src/chatbot/index.ts @@ -4,7 +4,50 @@ import type { App } from "@src/types"; const app = new Hono(); 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; diff --git a/apps/api/src/chatbot/nodes/availability.node.ts b/apps/api/src/chatbot/nodes/availability.node.ts new file mode 100644 index 0000000..385912a --- /dev/null +++ b/apps/api/src/chatbot/nodes/availability.node.ts @@ -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 }; + }; +}; diff --git a/apps/api/src/chatbot/nodes/booking.node.ts b/apps/api/src/chatbot/nodes/booking.node.ts new file mode 100644 index 0000000..263c84b --- /dev/null +++ b/apps/api/src/chatbot/nodes/booking.node.ts @@ -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, + }; + }; +}; diff --git a/apps/api/src/chatbot/nodes/conversation.node.ts b/apps/api/src/chatbot/nodes/conversation.node.ts new file mode 100644 index 0000000..a110207 --- /dev/null +++ b/apps/api/src/chatbot/nodes/conversation.node.ts @@ -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, + }; + }; +}; diff --git a/apps/api/src/chatbot/nodes/index.ts b/apps/api/src/chatbot/nodes/index.ts new file mode 100644 index 0000000..81c48e2 --- /dev/null +++ b/apps/api/src/chatbot/nodes/index.ts @@ -0,0 +1,6 @@ +export enum MyNodes { + CONVERSATION = "conversation", + BOOKING = "booking", + AVAILABILITY = "availability", + TOOLS = "tools", +} diff --git a/apps/api/src/chatbot/nodes/tool.node.ts b/apps/api/src/chatbot/nodes/tool.node.ts new file mode 100644 index 0000000..8e4c103 --- /dev/null +++ b/apps/api/src/chatbot/nodes/tool.node.ts @@ -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, +]); diff --git a/apps/api/src/chatbot/routers/availability.router.ts b/apps/api/src/chatbot/routers/availability.router.ts new file mode 100644 index 0000000..195948b --- /dev/null +++ b/apps/api/src/chatbot/routers/availability.router.ts @@ -0,0 +1,48 @@ +import { END } from "@langchain/langgraph"; +import { GraphState } from "../graph.state"; +import { MyNodes } from "../nodes"; +import { HumanMessage, SystemMessage } from "@langchain/core/messages"; +import { ChatMistralAI } from "@langchain/mistralai"; + +const SYSTEM_TEMPLATE = `You are a customer support to determine information to book and appointment expert. +Your job is to determine if a customer is ready to book and appointment when the user give date and time to book.`; + +const HUMAN_TEMPLATE = `The previous conversation is an interaction between a customer service representative and a user. +Retrieve whether there is a date and time to book an appointment in the conversation, +Respond with a JSON object containing a single key called "isReadyToBook" with one of the following values: true if the appropriate values exist to book, otherwise false.`; + +export const availabilityRouter = (llm: ChatMistralAI) => { + return async (state: GraphState) => { + const { messages } = state; + const lastMessage = messages.at(-1); + if ( + lastMessage && + "tool_calls" in lastMessage && + Array.isArray(lastMessage.tool_calls) && + lastMessage.tool_calls?.length + ) { + return MyNodes.TOOLS; + } + + const response = await llm.invoke( + [ + new SystemMessage(SYSTEM_TEMPLATE), + ...state.messages, + new HumanMessage(HUMAN_TEMPLATE), + ], + { + response_format: { + type: "json_object", + }, + }, + ); + const output = JSON.parse(response.content as string); + const isReadyToBook = output.isReadyToBook; + + if (isReadyToBook) { + return MyNodes.BOOKING; + } + + return END; + }; +}; diff --git a/apps/api/src/chatbot/routers/checkerTool.router.ts b/apps/api/src/chatbot/routers/checkerTool.router.ts new file mode 100644 index 0000000..7fe5ef7 --- /dev/null +++ b/apps/api/src/chatbot/routers/checkerTool.router.ts @@ -0,0 +1,17 @@ +import { END } from "@langchain/langgraph"; +import { GraphState } from "../graph.state"; +import { MyNodes } from "../nodes"; + +export const checkerToolRouter = (state: GraphState) => { + const { messages } = state; + const lastMessage = messages.at(-1); + if ( + lastMessage && + "tool_calls" in lastMessage && + Array.isArray(lastMessage.tool_calls) && + lastMessage.tool_calls?.length + ) { + return MyNodes.TOOLS; + } + return END; +}; diff --git a/apps/api/src/chatbot/routers/intent.router.ts b/apps/api/src/chatbot/routers/intent.router.ts new file mode 100644 index 0000000..025b3ca --- /dev/null +++ b/apps/api/src/chatbot/routers/intent.router.ts @@ -0,0 +1,64 @@ +import { GraphState } from "../graph.state"; +import { models } from "../getModel"; +import { MyNodes } from "../nodes"; +import { ChatMistralAI } from "@langchain/mistralai"; + +const CATEGORIZATION_SYSTEM_TEMPLATE = `You are a customer support routing expert. +Your job is to determine if a customer support representative is routing a user to the: + +- Booking or Scheduling Team +- Rescheduling team +- Cancellation team + +if the user's request is not related to the previous team and is just a conversational response.`; + +const CATEGORIZATION_HUMAN_TEMPLATE = `The previous conversation is an interaction between a customer support representative and a user. +Extract whether the representative is routing the user to a support team, or whether they are just responding conversationally. +Respond with a JSON object containing a single key called "nextRepresentative" with one of the following values: + +If they want to route the user to the Booking or Appointment team, respond only with the word "Booking". +If they want to route the user to the Rescheduling team, respond only with the word "Rescheduling". +If they want to route the user to the Canceling team, respond only with the word "Canceling". +Otherwise, respond only with the word "Conversation".`; + +export const intentRouter = (llm: ChatMistralAI) => { + return async (state: GraphState) => { + const { lastAgent } = state; + // TODO: + const isReadyToBook = false; + + if (lastAgent === MyNodes.AVAILABILITY || lastAgent === MyNodes.BOOKING) { + return lastAgent; + } + + const categorizationResponse = await llm.invoke( + [ + { + role: "system", + content: CATEGORIZATION_SYSTEM_TEMPLATE, + }, + ...state.messages, + { + role: "user", + content: CATEGORIZATION_HUMAN_TEMPLATE, + }, + ], + { + response_format: { + type: "json_object", + }, + }, + ); + const categorizationOutput = JSON.parse( + categorizationResponse.content as string, + ); + + const intent = categorizationOutput.nextRepresentative; + if (intent.includes("Booking") && isReadyToBook) { + return MyNodes.BOOKING; + } else if (intent.includes("Booking") && !isReadyToBook) { + return MyNodes.AVAILABILITY; + } + return MyNodes.CONVERSATION; + }; +}; diff --git a/apps/api/src/chatbot/routers/toolToNode.router.ts b/apps/api/src/chatbot/routers/toolToNode.router.ts new file mode 100644 index 0000000..afdba8c --- /dev/null +++ b/apps/api/src/chatbot/routers/toolToNode.router.ts @@ -0,0 +1,10 @@ +import { END } from "@langchain/langgraph"; +import { GraphState } from "../graph.state"; + +export const toolToNodeRouter = (state: GraphState) => { + const { lastAgent } = state; + if (lastAgent) { + return lastAgent; + } + return END; +}; diff --git a/apps/api/src/chatbot/tools/bookAppointment.tool.ts b/apps/api/src/chatbot/tools/bookAppointment.tool.ts new file mode 100644 index 0000000..cbb2fbf --- /dev/null +++ b/apps/api/src/chatbot/tools/bookAppointment.tool.ts @@ -0,0 +1,18 @@ +import { tool } from "@langchain/core/tools"; +import { z } from "zod"; + +export const bookAppointmentTool = tool( + async (input) => { + const { date, customerName } = input; + return `Appointment was scheduled for ${customerName} on ${date} 😋`; + }, + { + name: "book_appointment", + description: + "Book an appointment for a date and time, and include the client's name", + schema: z.object({ + date: z.string().datetime().describe("booking date"), + customerName: z.string().describe("client name"), + }), + }, +); diff --git a/apps/api/src/chatbot/tools/checkAvailability.tool.ts b/apps/api/src/chatbot/tools/checkAvailability.tool.ts new file mode 100644 index 0000000..6d48ba8 --- /dev/null +++ b/apps/api/src/chatbot/tools/checkAvailability.tool.ts @@ -0,0 +1,18 @@ +import { tool } from "@langchain/core/tools"; +import { z } from "zod"; + +export const checkAvailabilityTool = tool( + async (input) => { + const { desiredDate } = input; + + return `This availability for ${desiredDate} is as follows: + Available slots: 9am, 10am, 11am, 12pm, 1pm, 2pm, 3pm, 4pm, 5pm`; + }, + { + name: "check_availability", + description: "Check availability for a desired date", + schema: z.object({ + desiredDate: z.string().date().describe("desired date"), + }), + }, +); diff --git a/apps/api/src/chatbot/tools/cities.tool.ts b/apps/api/src/chatbot/tools/cities.tool.ts deleted file mode 100644 index 65f8fba..0000000 --- a/apps/api/src/chatbot/tools/cities.tool.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { tool } from '@langchain/core/tools'; -import { z } from 'zod'; - - -export const getCoolestCities = tool(() => { - return 'nyc, sf'; - }, { - name: 'get_coolest_cities', - description: 'Get a list of coolest cities', - schema: z.object({ - noOp: z.string().optional().describe("No-op parameter."), - }) - }) \ No newline at end of file diff --git a/apps/api/src/chatbot/tools/wheater.tool.ts b/apps/api/src/chatbot/tools/wheater.tool.ts deleted file mode 100644 index 8d26c2b..0000000 --- a/apps/api/src/chatbot/tools/wheater.tool.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { tool } from '@langchain/core/tools'; -import { z } from 'zod'; - -export const getWeather = tool((input) => { - if (['sf', 'san francisco'].includes(input.location.toLowerCase())) { - return 'It\'s 60 degrees and foggy.'; - } else { - return 'It\'s 90 degrees and sunny.'; - } -}, { - name: 'get_weather', - description: 'Call to get the current weather.', - schema: z.object({ - location: z.string().describe("Location to get the weather for."), - }) -}); diff --git a/apps/api/yarn.lock b/apps/api/yarn.lock index b8af1c5..f47e1c8 100644 --- a/apps/api/yarn.lock +++ b/apps/api/yarn.lock @@ -298,6 +298,16 @@ zod "^3.22.4" zod-to-json-schema "^3.22.4" +"@langchain/openai@^0.3.11": + version "0.3.11" + resolved "https://registry.npmjs.org/@langchain/openai/-/openai-0.3.11.tgz#c93ee298a87318562a1da6c2915a180fe5155ac4" + integrity sha512-mEFbpJ8w8NPArsquUlCwxvZTKNkXxqwzvTEYzv6Jb7gUoBDOZtwLg6AdcngTJ+w5VFh3wxgPy0g3zb9Aw0Qbpw== + dependencies: + js-tiktoken "^1.0.12" + openai "^4.68.0" + zod "^3.22.4" + zod-to-json-schema "^3.22.3" + "@mistralai/mistralai@^0.4.0": version "0.4.0" resolved "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-0.4.0.tgz#77ded49e7869a4119988f47ce347c347ab41571e" @@ -305,6 +315,14 @@ dependencies: node-fetch "^2.6.7" +"@types/node-fetch@^2.6.4": + version "2.6.11" + resolved "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz#9b39b78665dae0e82a08f02f4967d62c66f95d24" + integrity sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g== + dependencies: + "@types/node" "*" + form-data "^4.0.0" + "@types/node-forge@^1.3.0": version "1.3.11" resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.11.tgz#0972ea538ddb0f4d9c2fa0ec5db5724773a604da" @@ -319,6 +337,13 @@ dependencies: undici-types "~6.19.8" +"@types/node@^18.11.18": + version "18.19.59" + resolved "https://registry.npmjs.org/@types/node/-/node-18.19.59.tgz#2de1b95b0b468089b616b2feb809755d70a74949" + integrity sha512-vizm2EqwV/7Zay+A6J3tGl9Lhr7CjZe2HmWS988sefiEmsyP9CeXEleho6i4hJk/8UtZAo0bWN4QPZZr83RxvQ== + dependencies: + undici-types "~5.26.4" + "@types/retry@0.12.0": version "0.12.0" resolved "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" @@ -329,6 +354,13 @@ resolved "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz#e9c07fe50da0f53dc24970cca94d619ff03f6f6d" integrity sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ== +abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + acorn-walk@^8.2.0: version "8.3.4" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.4.tgz#794dd169c3977edf4ba4ea47583587c5866236b7" @@ -341,6 +373,13 @@ acorn@^8.11.0, acorn@^8.8.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.13.0.tgz#2a30d670818ad16ddd6a35d3842dacec9e5d7ca3" integrity sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w== +agentkeepalive@^4.2.1: + version "4.5.0" + resolved "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz#2673ad1389b3c418c5a20c5d7364f93ca04be923" + integrity sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew== + dependencies: + humanize-ms "^1.2.1" + ansi-styles@^5.0.0: version "5.2.0" resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" @@ -361,6 +400,11 @@ as-table@^1.0.36: dependencies: printable-characters "^1.0.42" +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + base64-js@^1.5.1: version "1.5.1" resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" @@ -411,6 +455,13 @@ chokidar@^3.5.3: optionalDependencies: fsevents "~2.3.2" +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + commander@^10.0.1: version "10.0.1" resolved "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" @@ -448,6 +499,11 @@ defu@^6.1.4: resolved "https://registry.yarnpkg.com/defu/-/defu-6.1.4.tgz#4e0c9cf9ff68fe5f3d7f2765cc1a012dfdcb0479" integrity sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg== +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + double-ended-queue@^2.1.0-0: version "2.1.0-0" resolved "https://registry.npmjs.org/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz#103d3527fd31528f40188130c841efdd78264e5c" @@ -491,6 +547,11 @@ estree-walker@^0.6.1: resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.6.1.tgz#53049143f40c6eb918b23671d1fe3219f3a1b362" integrity sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w== +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + eventemitter3@^4.0.4: version "4.0.7" resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" @@ -508,6 +569,28 @@ fill-range@^7.1.1: dependencies: to-regex-range "^5.0.1" +form-data-encoder@1.7.2: + version "1.7.2" + resolved "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz#1f1ae3dccf58ed4690b86d87e4f57c654fbab040" + integrity sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A== + +form-data@^4.0.0: + version "4.0.1" + resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz#ba1076daaaa5bfd7e99c1a6cb02aa0a5cff90d48" + integrity sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +formdata-node@^4.3.2: + version "4.4.1" + resolved "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz#23f6a5cb9cb55315912cbec4ff7b0f59bbd191e2" + integrity sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ== + dependencies: + node-domexception "1.0.0" + web-streams-polyfill "4.0.0-beta.3" + fsevents@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" @@ -550,6 +633,13 @@ hono@^4.6.7: resolved "https://registry.yarnpkg.com/hono/-/hono-4.6.7.tgz#5389be797be4e049991d29ad7aea499e70d28085" integrity sha512-wX4ZbOnzfNO61hUjuQbJ7OPGs1fWXXVVJ8VTPDb2Ls/x9HjCbVTm80Je6VTHMz5n5RGDtBgV9d9ZFZxBqx56ng== +humanize-ms@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" + integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ== + dependencies: + ms "^2.0.0" + is-binary-path@~2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" @@ -612,6 +702,18 @@ magic-string@^0.25.3: dependencies: sourcemap-codec "^1.4.8" +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12: + version "2.1.35" + resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + mime@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7" @@ -635,7 +737,7 @@ miniflare@3.20241022.0: youch "^3.2.2" zod "^3.22.3" -ms@^2.1.3: +ms@^2.0.0, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -650,6 +752,11 @@ nanoid@^3.3.3: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== +node-domexception@1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" + integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== + node-fetch@^2.6.7: version "2.7.0" resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" @@ -672,6 +779,19 @@ ohash@^1.1.4: resolved "https://registry.yarnpkg.com/ohash/-/ohash-1.1.4.tgz#ae8d83014ab81157d2c285abf7792e2995fadd72" integrity sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g== +openai@^4.68.0: + version "4.68.4" + resolved "https://registry.npmjs.org/openai/-/openai-4.68.4.tgz#f8d684c1f2408d362164ad71916e961941aeedd1" + integrity sha512-LRinV8iU9VQplkr25oZlyrsYGPGasIwYN8KFMAAFTHHLHjHhejtJ5BALuLFrkGzY4wfbKhOhuT+7lcHZ+F3iEA== + dependencies: + "@types/node" "^18.11.18" + "@types/node-fetch" "^2.6.4" + abort-controller "^3.0.0" + agentkeepalive "^4.2.1" + form-data-encoder "1.7.2" + formdata-node "^4.3.2" + node-fetch "^2.6.7" + p-finally@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" @@ -837,6 +957,11 @@ ufo@^1.5.4: resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.5.4.tgz#16d6949674ca0c9e0fbbae1fa20a71d7b1ded754" integrity sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ== +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + undici-types@~6.19.8: version "6.19.8" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" @@ -864,6 +989,11 @@ uuid@^10.0.0: resolved "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294" integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ== +web-streams-polyfill@4.0.0-beta.3: + version "4.0.0-beta.3" + resolved "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz#2898486b74f5156095e473efe989dcf185047a38" + integrity sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug== + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"