Java: Add booking sample with M365 booking system (#5842)

### Motivation and Context

<!-- Thank you for your contribution to the semantic-kernel repo!
Please help reviewers and future users, providing the following
information:
  1. Why is this change required?
  2. What problem does it solve?
  3. What scenario does it contribute to?
  4. If it fixes an open issue, please link to the issue here.
-->

### Description

<!-- Describe your changes, the overall approach, the underlying design.
These notes will help understanding how your code works. Thanks! -->

### Contribution Checklist

<!-- Before submitting this PR, please make sure: -->

- [ ] The code builds clean without any errors or warnings
- [ ] The PR follows the [SK Contribution
Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md)
and the [pre-submission formatting
script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts)
raises no violations
- [ ] All unit tests pass, and I have added new tests where possible
- [ ] I didn't break anyone 😄
This commit is contained in:
Milder Hernandez
2024-04-18 11:18:27 +01:00
committed by GitHub
parent 2634fb686d
commit 1b2ac89b40
5 changed files with 409 additions and 0 deletions
+75
View File
@@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.microsoft.semantic-kernel</groupId>
<artifactId>semantickernel-samples-parent</artifactId>
<version>1.0.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>booking-agent-m365</artifactId>
<name>Booking agent with M365</name>
<description>Booking agent using M365</description>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.microsoft.semantic-kernel</groupId>
<artifactId>semantickernel-api</artifactId>
</dependency>
<dependency>
<groupId>com.microsoft.semantic-kernel</groupId>
<artifactId>semantickernel-aiservices-openai</artifactId>
</dependency>
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-identity</artifactId>
</dependency>
<dependency>
<groupId>com.microsoft.graph</groupId>
<artifactId>microsoft-graph</artifactId>
<version>6.5.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -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 > ");
}
}
}
@@ -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<BookingAppointment> 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<BookingAppointment> 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;
}
}
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="info">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss} %-5level %c{1.} - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<Logger name="reactor.netty" level="warn">
<AppenderRef ref="Console"/>
</Logger>
<Logger name="io.netty" level="warn">
<AppenderRef ref="Console"/>
</Logger>
<Logger name="com.azure.core.implementation" level="warn">
<AppenderRef ref="Console"/>
</Logger>
<Logger name="com.microsoft.semantickernel.orchestration.FunctionInvocation" level="info">
<AppenderRef ref="Console"/>
</Logger>
<Root level="warn">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>
+1
View File
@@ -16,6 +16,7 @@
<modules>
<module>sample-code</module>
<module>booking-agent-m365</module>
</modules>
<properties>