diff --git a/java/samples/booking-agent-m365/pom.xml b/java/samples/booking-agent-m365/pom.xml
new file mode 100644
index 0000000000..01954247df
--- /dev/null
+++ b/java/samples/booking-agent-m365/pom.xml
@@ -0,0 +1,75 @@
+
+
+ 4.0.0
+
+ com.microsoft.semantic-kernel
+ semantickernel-samples-parent
+ 1.0.2-SNAPSHOT
+ ../pom.xml
+
+
+ booking-agent-m365
+ Booking agent with M365
+ Booking agent using M365
+
+
+ 17
+ 17
+ UTF-8
+
+
+
+
+ org.apache.logging.log4j
+ log4j-api
+ runtime
+
+
+ org.apache.logging.log4j
+ log4j-core
+ runtime
+
+
+ org.apache.logging.log4j
+ log4j-slf4j2-impl
+ runtime
+
+
+
+ com.microsoft.semantic-kernel
+ semantickernel-api
+
+
+ com.microsoft.semantic-kernel
+ semantickernel-aiservices-openai
+
+
+
+ com.azure
+ azure-identity
+
+
+
+ com.microsoft.graph
+ microsoft-graph
+ 6.5.1
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+ 17
+ 17
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/java/samples/booking-agent-m365/src/main/java/com/microsoft/semantickernel/BookingAgent.java b/java/samples/booking-agent-m365/src/main/java/com/microsoft/semantickernel/BookingAgent.java
new file mode 100644
index 0000000000..87c80c0b77
--- /dev/null
+++ b/java/samples/booking-agent-m365/src/main/java/com/microsoft/semantickernel/BookingAgent.java
@@ -0,0 +1,107 @@
+package com.microsoft.semantickernel;
+
+import com.azure.ai.openai.OpenAIAsyncClient;
+import com.azure.ai.openai.OpenAIClientBuilder;
+import com.azure.core.credential.AzureKeyCredential;
+import com.azure.core.credential.KeyCredential;
+import com.azure.identity.InteractiveBrowserCredential;
+import com.azure.identity.InteractiveBrowserCredentialBuilder;
+import com.microsoft.graph.serviceclient.GraphServiceClient;
+import com.microsoft.semantickernel.aiservices.openai.chatcompletion.OpenAIChatCompletion;
+import com.microsoft.semantickernel.orchestration.InvocationContext;
+import com.microsoft.semantickernel.orchestration.ToolCallBehavior;
+import com.microsoft.semantickernel.plugin.KernelPlugin;
+import com.microsoft.semantickernel.plugin.KernelPluginFactory;
+import com.microsoft.semantickernel.services.chatcompletion.ChatCompletionService;
+import com.microsoft.semantickernel.services.chatcompletion.ChatHistory;
+import com.microsoft.semantickernel.services.chatcompletion.ChatMessageContent;
+
+import java.util.Scanner;
+
+public class BookingAgent {
+
+ // Config for OpenAI
+ private static final String CLIENT_KEY = System.getenv("CLIENT_KEY");
+ private static final String AZURE_CLIENT_KEY = System.getenv("AZURE_CLIENT_KEY");
+ private static final String CLIENT_ENDPOINT = System.getenv("CLIENT_ENDPOINT");
+ private static final String MODEL_ID = System.getenv()
+ .getOrDefault("MODEL_ID", "gpt-35-turbo-2");
+
+ // Config for Graph
+ // https://learn.microsoft.com/en-us/graph/tutorials/java?tabs=aad&tutorial-step=1
+ private static final String TENANT_ID = System.getenv("TENANT_ID");
+ private static final String CLIENT_ID = System.getenv("CLIENT_ID");
+ // Booking Business ID
+ private static final String BOOKING_BUSINESS_ID = System.getenv("BOOKING_BUSINESS_ID");
+ // Service ID for the booking business
+ private static final String SERVICE_ID = System.getenv("SERVICE_ID");
+ // Must match the redirect URL in the Azure AD app registration
+ private static final String REDIRECT_URL = System.getenv("REDIRECT_URL");
+ private static GraphServiceClient userClient;
+
+ public static void initializeGraph() {
+ InteractiveBrowserCredential interactiveBrowserCredential = new InteractiveBrowserCredentialBuilder()
+ .clientId(CLIENT_ID)
+ .tenantId(TENANT_ID)
+ .redirectUrl(REDIRECT_URL)
+ .build();
+
+ userClient = new GraphServiceClient(interactiveBrowserCredential,
+ "User.Read", "BookingsAppointment.ReadWrite.All");
+ }
+
+ public static void main(String[] args) throws NoSuchMethodException {
+ System.out.println("======== Open AI - Booking System ========");
+
+ OpenAIAsyncClient client;
+ if (AZURE_CLIENT_KEY != null) {
+ client = new OpenAIClientBuilder()
+ .credential(new AzureKeyCredential(AZURE_CLIENT_KEY))
+ .endpoint(CLIENT_ENDPOINT)
+ .buildAsyncClient();
+
+ } else {
+ client = new OpenAIClientBuilder()
+ .credential(new KeyCredential(CLIENT_KEY))
+ .buildAsyncClient();
+ }
+
+ initializeGraph();
+
+ KernelPlugin plugin = KernelPluginFactory.createFromObject(new BookingPlugin(
+ userClient,
+ BOOKING_BUSINESS_ID,
+ SERVICE_ID,
+ "UTC"
+ ), "BookingRestaurant");
+
+ ChatCompletionService chat = OpenAIChatCompletion.builder()
+ .withModelId(MODEL_ID)
+ .withOpenAIAsyncClient(client)
+ .build();
+
+ Kernel kernel = Kernel.builder()
+ .withPlugin(plugin)
+ .withAIService(ChatCompletionService.class, chat)
+ .build();
+
+ ChatHistory chatHistory = new ChatHistory();
+
+ Scanner scanner = new Scanner(System.in);
+ String userMessage;
+ System.out.println("User > ");
+
+ while ((userMessage = scanner.nextLine()) != null) {
+
+ chatHistory.addUserMessage(userMessage);
+ InvocationContext invocationContext = InvocationContext.builder().
+ withToolCallBehavior(ToolCallBehavior.allowAllKernelFunctions(true)).build();
+
+ ChatMessageContent> result = chat.getChatMessageContentsAsync(chatHistory, kernel, invocationContext).block().get(0);
+ chatHistory.addAssistantMessage(result.getContent());
+
+ System.out.println("Assistant > " + result);
+ System.out.println("User > ");
+ }
+ }
+}
diff --git a/java/samples/booking-agent-m365/src/main/java/com/microsoft/semantickernel/BookingPlugin.java b/java/samples/booking-agent-m365/src/main/java/com/microsoft/semantickernel/BookingPlugin.java
new file mode 100644
index 0000000000..37cc3e0992
--- /dev/null
+++ b/java/samples/booking-agent-m365/src/main/java/com/microsoft/semantickernel/BookingPlugin.java
@@ -0,0 +1,200 @@
+package com.microsoft.semantickernel;
+
+import com.microsoft.graph.models.BookingAppointment;
+import com.microsoft.graph.models.BookingCustomerInformation;
+import com.microsoft.graph.models.BookingCustomerInformationBase;
+import com.microsoft.graph.models.DateTimeTimeZone;
+import com.microsoft.graph.models.Location;
+import com.microsoft.graph.models.User;
+import com.microsoft.graph.serviceclient.GraphServiceClient;
+import com.microsoft.kiota.PeriodAndDuration;
+import com.microsoft.semantickernel.semanticfunctions.annotations.DefineKernelFunction;
+import com.microsoft.semantickernel.semanticfunctions.annotations.KernelFunctionParameter;
+
+import java.time.Duration;
+import java.time.OffsetDateTime;
+import java.util.List;
+
+public class BookingPlugin {
+ public static final int BOOKING_HOURS = 2;
+ private final GraphServiceClient graphServiceClient;
+ private final String bookingBusinessId;
+ private final String serviceId;
+ private final String userTimeZone;
+
+ public BookingPlugin(GraphServiceClient graphServiceClient,
+ String bookingBusinessId,
+ String serviceId,
+ String userTimeZone) {
+ this.graphServiceClient = graphServiceClient;
+ this.bookingBusinessId = bookingBusinessId;
+ this.serviceId = serviceId;
+ this.userTimeZone = userTimeZone;
+ }
+
+ @DefineKernelFunction(
+ name = "bookTable",
+ description = "Book a table at a restaurant",
+ returnType = "string"
+ )
+ public String bookTable(
+ @KernelFunctionParameter(
+ name = "restaurant",
+ description = "The name of the restaurant")
+ String restaurant,
+ @KernelFunctionParameter(
+ name = "date",
+ description = "The date of the reservation in UTC",
+ type = OffsetDateTime.class)
+ OffsetDateTime date,
+ @KernelFunctionParameter(
+ name = "partySize",
+ description = "The number of people in the party",
+ type = int.class)
+ int partySize
+ ) {
+ BookingAppointment bookingAppointment = new BookingAppointment();
+ bookingAppointment.setOdataType("#microsoft.graph.bookingAppointment");
+ bookingAppointment.setCustomerTimeZone(userTimeZone);
+ bookingAppointment.setSmsNotificationsEnabled(false);
+ DateTimeTimeZone endDateTime = new DateTimeTimeZone();
+ endDateTime.setOdataType("#microsoft.graph.dateTimeTimeZone");
+ endDateTime.setDateTime(date.plusHours(BOOKING_HOURS).toString());
+ endDateTime.setTimeZone("UTC");
+ bookingAppointment.setEndDateTime(endDateTime);
+ bookingAppointment.setIsLocationOnline(false);
+ bookingAppointment.setOptOutOfCustomerEmail(false);
+ bookingAppointment.setAnonymousJoinWebUrl(null);
+ PeriodAndDuration postBuffer = PeriodAndDuration.ofDuration(Duration.parse("PT10M"));
+ bookingAppointment.setPostBuffer(postBuffer);
+ PeriodAndDuration preBuffer = PeriodAndDuration.ofDuration(Duration.parse("PT5M"));
+ bookingAppointment.setPreBuffer(preBuffer);
+ bookingAppointment.setServiceId(serviceId);
+ Location serviceLocation = new Location();
+ serviceLocation.setOdataType("#microsoft.graph.location");
+ serviceLocation.setDisplayName(restaurant);
+ bookingAppointment.setServiceLocation(serviceLocation);
+ DateTimeTimeZone startDateTime = new DateTimeTimeZone();
+ startDateTime.setOdataType("#microsoft.graph.dateTimeTimeZone");
+ startDateTime.setDateTime(date.toString());
+ startDateTime.setTimeZone("UTC");
+ bookingAppointment.setStartDateTime(startDateTime);
+ bookingAppointment.setMaximumAttendeesCount(partySize);
+ bookingAppointment.setFilledAttendeesCount(partySize);
+
+ graphServiceClient.solutions().bookingBusinesses().byBookingBusinessId(bookingBusinessId).appointments().post(bookingAppointment);
+
+ return "Successful booking at " + restaurant;
+ }
+
+ @DefineKernelFunction(
+ name = "listReservations",
+ description = "List all reservations for a restaurant",
+ returnType = "string"
+ )
+ public String listReservations(
+ @KernelFunctionParameter(
+ name = "restaurant",
+ description = "The name of the restaurant")
+ String restaurant
+ ) {
+ List appointments = graphServiceClient.solutions().bookingBusinesses().byBookingBusinessId(bookingBusinessId).appointments().get().getValue();
+
+ StringBuilder appointmentsList = new StringBuilder();
+ for (BookingAppointment appointment : appointments) {
+ if (appointment.getServiceLocation().getDisplayName().equals(restaurant)) {
+ appointmentsList.append(appointment.getServiceLocation().getDisplayName())
+ .append(" at ")
+ .append(appointment.getStartDateTime().getDateTime())
+ .append(" for ")
+ .append(appointment.getMaximumAttendeesCount())
+ .append(" people\n");
+ }
+ }
+
+ if (appointmentsList.toString().isEmpty()) {
+ return "No reservations found for " + restaurant;
+ }
+
+ return appointmentsList.toString();
+ }
+
+ private BookingAppointment getAppointment(String restaurant, OffsetDateTime date) {
+ List appointments = graphServiceClient.solutions().bookingBusinesses().byBookingBusinessId(bookingBusinessId).appointments().get().getValue();
+
+ for (BookingAppointment appointment : appointments) {
+ if (appointment.getServiceLocation().getDisplayName().equals(restaurant)
+ && OffsetDateTime.parse(appointment.getStartDateTime().getDateTime()).equals(date)) {
+ return appointment;
+ }
+ }
+
+ return null;
+ }
+
+ @DefineKernelFunction(
+ name = "modifyReservation",
+ description = "Modify an existing reservation at a restaurant",
+ returnType = "string"
+ )
+ public String modifyReservation(
+ @KernelFunctionParameter(
+ name = "restaurant",
+ description = "The name of the restaurant")
+ String restaurant,
+ @KernelFunctionParameter(
+ name = "date",
+ description = "The date of the reservation in UTC",
+ type = OffsetDateTime.class)
+ OffsetDateTime date,
+ @KernelFunctionParameter(
+ name = "updatedDate",
+ description = "The updated date of the reservation in UTC",
+ type = OffsetDateTime.class)
+ OffsetDateTime updatedDate
+ ){
+ BookingAppointment appointment = getAppointment(restaurant, date);
+ if (appointment == null) {
+ return "No reservation found for " + restaurant + " on " + date;
+ }
+
+ appointment.setStartDateTime(new DateTimeTimeZone() {{
+ setDateTime(updatedDate.toString());
+ setTimeZone("UTC");
+ }});
+ appointment.setEndDateTime(new DateTimeTimeZone() {{
+ setDateTime(updatedDate.plusHours(BOOKING_HOURS).toString());
+ setTimeZone("UTC");
+ }});
+
+ graphServiceClient.solutions().bookingBusinesses().byBookingBusinessId(bookingBusinessId).appointments().byBookingAppointmentId(appointment.getId()).patch(appointment);
+
+ return "Reservation updated at " + restaurant + " from " + date + " to " + updatedDate;
+ }
+
+ @DefineKernelFunction(
+ name = "cancelReservation",
+ description = "Cancel a reservation at a restaurant",
+ returnType = "string"
+ )
+ public String cancelReservation(
+ @KernelFunctionParameter(
+ name = "restaurant",
+ description = "The name of the restaurant")
+ String restaurant,
+ @KernelFunctionParameter(
+ name = "date",
+ description = "The date of the reservation in UTC",
+ type = OffsetDateTime.class)
+ OffsetDateTime date
+ ) {
+ BookingAppointment appointment = getAppointment(restaurant, date);
+ if (appointment == null) {
+ return "No reservation found for " + restaurant + " on " + date;
+ }
+
+ graphServiceClient.solutions().bookingBusinesses().byBookingBusinessId(bookingBusinessId).appointments().byBookingAppointmentId(appointment.getId()).delete();
+
+ return "Reservation cancelled at " + restaurant + " on " + date;
+ }
+}
diff --git a/java/samples/booking-agent-m365/src/main/resources/log4j2.xml b/java/samples/booking-agent-m365/src/main/resources/log4j2.xml
new file mode 100644
index 0000000000..ef49e0ed73
--- /dev/null
+++ b/java/samples/booking-agent-m365/src/main/resources/log4j2.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/java/samples/pom.xml b/java/samples/pom.xml
index e6c6ab868e..dddfb6c6fd 100644
--- a/java/samples/pom.xml
+++ b/java/samples/pom.xml
@@ -16,6 +16,7 @@
sample-code
+ booking-agent-m365