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