chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
# Restaurant - Demo Application
|
||||
|
||||
This sample provides a practical demonstration of how to leverage features from the [Semantic Kernel](https://learn.microsoft.com/en-us/semantic-kernel) to build a console application. Specifically, the application utilizes the [Business Schedule and Booking API](https://www.microsoft.com/en-us/microsoft-365/business/scheduling-and-booking-app) through Microsoft Graph to enable a Large Language Model (LLM) to book restaurant appointments efficiently. This guide will walk you through the necessary steps to integrate these technologies seamlessly.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Python 3.10, 3.11, or 3.12.
|
||||
- [Microsoft 365 Business License](https://www.microsoft.com/en-us/microsoft-365/business/compare-all-microsoft-365-business-products) to use [Business Schedule and Booking API](https://www.microsoft.com/en-us/microsoft-365/business/scheduling-and-booking-app).
|
||||
- [Azure Entra Id](https://www.microsoft.com/en-us/security/business/identity-access/microsoft-entra-id) administrator account to register an application and set the necessary credentials and permissions.
|
||||
|
||||
### Function Calling Enabled Models
|
||||
|
||||
This sample uses function calling capable models and has been tested with the following models:
|
||||
|
||||
| Model type | Model name/id | Model version | Supported |
|
||||
| --------------- | ------------------------- | ------------------: | --------- |
|
||||
| Chat Completion | gpt-3.5-turbo | 0125 | ✅ |
|
||||
| Chat Completion | gpt-3.5-turbo | 1106 | ✅ |
|
||||
| Chat Completion | gpt-3.5-turbo-0613 | 0613 | ✅ |
|
||||
| Chat Completion | gpt-3.5-turbo-0301 | 0301 | ❌ |
|
||||
| Chat Completion | gpt-3.5-turbo-16k | 0613 | ✅ |
|
||||
| Chat Completion | gpt-4 | 0613 | ✅ |
|
||||
| Chat Completion | gpt-4-0613 | 0613 | ✅ |
|
||||
| Chat Completion | gpt-4-0314 | 0314 | ❌ |
|
||||
| Chat Completion | gpt-4-turbo | 2024-04-09 | ✅ |
|
||||
| Chat Completion | gpt-4-turbo-2024-04-09 | 2024-04-09 | ✅ |
|
||||
| Chat Completion | gpt-4-turbo-preview | 0125-preview | ✅ |
|
||||
| Chat Completion | gpt-4-0125-preview | 0125-preview | ✅ |
|
||||
| Chat Completion | gpt-4-vision-preview | 1106-vision-preview | ✅ |
|
||||
| Chat Completion | gpt-4-1106-vision-preview | 1106-vision-preview | ✅ |
|
||||
|
||||
ℹ️ OpenAI Models older than 0613 version do not support function calling.
|
||||
|
||||
ℹ️ When using Azure OpenAI, ensure that the model name of your deployment matches any of the above supported models names.
|
||||
|
||||
## Configuring the sample
|
||||
|
||||
Please make sure either your environment variables or your .env file contains the following:
|
||||
|
||||
- "BOOKING_SAMPLE_CLIENT_ID"
|
||||
- "BOOKING_SAMPLE_TENANT_ID"
|
||||
- "BOOKING_SAMPLE_CLIENT_SECRET"
|
||||
- "BOOKING_SAMPLE_BUSINESS_ID"
|
||||
- "BOOKING_SAMPLE_SERVICE_ID"
|
||||
|
||||
If wanting to use the `.env` file, you must pass the `env_file_path` parameter with a valid path:
|
||||
|
||||
```python
|
||||
booking_sample_settings = BookingSampleSettings(env_file_path=env_file_path)
|
||||
```
|
||||
|
||||
This will tell Pydantic settings to also load the `.env` file instead of just trying to load environment variables.
|
||||
|
||||
### Create an App Registration in Azure Active Directory
|
||||
|
||||
1. Go to the [Azure Portal](https://portal.azure.com/).
|
||||
2. Select the Azure Active Directory service.
|
||||
3. Select App registrations and click on New registration.
|
||||
4. Fill in the required fields and click on Register.
|
||||
5. Copy the Application **(client) Id** for later use.
|
||||
6. Save Directory **(tenant) Id** for later use..
|
||||
7. Click on Certificates & secrets and create a new client secret. (Any name and expiration date will work)
|
||||
8. Copy the **client secret** value for later use.
|
||||
9. Click on API permissions and add the following permissions:
|
||||
- Microsoft Graph
|
||||
- Application permissions
|
||||
- BookingsAppointment.ReadWrite.All
|
||||
- Delegated permissions
|
||||
- OpenId permissions
|
||||
- offline_access
|
||||
- profile
|
||||
- openid
|
||||
|
||||
### Create Or Use a Booking Service and Business
|
||||
|
||||
1. Go to the [Bookings Homepage](https://outlook.office.com/bookings) website.
|
||||
2. Create a new Booking Page and add a Service to the Booking (Skip if you don't ).
|
||||
3. Access [Graph Explorer](https://developer.microsoft.com/en-us/graph/graph-explorer)
|
||||
4. Run the following query to get the Booking Business Id:
|
||||
```http
|
||||
GET https://graph.microsoft.com/v1.0/solutions/bookingBusinesses
|
||||
```
|
||||
5. Copy the **Booking Business Id** for later use.
|
||||
6. Run the following query and replace it with your **Booking Business Id** to get the Booking Service Id
|
||||
```http
|
||||
GET https://graph.microsoft.com/v1.0/solutions/bookingBusinesses/{bookingBusiness-id}/services
|
||||
```
|
||||
7. Copy the **Booking Service Id** for later use.
|
||||
|
||||
## Running the sample
|
||||
|
||||
Either run the sample in an IDE, like VSCode, or via the command line.
|
||||
|
||||
### Example of a conversation
|
||||
|
||||
> **User** > Please book a restaurant for me.
|
||||
|
||||
> **Assistant** > Sure, I can help you with that. Could you please provide me with the following information:
|
||||
>
|
||||
> 1. Name of the restaurant you would like to book.
|
||||
> 2. Date and time of the reservation.
|
||||
> 3. Number of people in your party.
|
||||
> 4. Your name.
|
||||
> 5. Your email address.
|
||||
> 6. Your phone number.
|
||||
>
|
||||
> Once I have all this information, I will proceed with the booking.
|
||||
|
||||
> **User** > I would like to book a table at the restaurant "The French Laundry" on the 15th of March at 7:00 PM for 4 people. My name is John Doe, my email address is john@internet.com, and my phone number is 123-456-7890.
|
||||
|
||||
> **System** > Do you want to book a table at The French Laundry on 15/03/2023 19:00:00 for 4 people?
|
||||
|
||||
> **System** > Please confirm by typing 'yes' or 'no'.
|
||||
|
||||
> **User** > yes
|
||||
|
||||
> **Assistant** > Great! I have successfully booked a table for you at "The French Laundry" on the 15th of March at 7:00 PM for 4 people.
|
||||
>
|
||||
> Your reservation has been made under the name John Doe.
|
||||
>
|
||||
> If you have any further questions or need to make any changes to your reservation, please let me know.
|
||||
|
||||
> **User** > List my current bookings
|
||||
|
||||
> **Assistant** > You currently have one booking:
|
||||
>
|
||||
> - Restaurant: The French Laundry
|
||||
> - Date and Time: 15th of March at 7:00 PM
|
||||
> - Party Size: 4 people
|
||||
>
|
||||
> If you need any further assistance or if there's anything else I can help you with, please let me know.
|
||||
|
||||
> **User** > Cancel my booking
|
||||
|
||||
> **System** > `[Cancelling a reservation for 4 at The French Laundry on 2023-03-15 at 19:00:00]`
|
||||
|
||||
> **Assistant** > I have successfully canceled your booking at "The French Laundry" on the 15th of March at 7:00 PM for 4 people.
|
||||
>
|
||||
> If you have any other questions or need further assistance, please let me know.
|
||||
@@ -0,0 +1,45 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from pydantic import SecretStr
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class BookingSampleSettings(BaseSettings):
|
||||
"""Restaurant Booking Sample settings
|
||||
|
||||
The settings are first loaded from environment variables with the prefix 'BOOKING_'. If the
|
||||
environment variables are not found, the settings can be loaded from a .env file with the
|
||||
encoding 'utf-8'. If the settings are not found in the .env file, the settings are ignored;
|
||||
however, validation will fail alerting that the settings are missing.
|
||||
|
||||
Required settings for prefix 'BOOKING_' are:
|
||||
- client_id = The App Registration Client ID (Env var BOOKING_CLIENT_ID)
|
||||
- tenant_id = The App Registration Tenant ID (Env var BOOKING_TENANT_ID)
|
||||
- client_secret = The App Registration Client Secret (Env var BOOKING_CLIENT_SECRET)
|
||||
- business_id = The sample booking service ID (Env var BOOKING_BUSINESS_ID)
|
||||
- service_id = The sample booking service ID (Env var BOOKING_SERVICE_ID)
|
||||
|
||||
For more information on these required settings, please see the sample's README.md file.
|
||||
"""
|
||||
|
||||
env_file_path: str | None = None
|
||||
client_id: str
|
||||
tenant_id: str
|
||||
client_secret: SecretStr
|
||||
business_id: str
|
||||
service_id: str
|
||||
|
||||
class Config:
|
||||
env_prefix = "BOOKING_"
|
||||
env_file = None
|
||||
env_file_encoding = "utf-8"
|
||||
extra = "ignore"
|
||||
case_sensitive = False
|
||||
|
||||
@classmethod
|
||||
def create(cls, **kwargs):
|
||||
if kwargs.get("env_file_path"):
|
||||
cls.Config.env_file = kwargs["env_file_path"]
|
||||
else:
|
||||
cls.Config.env_file = None
|
||||
return cls(**kwargs)
|
||||
@@ -0,0 +1,3 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
# intentionally left empty
|
||||
@@ -0,0 +1,140 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Annotated
|
||||
|
||||
from msgraph import GraphServiceClient
|
||||
from msgraph.generated.models.booking_appointment import BookingAppointment
|
||||
from msgraph.generated.models.booking_customer_information import BookingCustomerInformation
|
||||
from msgraph.generated.models.date_time_time_zone import DateTimeTimeZone
|
||||
from msgraph.generated.models.location import Location
|
||||
|
||||
from semantic_kernel.functions.kernel_function_decorator import kernel_function
|
||||
|
||||
|
||||
class BookingsPlugin:
|
||||
"""A plugin for booking tables at a restaurant."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
graph_client: GraphServiceClient,
|
||||
booking_business_id: str,
|
||||
booking_service_id: str,
|
||||
customer_timezone: str = "America/Chicago",
|
||||
):
|
||||
"""Initializes a new instance of the BookingsPlugin class.
|
||||
|
||||
Args:
|
||||
graph_client (GraphServiceClient): The GraphServiceClient instance.
|
||||
booking_business_id (str): The ID of the booking business.
|
||||
service_id (str): The ID of the service.
|
||||
customer_timezone (str, optional): The timezone of the customer. Defaults to "America/Chicago".
|
||||
"""
|
||||
self.graph_client = graph_client
|
||||
self.booking_business_id = booking_business_id
|
||||
self.booking_service_id = booking_service_id
|
||||
self.customer_timezone = customer_timezone
|
||||
|
||||
@kernel_function(name="book_table", description="Book a table at a restaurant")
|
||||
async def book_table(
|
||||
self,
|
||||
restaurant: Annotated[str, "The name of the restaurant"],
|
||||
date_time: Annotated[str, "The time in UTC, formatted as an ISO datetime string, like 2024-09-15T19:00:00"],
|
||||
party_size: Annotated[int, "The number of people in the party"],
|
||||
customer_name: Annotated[str, "The name of the customer"],
|
||||
customer_email: Annotated[str, "The email of the customer"],
|
||||
customer_phone: Annotated[str, "The phone number of the customer"],
|
||||
) -> Annotated[str, "The booking appointment ID"]:
|
||||
"""Book a table at a restaurant.
|
||||
|
||||
Args:
|
||||
restaurant (str): The name of the restaurant.
|
||||
date_time (datetime): The time in UTC.
|
||||
party_size (int): The number of people in the party.
|
||||
customer_name (str): The name of the customer.
|
||||
customer_email (str): The email of the customer.
|
||||
customer_phone (str): The phone number of the customer.
|
||||
|
||||
Returns:
|
||||
str: The status of the booking.
|
||||
"""
|
||||
print(f"System > Do you want to book a table at {restaurant} on {date_time} for {party_size} people?")
|
||||
print("System > Please confirm by typing 'yes' or 'no'.")
|
||||
confirmation = input("User:> ")
|
||||
if confirmation.lower() != "yes":
|
||||
return "Booking aborted by the user."
|
||||
request_body = BookingAppointment(
|
||||
odata_type="#microsoft.graph.bookingAppointment",
|
||||
customer_time_zone=self.customer_timezone,
|
||||
sms_notifications_enabled=False,
|
||||
start_date_time=DateTimeTimeZone(
|
||||
odata_type="#microsoft.graph.dateTimeTimeZone",
|
||||
date_time=date_time,
|
||||
time_zone="UTC",
|
||||
),
|
||||
end_date_time=DateTimeTimeZone(
|
||||
odata_type="#microsoft.graph.dateTimeTimeZone",
|
||||
date_time=(datetime.fromisoformat(date_time) + timedelta(hours=2)).isoformat(),
|
||||
time_zone="UTC",
|
||||
),
|
||||
is_location_online=False,
|
||||
opt_out_of_customer_email=False,
|
||||
anonymous_join_web_url=None,
|
||||
service_id=self.booking_service_id,
|
||||
service_location=Location(
|
||||
odata_type="#microsoft.graph.location",
|
||||
display_name=restaurant,
|
||||
),
|
||||
maximum_attendees_count=party_size,
|
||||
filled_attendees_count=party_size,
|
||||
customers=[
|
||||
BookingCustomerInformation(
|
||||
odata_type="#microsoft.graph.bookingCustomerInformation",
|
||||
name=customer_name,
|
||||
email_address=customer_email,
|
||||
phone=customer_phone,
|
||||
time_zone=self.customer_timezone,
|
||||
),
|
||||
],
|
||||
additional_data={
|
||||
"price_type@odata_type": "#microsoft.graph.bookingPriceType",
|
||||
"reminders@odata_type": "#Collection(microsoft.graph.bookingReminder)",
|
||||
"customers@odata_type": "#Collection(microsoft.graph.bookingCustomerInformation)",
|
||||
},
|
||||
)
|
||||
|
||||
response = await self.graph_client.solutions.booking_businesses.by_booking_business_id(
|
||||
self.booking_business_id
|
||||
).appointments.post(request_body)
|
||||
|
||||
return f"Booking successful! Your reservation ID is {response.id}."
|
||||
|
||||
@kernel_function(name="list_revervations", description="List all reservations")
|
||||
async def list_reservations(self) -> Annotated[str, "The list of reservations"]:
|
||||
"""List the reservations for the booking business."""
|
||||
appointments = await self.graph_client.solutions.booking_businesses.by_booking_business_id(
|
||||
self.booking_business_id
|
||||
).appointments.get()
|
||||
return "\n".join([
|
||||
f"{appointment.service_location.display_name} on {appointment.start_date_time.date_time} with id: {appointment.id}" # noqa: E501
|
||||
for appointment in appointments.value
|
||||
])
|
||||
|
||||
@kernel_function(name="cancel_reservation", description="Cancel a reservation")
|
||||
async def cancel_reservation(
|
||||
self,
|
||||
reservation_id: Annotated[str, "The ID of the reservation"],
|
||||
restaurant: Annotated[str, "The name of the restaurant"],
|
||||
date: Annotated[str, "The date of the reservation"],
|
||||
time: Annotated[str, "The time of the reservation"],
|
||||
party_size: Annotated[int, "The number of people in the party"],
|
||||
) -> Annotated[str, "The cancellation status of the reservation"]:
|
||||
"""Cancel a reservation."""
|
||||
print(f"System > [Cancelling a reservation for {party_size} at {restaurant} on {date} at {time}]")
|
||||
|
||||
_ = (
|
||||
await self.graph_client.solutions.booking_businesses.by_booking_business_id(self.booking_business_id)
|
||||
.appointments.by_booking_appointment_id(reservation_id)
|
||||
.delete()
|
||||
)
|
||||
return "Cancellation successful!"
|
||||
@@ -0,0 +1,109 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
|
||||
from azure.identity import ClientSecretCredential
|
||||
from booking_sample_settings import BookingSampleSettings
|
||||
from bookings_plugin.bookings_plugin import BookingsPlugin
|
||||
from msgraph import GraphServiceClient
|
||||
from pydantic import ValidationError
|
||||
|
||||
from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase
|
||||
from semantic_kernel.connectors.ai.open_ai.prompt_execution_settings.open_ai_prompt_execution_settings import (
|
||||
OpenAIChatPromptExecutionSettings,
|
||||
)
|
||||
from semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion import OpenAIChatCompletion
|
||||
from semantic_kernel.contents.chat_history import ChatHistory
|
||||
from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError
|
||||
from semantic_kernel.functions.kernel_arguments import KernelArguments
|
||||
from semantic_kernel.kernel import Kernel
|
||||
|
||||
kernel = Kernel()
|
||||
|
||||
service_id = "open_ai"
|
||||
ai_service = OpenAIChatCompletion(service_id=service_id, ai_model_id="gpt-3.5-turbo")
|
||||
kernel.add_service(ai_service)
|
||||
|
||||
try:
|
||||
booking_sample_settings = BookingSampleSettings()
|
||||
except ValidationError as e:
|
||||
raise ServiceInitializationError("Failed to initialize the booking sample settings.") from e
|
||||
|
||||
tenant_id = booking_sample_settings.tenant_id
|
||||
client_id = booking_sample_settings.client_id
|
||||
client_secret = booking_sample_settings.client_secret
|
||||
client_secret_credential = ClientSecretCredential(tenant_id=tenant_id, client_id=client_id, client_secret=client_secret)
|
||||
|
||||
graph_client = GraphServiceClient(credentials=client_secret_credential, scopes=["https://graph.microsoft.com/.default"])
|
||||
|
||||
booking_business_id = booking_sample_settings.business_id
|
||||
booking_service_id = booking_sample_settings.service_id
|
||||
|
||||
bookings_plugin = BookingsPlugin(
|
||||
graph_client=graph_client,
|
||||
booking_business_id=booking_business_id,
|
||||
booking_service_id=booking_service_id,
|
||||
)
|
||||
|
||||
kernel.add_plugin(bookings_plugin, "BookingsPlugin")
|
||||
|
||||
chat_function = kernel.add_function(
|
||||
plugin_name="ChatBot",
|
||||
function_name="Chat",
|
||||
prompt="{{$chat_history}}{{$user_input}}",
|
||||
template_format="semantic-kernel",
|
||||
)
|
||||
|
||||
settings: OpenAIChatPromptExecutionSettings = kernel.get_prompt_execution_settings_from_service_id(
|
||||
service_id, ChatCompletionClientBase
|
||||
)
|
||||
settings.max_tokens = 2000
|
||||
settings.temperature = 0.1
|
||||
settings.top_p = 0.8
|
||||
settings.function_choice_behavior.Auto(filters={"exclude_plugin": ["ChatBot"]})
|
||||
|
||||
chat_history = ChatHistory(
|
||||
system_message="When responding to the user's request to book a table, include the reservation ID."
|
||||
)
|
||||
|
||||
|
||||
async def chat() -> bool:
|
||||
try:
|
||||
user_input = input("User:> ")
|
||||
except KeyboardInterrupt:
|
||||
print("\n\nExiting chat...")
|
||||
return False
|
||||
except EOFError:
|
||||
print("\n\nExiting chat...")
|
||||
return False
|
||||
|
||||
if user_input == "exit":
|
||||
print("\n\nExiting chat...")
|
||||
return False
|
||||
|
||||
# Note the reservation returned contains an ID. That ID can be used to cancel the reservation,
|
||||
# when the bookings API supports it.
|
||||
answer = await kernel.invoke(
|
||||
chat_function, KernelArguments(settings=settings, user_input=user_input, chat_history=chat_history)
|
||||
)
|
||||
chat_history.add_user_message(user_input)
|
||||
chat_history.add_assistant_message(str(answer))
|
||||
print(f"Assistant:> {answer}")
|
||||
return True
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
chatting = True
|
||||
print(
|
||||
"Welcome to your Restaurant Booking Assistant.\
|
||||
\n Type 'exit' to exit.\
|
||||
\n Please enter the following information to book a table: the restaurant, the date and time, \
|
||||
\n the number of people, your name, phone, and email. You may ask me for help booking a table, \
|
||||
\n listing reservations, or cancelling a reservation. When cancelling please provide the reservation ID."
|
||||
)
|
||||
while chatting:
|
||||
chatting = await chat()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user