AI Receptionist for Apartment Rentals built with CrewAI

Recently, I had a particularly frustrating experience with an apartment rental. The 24-hour front desk turned out to be a person available via WhatsApp. They took their time to answer questions and seemed shocked that someone had booked the apartment because they didn’t even know who was arriving and when. On top of that, I couldn’t just call them because they were not speaking English and had to use a translator app to chat. Overall, I couldn’t help but think all of this could be done much better by an AI chatbot. So, let’s build such a chatbot with CrewAI.

A conversation with the AI Receptionist
A conversation with the AI Receptionist

AI Receptionist Capabilities

For the sake of a tutorial, I will show you a simplified version of the chatbot. The chatbot can:

  • set the access code to the key box and share it with the guest
  • change the WiFi password to the guest network and tell what’s the current password
  • answer questions in a friendly manner
  • escalate the conversation to a human when needed

In the tutorial, I will leave out the parts unrelated to the chatbot so we will use a mock implementation for the smart lock and WiFi Router API. Also, the integration with a chat platform will be omitted.

Setting up the project

We need to download CrewAI and CrewAI Tools.

pip install crewai crewai-tools

After that, we need an AI client. I will use OpenAI and the gpt-4o model.

import os
from langchain_openai import ChatOpenAI


llm = ChatOpenAI(
    api_key=os.environ["OPENAI_API_KEY"],
    model="gpt-4o"
)

Tools available for the AI Receptionist

To interact with external services, we need to define tools. Because our tools will interact with systems with multiple implementations, depending on the vendor, we will define client interfaces as abstract classes. It will allow us to share the same interface for all smart lock and WiFi Router vendors.

In the smart lock implementation, we want to generate an access code for the key box. We can do it randomly and store the key in a database or have some hashing function that produces a separate code for each guest but keeps the code the same if they book the apartment again. I choose the hashing function approach.

Why is it a smart lock for a key box instead of the door? It seems more fault-tolerant. It’s easier to come and give the guest the key when the smart lock doesn’t work than to deal with a locked apartment door.

from abc import ABC, abstractmethod
from pydantic import BaseModel
import string
import random
import hashlib


class SmartLock(ABC, BaseModel):
  guest_id: str
  apartment_id: str
  access_code_length: int = 4

  def _generate_access_code(self) -> str:
    hashable_string = self.guest_id + self.apartment_id
    guest_hash = hashlib.sha256(hashable_string.encode()).hexdigest()

    access_code = ""
    for i in guest_hash:
      if i.isdigit():
        access_code += i
        if len(access_code) == self.access_code_length:
          break

    if (len(access_code) != self.access_code_length):
      raise Exception("Access code not generated. Use the Escalate tool to notify the human-operator.")

    return access_code

  @abstractmethod
  def set_code(self, new_code: str):
    pass

For the WiFi Router, we want to change the password to a random string and return the new password to the user:

class UpdateGuestWifiPassword(BaseModel, ABC):
  password_length: int = 14

  def __generate_random_password(self) -> str:
    characters = string.ascii_letters + string.digits
    password = ''.join(random.choice(characters) for i in range(self.password_length))
    return password


  @abstractmethod
  def _set_new_password(self, new_password: str) -> None:
    pass

  def update(self) -> str:
    new_password = self.__generate_random_password()
    self._set_new_password(new_password)
    return new_password

As promised, we will use a mock implementation for the smart lock and WiFi Router API.

class NoOpSmartLock(SmartLock):
  def set_code(self, new_code: str):
    print(f"Setting code to {new_code}")

class NoOpWifiRouter(UpdateGuestWifiPassword):
  def _set_new_password(self, new_password: str) -> None:
    print(f"Setting new password to {new_password}")

Now, we can implement the tools for the chatbot in a way that is supported by CrewAI. We need three tools:

from crewai_tools import BaseTool


class KeyBoxTool(BaseTool):
  name: str = "KeyBox"
  description: str = "Returns the access code to the box containing the apartment key. The access code is different for every guest but stays the same throughout their stay. The tool doesn't accept any arguments."
  smart_lock: SmartLock

  def _run(self) -> str:
    access_code = self.smart_lock._generate_access_code()
    self.smart_lock.set_code(access_code)
    return access_code


class Escalate(BaseTool):
  name: str = "Escalate"
  description: str = "Escalate the issue to the human operator. The tool accepts no arguments."

  def _run(self) -> str:
    # in the real implementation, I suggest you mark the conversation as escalated and stop sending new messages to AI until the human operator marks the issue as resolved
    # also, you will need to notify the operator somehow. I would use Opsgenie or PagerDuty for that because their notifications are super-annoying, and you cannot ignore them
    return "Escalated. Notify the user that a human operator will help them. Stop calling tools."


class WifiTool(BaseTool):
  name: str = "WifiRouter"
  description: str = "Return a new password for the Wifi. The tool doesn't accept any arguments. The tool should be used only once per conversation because it resets the password. If the user asks about the password again, reply with the same password as before."
  wifi_router: UpdateGuestWifiPassword

  def _run(self) -> str:
    return self.wifi_router.update()

Chatbot implementation with CrewAI

Finally, we are ready to implement the chatbot. In CrewAI, we need agents capable of choosing the right tool. Let’s define the receptionist agent:

from crewai import Agent


receptionist_agent = Agent(
    role="Receptionist",
    goal="Help the guests with their reservation",
    backstory="""You are a receptionist for a single-apartment short-term rental property. You help guests access the apartment, setup wifi, or escalate any questions they may have that you cannot handle.

 The key is in the black box on the right side of the door. When the user asks about it, retrieve the access code before replying.

 If the user's message doesn't require the usage of any tool, reply in a friendly way.
 If you escalate the issue, notify the user and call no other tools.""",
    llm=llm,
    max_iter=3,
    max_retry_limit=2,
    tools=[keyboxtool, wifitool, escalate],
    verbose=True
)

We also need a task. The task is the assignment for the agent. In the task description, we will include the conversation history and the instructions for AI. The conversation history will be a placeholder that we will fill in with messages later.

from crewai import Task


task = Task(
    description="""Reply to the user in the following conversation:

    {conversation_history}""",
    agent=receptionist_agent,
    tools=[keyboxtool, wifitool, escalate],
    expected_output="A single-sentence response to the user's message."
)

The final setup step is to gather the crew and assign tasks. We have one agent and one task, so the setup is straightforward:

from crewai import Crew


crew = Crew(
    agents=[receptionist_agent],
    tasks=[task],
    verbose=True
)

Running the chatbot

Because we want to preserve the conversation history, we have to keep it somewhere. The easiest way is to use a string where every line is a new message. To make the interactions easier, I implemented a helper function that adds the current message to the history, passes it to the crew, gets the response, updates the history to preserve the response, too, and returns the message we must send back to the user.

def conversation_with_agent(user_input, conversation_history):
    conversation_history += f"\nUser: {user_input}\n"
    result = crew.kickoff(inputs={'conversation_history': conversation_history})

    agent_response = result.raw
    conversation_history += f"Agent: {agent_response}\n"
    print(agent_response)

    return conversation_history, agent_response

Finally, we can run the chatbot and see how it works:

conversation_history = """"""

conversation_history, agent_response = conversation_with_agent("Hi", conversation_history)

Let me show you the log:

[2024-09-08 08:00:03][DEBUG]: == Working Agent: Receptionist
 [2024-09-08 08:00:03][INFO]: == Starting Task: Reply to the user in the following conversation


User: Hi



> Entering new CrewAgentExecutor chain...
I should greet the user in a friendly manner.

Final Answer: Hello! How can I assist you with your reservation today?

> Finished chain.
 [2024-09-08 08:00:04][DEBUG]: == [Receptionist] Task output: Hello! How can I assist you with your reservation today?


Hello! How can I assist you with your reservation today?

When we ask about opening the door, the agent uses the KeyBox tool and sends us the code. Note that the agent also used the information about the location of the key box.

conversation_history, agent_response = conversation_with_agent("How can I open the door?", conversation_history)
[2024-09-08 08:00:06][DEBUG]: == Working Agent: Receptionist
 [2024-09-08 08:00:06][INFO]: == Starting Task: Reply to the user in the following conversation


User: Hi
Agent: Hello! How can I assist you with your reservation today?

User: How can I open the door?



> Entering new CrewAgentExecutor chain...
The user asked about how to open the door, which involves retrieving the access code for the key box.

Action: KeyBox
Action Input: {}Setting code to 2390


2390

Thought: I now know the final answer.

Final Answer: The key is in the black box on the right side of the door; you can open it using the access code 2390.

> Finished chain.
 [2024-09-08 08:00:07][DEBUG]: == [Receptionist] Task output: The key is in the black box on the right side of the door; you can open it using the access code 2390.


The key is in the black box on the right side of the door; you can open it using the access code 2390.

When we ask about the WiFi password, the agent uses the WifiRouter tool and sends us the new password.

conversation_history, agent_response = conversation_with_agent("Where do I find the wifi password?", conversation_history)
...
> Entering new CrewAgentExecutor chain...
I need to use the WifiRouter tool to retrieve the new password for the wifi.

Action: WifiRouter
Action Input: {}Setting new password to TlrmJIlAOr40ld


TlrmJIlAOr40ld

Thought: I now know the final answer.

Final Answer: The wifi password is TlrmJIlAOr40ld.
...

Issue escalation works, too:

conversation_history, agent_response = conversation_with_agent("Help! I locked myself in the restroom and cannot get out!", conversation_history)
...
> Entering new CrewAgentExecutor chain...
The issue of being locked in the restroom is something that I cannot resolve directly. I need to escalate this to the human operator.

Action: Escalate
Action Input: {}


Escalated. Notify the user that a human-operator will help them. Stop calling tools.

Thought: I need to notify the user that the issue has been escalated to a human operator.

Final Answer: Your issue has been escalated to a human operator who will assist you shortly.

> Finished chain.
...

And lastly, we told the agent not to call the WifiRouter tool more than once. If we ask about the password again, it will reply with the same password as before:

conversation_history, agent_response = conversation_with_agent("What was the wifi password again?", conversation_history)
...
> Entering new CrewAgentExecutor chain...
The user is asking for the wifi password again. Since I've already provided the wifi password in the previous interaction, I should provide the same password again.

Final Answer: The wifi password is TlrmJIlAOr40ld.

> Finished chain.

Do you need help building AI Receptionist for Apartment Rentals for your business?
You can hire me!

And if you need an AI chatbot for your cyberpunk-style capsule hotel, let me know. I want to be involved in that project.

Older post

人工智能未能提高公司生产力的 4 个原因和 3 个解决方法

47%的使用人工智能的员工难以达到预期的生产率。文章解释了技能差距的原因,并展示了利用人工智能提高生产率的有效策略,包括量身定制的培训计划和智能自动化解决方案

Are you looking for an experienced AI consultant? Do you need assistance with your RAG or Agentic Workflow?
Schedule a call, send me a message on LinkedIn. Schedule a call or send me a message on LinkedIn

>