diff --git a/.doc_gen/metadata/bedrock-runtime_metadata.yaml b/.doc_gen/metadata/bedrock-runtime_metadata.yaml index a0c867e1753..bf60cc94bfb 100644 --- a/.doc_gen/metadata/bedrock-runtime_metadata.yaml +++ b/.doc_gen/metadata/bedrock-runtime_metadata.yaml @@ -189,6 +189,22 @@ bedrock-runtime_Scenario_ToolUse: synopsis: "build a typical interaction between an application, a generative AI model, and connected tools or APIs to mediate interactions between the AI and the outside world. It uses the example of connecting an external weather API to the AI model so it can provide real-time weather information based on user input." category: Scenarios languages: + Java: + versions: + - sdk_version: 2 + github: javav2/example_code/bedrock-runtime + excerpts: + - description: "The primary execution of the scenario flow. This scenario orchestrates the conversation between the user, the &BR; Converse API, and a weather tool." + snippet_tags: + - bedrock.converseTool.javav2.scenario + - description: "The weather tool used by the demo. This file defines the tool specification and implements the logic to retrieve weather data using from the Open-Meteo API." + genai: some + snippet_tags: + - bedrock.converseTool.javav2.weathertool + - description: "The Converse API action with a tool configuration." + genai: some + snippet_tags: + - bedrockruntime.java2.converse.main .NET: versions: - sdk_version: 3 diff --git a/.doc_gen/metadata/location_metadata.yaml b/.doc_gen/metadata/location_metadata.yaml index 323f4c73037..10c469a4c9c 100644 --- a/.doc_gen/metadata/location_metadata.yaml +++ b/.doc_gen/metadata/location_metadata.yaml @@ -5,6 +5,15 @@ location_Hello: synopsis: get started using &ALlong;. category: Hello languages: + Kotlin: + versions: + - sdk_version: 1 + github: kotlin/services/location + sdkguide: + excerpts: + - description: + snippet_tags: + - location.kotlin.hello.main Java: versions: - sdk_version: 2 @@ -18,6 +27,15 @@ location_Hello: location: {ListGeofencesPaginator} location_CreateMap: languages: + Kotlin: + versions: + - sdk_version: 1 + github: kotlin/services/location + sdkguide: + excerpts: + - description: + snippet_tags: + - location.kotlin.create.map.main Java: versions: - sdk_version: 2 @@ -31,6 +49,15 @@ location_CreateMap: location: {CreateMap} location_CreateKey: languages: + Kotlin: + versions: + - sdk_version: 1 + github: kotlin/services/location + sdkguide: + excerpts: + - description: + snippet_tags: + - location.kotlin.create.key.main Java: versions: - sdk_version: 2 @@ -44,6 +71,15 @@ location_CreateKey: location: {CreateKey} location_CreateGeofenceCollection: languages: + Kotlin: + versions: + - sdk_version: 1 + github: kotlin/services/location + sdkguide: + excerpts: + - description: + snippet_tags: + - location.kotlin.create.collection.main Java: versions: - sdk_version: 2 @@ -57,6 +93,15 @@ location_CreateGeofenceCollection: location: {CreateGeofenceCollection} location_PutGeofence: languages: + Kotlin: + versions: + - sdk_version: 1 + github: kotlin/services/location + sdkguide: + excerpts: + - description: + snippet_tags: + - location.kotlin.put.geo.main Java: versions: - sdk_version: 2 @@ -70,6 +115,15 @@ location_PutGeofence: location: {PutGeofence} location_CreateTracker: languages: + Kotlin: + versions: + - sdk_version: 1 + github: kotlin/services/location + sdkguide: + excerpts: + - description: + snippet_tags: + - location.kotlin.create.tracker.main Java: versions: - sdk_version: 2 @@ -83,6 +137,15 @@ location_CreateTracker: location: {CreateTracker} location_BatchUpdateDevicePosition: languages: + Kotlin: + versions: + - sdk_version: 1 + github: kotlin/services/location + sdkguide: + excerpts: + - description: + snippet_tags: + - location.kotlin.update.device.position.main Java: versions: - sdk_version: 2 @@ -96,6 +159,15 @@ location_BatchUpdateDevicePosition: location: {BatchUpdateDevicePosition} location_GetDevicePosition: languages: + Kotlin: + versions: + - sdk_version: 1 + github: kotlin/services/location + sdkguide: + excerpts: + - description: + snippet_tags: + - location.kotlin.get.device.position.main Java: versions: - sdk_version: 2 @@ -109,6 +181,15 @@ location_GetDevicePosition: location: {GetDevicePosition} location_CreateRouteCalculator: languages: + Kotlin: + versions: + - sdk_version: 1 + github: kotlin/services/location + sdkguide: + excerpts: + - description: + snippet_tags: + - location.kotlin.create.calculator.main Java: versions: - sdk_version: 2 @@ -122,6 +203,15 @@ location_CreateRouteCalculator: location: {CreateRouteCalculator} location_CalculateRoute: languages: + Kotlin: + versions: + - sdk_version: 1 + github: kotlin/services/location + sdkguide: + excerpts: + - description: + snippet_tags: + - location.kotlin.calc.distance.main Java: versions: - sdk_version: 2 @@ -135,6 +225,15 @@ location_CalculateRoute: location: {CalculateRoute} location_DeleteGeofenceCollection: languages: + Kotlin: + versions: + - sdk_version: 1 + github: kotlin/services/location + sdkguide: + excerpts: + - description: + snippet_tags: + - location.kotlin.delete.collection.main Java: versions: - sdk_version: 2 @@ -148,6 +247,15 @@ location_DeleteGeofenceCollection: location: {DeleteGeofenceCollection} location_DeleteKey: languages: + Kotlin: + versions: + - sdk_version: 1 + github: kotlin/services/location + sdkguide: + excerpts: + - description: + snippet_tags: + - location.kotlin.delete.key.main Java: versions: - sdk_version: 2 @@ -161,6 +269,15 @@ location_DeleteKey: location: {DeleteKey} location_DeleteMap: languages: + Kotlin: + versions: + - sdk_version: 1 + github: kotlin/services/location + sdkguide: + excerpts: + - description: + snippet_tags: + - location.kotlin.delete.map.main Java: versions: - sdk_version: 2 @@ -174,6 +291,15 @@ location_DeleteMap: location: {DeleteMap} location_DeleteTracker: languages: + Kotlin: + versions: + - sdk_version: 1 + github: kotlin/services/location + sdkguide: + excerpts: + - description: + snippet_tags: + - location.kotlin.delete.tracker.main Java: versions: - sdk_version: 2 @@ -187,6 +313,15 @@ location_DeleteTracker: location: {DeleteTracker} location_DeleteRouteCalculator: languages: + Kotlin: + versions: + - sdk_version: 1 + github: kotlin/services/location + sdkguide: + excerpts: + - description: + snippet_tags: + - location.kotlin.delete.calculator.main Java: versions: - sdk_version: 2 @@ -214,6 +349,15 @@ location_Scenario: - Delete the &ALshort; Assets. category: Basics languages: + Kotlin: + versions: + - sdk_version: 1 + github: kotlin/services/location + sdkguide: + excerpts: + - description: + snippet_tags: + - location.kotlin.scenario.main Java: versions: - sdk_version: 2 diff --git a/javav2/example_code/bedrock-runtime/pom.xml b/javav2/example_code/bedrock-runtime/pom.xml index 146df37aa94..0cb078244ee 100644 --- a/javav2/example_code/bedrock-runtime/pom.xml +++ b/javav2/example_code/bedrock-runtime/pom.xml @@ -45,6 +45,15 @@ software.amazon.awssdk sts + + com.fasterxml.jackson.core + jackson-databind + 2.16.1 + + + software.amazon.awssdk + netty-nio-client + org.json json diff --git a/javav2/example_code/bedrock-runtime/src/main/java/com/example/bedrockruntime/scenario/BedrockActions.java b/javav2/example_code/bedrock-runtime/src/main/java/com/example/bedrockruntime/scenario/BedrockActions.java new file mode 100644 index 00000000000..c0fcc4f3bbe --- /dev/null +++ b/javav2/example_code/bedrock-runtime/src/main/java/com/example/bedrockruntime/scenario/BedrockActions.java @@ -0,0 +1,104 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.example.bedrockruntime.scenario; + +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.core.retry.RetryMode; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.bedrockruntime.BedrockRuntimeAsyncClient; +import software.amazon.awssdk.services.bedrockruntime.model.ConverseRequest; +import software.amazon.awssdk.services.bedrockruntime.model.ConverseResponse; +import software.amazon.awssdk.services.bedrockruntime.model.Message; +import software.amazon.awssdk.services.bedrockruntime.model.SystemContentBlock; +import software.amazon.awssdk.services.bedrockruntime.model.Tool; +import software.amazon.awssdk.services.bedrockruntime.model.ToolConfiguration; +import software.amazon.awssdk.services.bedrockruntime.model.ToolSpecification; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +public class BedrockActions { + + private static volatile BedrockRuntimeAsyncClient bedrockRuntimeClient; + + private BedrockRuntimeAsyncClient getClient() { + if (bedrockRuntimeClient == null) { + /* + The `NettyNioAsyncHttpClient` class is part of the AWS SDK for Java, version 2, + and it is designed to provide a high-performance, asynchronous HTTP client for interacting with AWS services. + It uses the Netty framework to handle the underlying network communication and the Java NIO API to + provide a non-blocking, event-driven approach to HTTP requests and responses. + */ + + SdkAsyncHttpClient httpClient = NettyNioAsyncHttpClient.builder() + .maxConcurrency(50) // Adjust as needed. + .connectionTimeout(Duration.ofSeconds(60)) // Set the connection timeout. + .readTimeout(Duration.ofSeconds(60)) // Set the read timeout. + .writeTimeout(Duration.ofSeconds(60)) // Set the write timeout. + .build(); + + ClientOverrideConfiguration overrideConfig = ClientOverrideConfiguration.builder() + .apiCallTimeout(Duration.ofMinutes(2)) // Set the overall API call timeout. + .apiCallAttemptTimeout(Duration.ofSeconds(90)) // Set the individual call attempt timeout. + .retryStrategy(RetryMode.STANDARD) + .build(); + + bedrockRuntimeClient = BedrockRuntimeAsyncClient.builder() + .region(Region.US_EAST_1) + .httpClient(httpClient) + .overrideConfiguration(overrideConfig) + .build(); + } + return bedrockRuntimeClient; + } + + // snippet-start:[bedrockruntime.java2.converse.main] + /** + * Sends an asynchronous converse request to the AI model. + * + * @param modelId the unique identifier of the AI model to be used for the converse request + * @param systemPrompt the system prompt to be included in the converse request + * @param conversation a list of messages representing the conversation history + * @param toolSpec the specification of the tool to be used in the converse request + * @return the converse response received from the AI model + */ + public ConverseResponse sendConverseRequestAsync(String modelId, String systemPrompt, List conversation, ToolSpecification toolSpec) { + List toolList = new ArrayList<>(); + Tool tool = Tool.builder() + .toolSpec(toolSpec) + .build(); + + toolList.add(tool); + + ToolConfiguration configuration = ToolConfiguration.builder() + .tools(toolList) + .build(); + + SystemContentBlock block = SystemContentBlock.builder() + .text(systemPrompt) + .build(); + + ConverseRequest request = ConverseRequest.builder() + .modelId(modelId) + .system(block) + .messages(conversation) + .toolConfig(configuration) + .build(); + + try { + ConverseResponse response = getClient().converse(request).join(); + return response; + + } catch (Exception ex) { + ex.printStackTrace(); + } + + return null; + } + // snippet-end:[bedrockruntime.java2.converse.main] +} + + diff --git a/javav2/example_code/bedrock-runtime/src/main/java/com/example/bedrockruntime/scenario/BedrockScenario.java b/javav2/example_code/bedrock-runtime/src/main/java/com/example/bedrockruntime/scenario/BedrockScenario.java new file mode 100644 index 00000000000..9df0e92e008 --- /dev/null +++ b/javav2/example_code/bedrock-runtime/src/main/java/com/example/bedrockruntime/scenario/BedrockScenario.java @@ -0,0 +1,315 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.example.bedrockruntime.scenario; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Scanner; +import software.amazon.awssdk.core.document.Document; +import software.amazon.awssdk.services.bedrockruntime.model.ContentBlock; +import software.amazon.awssdk.services.bedrockruntime.model.ConversationRole; +import software.amazon.awssdk.services.bedrockruntime.model.ConverseOutput; +import software.amazon.awssdk.services.bedrockruntime.model.ConverseResponse; +import software.amazon.awssdk.services.bedrockruntime.model.Message; +import software.amazon.awssdk.services.bedrockruntime.model.ToolResultBlock; +import software.amazon.awssdk.services.bedrockruntime.model.ToolResultContentBlock; +import software.amazon.awssdk.services.bedrockruntime.model.ToolSpecification; +import software.amazon.awssdk.services.bedrockruntime.model.ToolUseBlock; + +// snippet-start:[bedrock.converseTool.javav2.scenario] +/* + This demo illustrates a tool use scenario using Amazon Bedrock's Converse API and a weather tool. + The program interacts with a foundation model on Amazon Bedrock to provide weather information based on user + input. It uses the Open-Meteo API (https://open-meteo.com) to retrieve current weather data for a given location. + */ +public class BedrockScenario { + public static final String DASHES = new String(new char[80]).replace("\0", "-"); + private static String modelId = "anthropic.claude-3-sonnet-20240229-v1:0"; + private static String defaultPrompt = "What is the weather like in Seattle?"; + private static WeatherTool weatherTool = new WeatherTool(); + + // The maximum number of recursive calls allowed in the tool use function. + // This helps prevent infinite loops and potential performance issues. + private static int maxRecursions = 5; + static BedrockActions bedrockActions = new BedrockActions(); + public static boolean interactive = true; + + private static final String systemPrompt = """ + You are a weather assistant that provides current weather data for user-specified locations using only + the Weather_Tool, which expects latitude and longitude. Infer the coordinates from the location yourself. + If the user provides coordinates, infer the approximate location and refer to it in your response. + To use the tool, you strictly apply the provided tool specification. + + - Explain your step-by-step process, and give brief updates before each step. + - Only use the Weather_Tool for data. Never guess or make up information. + - Repeat the tool use for subsequent requests if necessary. + - If the tool errors, apologize, explain weather is unavailable, and suggest other options. + - Report temperatures in °C (°F) and wind in km/h (mph). Keep weather reports concise. Sparingly use + emojis where appropriate. + - Only respond to weather queries. Remind off-topic users of your purpose. + - Never claim to search online, access external data, or use tools besides Weather_Tool. + - Complete the entire process until you have all required data before sending the complete response. + """; + + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + System.out.println(""" + ================================================= + Welcome to the Amazon Bedrock Tool Use demo! + ================================================= + + This assistant provides current weather information for user-specified locations. + You can ask for weather details by providing the location name or coordinates. + + Example queries: + - What's the weather like in New York? + - Current weather for latitude 40.70, longitude -74.01 + - Is it warmer in Rome or Barcelona today? + + To exit the program, simply type 'x' and press Enter. + + P.S.: You're not limited to single locations, or even to using English! + Have fun and experiment with the app! + """); + System.out.println(DASHES); + + try { + runConversation(scanner); + + } catch (Exception ex) { + System.out.println("There was a problem running the scenario: "+ ex.getMessage()); + } + + waitForInputToContinue(scanner); + + System.out.println(DASHES); + System.out.println("Amazon Bedrock Converse API with Tool Use Feature Scenario is complete."); + System.out.println(DASHES); + } + + /** + Starts the conversation with the user and handles the interaction with Bedrock. + */ + private static List runConversation(Scanner scanner) { + List conversation = new ArrayList<>(); + + // Get the first user input + String userInput = getUserInput("Your weather info request:", scanner); + System.out.println(userInput); + + while (userInput != null) { + ContentBlock block = ContentBlock.builder() + .text(userInput) + .build(); + + List blockList = new ArrayList<>(); + blockList.add(block); + + Message message = Message.builder() + .role(ConversationRole.USER) + .content(blockList) + .build(); + + conversation.add(message); + + // Send the conversation to Amazon Bedrock. + ConverseResponse bedrockResponse = sendConversationToBedrock(conversation); + + // Recursively handle the model's response until the model has returned its final response or the recursion counter has reached 0. + processModelResponse(bedrockResponse, conversation, maxRecursions); + + // Repeat the loop until the user decides to exit the application. + userInput = getUserInput("Your weather info request:", scanner); + } + printFooter(); + return conversation; + } + + /** + * Processes the response from the model and updates the conversation accordingly. + * + * @param modelResponse the response from the model + * @param conversation the ongoing conversation + * @param maxRecursion the maximum number of recursions allowed + */ + private static void processModelResponse(ConverseResponse modelResponse, List conversation, int maxRecursion) { + if (maxRecursion <= 0) { + // Stop the process, the number of recursive calls could indicate an infinite loop + System.out.println("\tWarning: Maximum number of recursions reached. Please try again."); + } + + // Append the model's response to the ongoing conversation + conversation.add(modelResponse.output().message()); + + String modelResponseVal = modelResponse.stopReasonAsString(); + if (modelResponseVal.compareTo("tool_use") == 0) { + // If the stop reason is "tool_use", forward everything to the tool use handler + handleToolUse(modelResponse.output(), conversation, maxRecursion - 1); + } + + if (modelResponseVal.compareTo ("end_turn") ==0) { + // If the stop reason is "end_turn", print the model's response text, and finish the process + PrintModelResponse(modelResponse.output().message().content().get(0).text()); + if (!interactive) { + defaultPrompt = "x"; + } + } + } + + /** + * Handles the use of a tool by the model in a conversation. + * + * @param modelResponse the response from the model, which may include a tool use request + * @param conversation the current conversation, which will be updated with the tool use results + * @param maxRecursion the maximum number of recursive calls allowed to handle the model's response + */ + private static void handleToolUse(ConverseOutput modelResponse, List conversation, int maxRecursion) { + List toolResults = new ArrayList<>(); + + // The model's response can consist of multiple content blocks + for (ContentBlock contentBlock : modelResponse.message().content()) { + if (contentBlock.text() != null && !contentBlock.text().isEmpty()) { + // If the content block contains text, print it to the console + PrintModelResponse(contentBlock.text()); + } + + if (contentBlock.toolUse() != null) { + ToolResponse toolResponse = invokeTool(contentBlock.toolUse()); + + // Add the tool use ID and the tool's response to the list of results + List contentBlockList = new ArrayList<>(); + ToolResultContentBlock block = ToolResultContentBlock.builder() + .json(toolResponse.getContent()) + .build(); + contentBlockList.add(block); + + ToolResultBlock toolResultBlock = ToolResultBlock.builder() + .toolUseId(toolResponse.getToolUseId()) + .content(contentBlockList) + .build(); + + ContentBlock contentBlock1 = ContentBlock.builder() + .toolResult(toolResultBlock) + .build(); + + toolResults.add(contentBlock1); + } + } + + // Embed the tool results in a new user message + Message message = Message.builder() + .role(ConversationRole.USER) + .content(toolResults) + .build(); + + // Append the new message to the ongoing conversation + //conversation.add(message); + conversation.add(message); + + // Send the conversation to Amazon Bedrock + var response = sendConversationToBedrock(conversation); + + // Recursively handle the model's response until the model has returned its final response or the recursion counter has reached 0 + processModelResponse(response, conversation, maxRecursion); + } + + // Invokes the specified tool with the given payload and returns the tool's response. + // If the requested tool does not exist, an error message is returned. + private static ToolResponse invokeTool(ToolUseBlock payload) { + String toolName = payload.name(); + + if (Objects.equals(toolName, "Weather_Tool")){ + Map inputData = payload.input().asMap(); + printToolUse(toolName, inputData); + + // Invoke the weather tool with the input data provided + Document weatherResponse = weatherTool.fetchWeatherData(inputData.get("latitude").toString(), inputData.get("longitude").toString()); + + ToolResponse toolResponse = new ToolResponse(); + toolResponse.setContent(weatherResponse); + toolResponse.setToolUseId(payload.toolUseId()); + return toolResponse; + } else { + String errorMessage = "The requested tool with name "+toolName +" does not exist."; + System.out.println(errorMessage); + return null; + } + } + + public static void printToolUse(String toolName, Map inputData) { + System.out.println("Invoking tool: " + toolName + " with input: " + inputData.get("latitude").toString() + ", " + inputData.get("longitude").toString() + "..."); + } + + private static void PrintModelResponse(String message) { + System.out.println("\tThe model's response:\n"); + System.out.println(message); + System.out.println(""); + } + + private static ConverseResponse sendConversationToBedrock(List conversation) { + System.out.println("Calling Bedrock..."); + + // Send the conversation, system prompt, and tool configuration, and return the response + return bedrockActions.sendConverseRequestAsync(modelId, systemPrompt, conversation, weatherTool.getToolSpec()); + } + + private static ConverseResponse sendConversationToBedrockwithSpec(List conversation, ToolSpecification toolSpec) { + System.out.println("Calling Bedrock..."); + + // Send the conversation, system prompt, and tool configuration, and return the response + return bedrockActions.sendConverseRequestAsync(modelId, systemPrompt, conversation, toolSpec); + } + + public static String getUserInput(String prompt, Scanner scanner) { + String userInput = defaultPrompt; + if (interactive) { + System.out.println("*".repeat(80)); + System.out.println(prompt + " (x to exit): \n\t"); + userInput = scanner.nextLine(); + } + + if (userInput == null || userInput.trim().isEmpty()) { + return getUserInput("\tPlease enter your weather info request, e.g., the name of a city", scanner); + } + + if (userInput.equalsIgnoreCase("x")) { + return null; + } + + return userInput; + } + + private static void waitForInputToContinue(Scanner scanner) { + while (true) { + System.out.println(""); + System.out.println("Enter 'c' followed by to continue:"); + String input = scanner.nextLine(); + + if (input.trim().equalsIgnoreCase("c")) { + System.out.println("Continuing with the program..."); + System.out.println(""); + break; + } else { + // Handle invalid input. + System.out.println("Invalid input. Please try again."); + } + } + } + + public static void printFooter() + { + System.out.println(""" + ================================================= + Thank you for checking out the Amazon Bedrock Tool Use demo. We hope you + learned something new, or got some inspiration for your own apps today! + + For more Bedrock examples in different programming languages, have a look at: + https://docs.aws.amazon.com/bedrock/latest/userguide/service_code_examples.html + ================================================= + """); + } +} +// snippet-end:[bedrock.converseTool.javav2.scenario] \ No newline at end of file diff --git a/javav2/example_code/bedrock-runtime/src/main/java/com/example/bedrockruntime/scenario/ToolResponse.java b/javav2/example_code/bedrock-runtime/src/main/java/com/example/bedrockruntime/scenario/ToolResponse.java new file mode 100644 index 00000000000..0a71041b0c0 --- /dev/null +++ b/javav2/example_code/bedrock-runtime/src/main/java/com/example/bedrockruntime/scenario/ToolResponse.java @@ -0,0 +1,27 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.example.bedrockruntime.scenario; + +import software.amazon.awssdk.core.document.Document; + +public class ToolResponse { + private String toolUseId; + private Document content; + + public String getToolUseId() { + return toolUseId; + } + + public void setToolUseId(String toolUseId) { + this.toolUseId = toolUseId; + } + + public Document getContent() { + return content; + } + + public void setContent(Document content) { + this.content = content; + } +} \ No newline at end of file diff --git a/javav2/example_code/bedrock-runtime/src/main/java/com/example/bedrockruntime/scenario/WeatherTool.java b/javav2/example_code/bedrock-runtime/src/main/java/com/example/bedrockruntime/scenario/WeatherTool.java new file mode 100644 index 00000000000..f8333b00109 --- /dev/null +++ b/javav2/example_code/bedrock-runtime/src/main/java/com/example/bedrockruntime/scenario/WeatherTool.java @@ -0,0 +1,164 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.example.bedrockruntime.scenario; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.json.JSONArray; +import software.amazon.awssdk.core.SdkNumber; +import software.amazon.awssdk.core.document.Document; +import software.amazon.awssdk.services.bedrockruntime.model.ToolSpecification; +import software.amazon.awssdk.services.bedrockruntime.model.ToolInputSchema; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.math.BigInteger; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +import org.json.JSONObject; + +// snippet-start:[bedrock.converseTool.javav2.weathertool] +public class WeatherTool { + + private static final Logger logger = LoggerFactory.getLogger(WeatherTool.class); + private static java.net.http.HttpClient httpClient = null; + + + /** + * Returns the JSON Schema specification for the Weather tool. The tool specification + * defines the input schema and describes the tool's functionality. + * For more information, see https://json-schema.org/understanding-json-schema/reference. + * + * @return The tool specification for the Weather tool. + */ + public ToolSpecification getToolSpec() { + // Build the JSON schema using JSONObject and JSONArray + Map latitudeMap = new HashMap<>(); + latitudeMap.put("type", Document.fromString("string")); + latitudeMap.put("description", Document.fromString("Geographical WGS84 latitude of the location.")); + + // Create the nested "longitude" object + Map longitudeMap = new HashMap<>(); + longitudeMap.put("type", Document.fromString("string")); + longitudeMap.put("description", Document.fromString("Geographical WGS84 longitude of the location.")); + + // Create the "properties" object + Map propertiesMap = new HashMap<>(); + propertiesMap.put("latitude", Document.fromMap(latitudeMap)); + propertiesMap.put("longitude", Document.fromMap(longitudeMap)); + + // Create the "required" array + List requiredList = new ArrayList<>(); + requiredList.add(Document.fromString("latitude")); + requiredList.add(Document.fromString("longitude")); + + // Create the root object + Map rootMap = new HashMap<>(); + rootMap.put("type", Document.fromString("object")); + rootMap.put("properties", Document.fromMap(propertiesMap)); + rootMap.put("required", Document.fromList(requiredList)); + + // Now create the Document representing the JSON schema + Document document = Document.fromMap(rootMap); + + ToolSpecification specification = ToolSpecification.builder() + .name("Weather_Tool") + .description("Get the current weather for a given location, based on its WGS84 coordinates.") + .inputSchema(ToolInputSchema.builder() + .json(document) + .build()) + .build(); + + return specification; + } + + /** + * Fetches weather data for the given latitude and longitude. + * + * @param latitude the latitude coordinate + * @param longitude the longitude coordinate + * @return a {@link CompletableFuture} containing the weather data as a JSON string + */ + public Document fetchWeatherData(String latitude, String longitude) { + HttpClient httpClient = HttpClient.newHttpClient(); + + // Ensure no extra double quotes + latitude = latitude.replace("\"", ""); + longitude = longitude.replace("\"", ""); + + String endpoint = "https://api.open-meteo.com/v1/forecast"; + String url = String.format("%s?latitude=%s&longitude=%s¤t_weather=True", endpoint, latitude, longitude); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .build(); + + try { + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() == 200) { + // Convert response body to AWS SDK Document. At this point, + // its valid JSON + String weatherJson = response.body(); + System.out.println(weatherJson); + + // Convert JSON string to a Document + //Document weatherDocument = Document.fromString(weatherJson); + ObjectMapper objectMapper = new ObjectMapper(); + //Map jsonMap = objectMapper.readValue(weatherJson, Map.class); + + // + Map rawMap = objectMapper.readValue(weatherJson, new TypeReference>() {}); + Map documentMap = convertToDocumentMap(rawMap); + + // Create a Document object from the Map + Document weatherDocument = Document.fromMap(documentMap); + // Document weatherDocument = Document.fromMap(jsonMap); + + System.out.println(weatherDocument); + return weatherDocument; + } else { + throw new RuntimeException("Error fetching weather data: " + response.statusCode()); + } + } catch (Exception e) { + System.out.println("Error fetching weather data: " + e.getMessage()); + throw new RuntimeException("Error fetching weather data", e); + } + + } + + private static Map convertToDocumentMap(Map inputMap) { + Map result = new HashMap<>(); + for (Map.Entry entry : inputMap.entrySet()) { + result.put(entry.getKey(), convertToDocument(entry.getValue())); + } + return result; + } + + // Convert different types of Objects to Document + // Convert different types of Objects to Document + private static Document convertToDocument(Object value) { + if (value instanceof Map) { + return Document.fromMap(convertToDocumentMap((Map) value)); + } else if (value instanceof Integer) { // ✅ Fix: Use fromInteger() for integers + return Document.fromNumber(SdkNumber.fromInteger((Integer) value)); + } else if (value instanceof Double) { // ✅ Fix: Use fromDouble() for floating-point numbers + return Document.fromNumber(SdkNumber.fromDouble((Double) value)); + } else if (value instanceof Boolean) { + return Document.fromBoolean((Boolean) value); + } else if (value instanceof String) { + return Document.fromString((String) value); + } + return Document.fromNull(); // Handle null values safely + } +} +// snippet-end:[bedrock.converseTool.javav2.weathertool] diff --git a/javav2/example_code/bedrock-runtime/src/test/java/scenarios/TestBedrockTool.java b/javav2/example_code/bedrock-runtime/src/test/java/scenarios/TestBedrockTool.java new file mode 100644 index 00000000000..8100454cfe0 --- /dev/null +++ b/javav2/example_code/bedrock-runtime/src/test/java/scenarios/TestBedrockTool.java @@ -0,0 +1,51 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package scenarios; + +import com.example.bedrockruntime.scenario.BedrockActions; +import com.example.bedrockruntime.scenario.WeatherTool; +import org.junit.jupiter.api.*; +import software.amazon.awssdk.services.bedrockruntime.model.ContentBlock; +import software.amazon.awssdk.services.bedrockruntime.model.ConversationRole; +import software.amazon.awssdk.services.bedrockruntime.model.ConverseResponse; +import software.amazon.awssdk.services.bedrockruntime.model.Message; +import java.util.ArrayList; +import java.util.List; +import static org.junit.jupiter.api.Assertions.*; + +@TestInstance(TestInstance.Lifecycle.PER_METHOD) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class TestBedrockTool { + private static String prompt = "How is the weather in New York"; + static BedrockActions bedrockActions = new BedrockActions(); + private static WeatherTool weatherTool = new WeatherTool(); + private static String modelId = "anthropic.claude-3-sonnet-20240229-v1:0"; + + @Test + @Tag("IntegrationTest") + @Order(1) + public void testCreateAssetModel() { + List conversation = new ArrayList<>(); + ContentBlock block = ContentBlock.builder() + .text(prompt) + .build(); + + List blockList = new ArrayList<>(); + blockList.add(block); + + Message message = Message.builder() + .role(ConversationRole.USER) + .content(blockList) + .build(); + + conversation.add(message); + ConverseResponse bedrockResponse = bedrockActions.sendConverseRequestAsync(modelId, prompt, conversation, weatherTool.getToolSpec()); + + // Assertions + assertNotNull(bedrockResponse, "Response should not be null"); + assertNotNull(bedrockResponse.output().message(), "Output text should not be null"); + System.out.println("Test 1 passed"); + } +} + diff --git a/kotlin/services/location/.gitignore b/kotlin/services/location/.gitignore new file mode 100644 index 00000000000..b63da4551b2 --- /dev/null +++ b/kotlin/services/location/.gitignore @@ -0,0 +1,42 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/kotlin/services/location/README.md b/kotlin/services/location/README.md new file mode 100644 index 00000000000..1f5ca7d540d --- /dev/null +++ b/kotlin/services/location/README.md @@ -0,0 +1,131 @@ +# Amazon Location code examples for the SDK for Kotlin + +## Overview + +Shows how to use the AWS SDK for Kotlin to work with Amazon Location Service. + + + + +_Amazon Location lets you easily and securely add maps, places, routes, geofences, and trackers, to your applications._ + +## ⚠ Important + +* Running this code might result in charges to your AWS account. For more details, see [AWS Pricing](https://aws.amazon.com/pricing/) and [Free Tier](https://aws.amazon.com/free/). +* Running the tests might result in charges to your AWS account. +* We recommend that you grant your code least privilege. At most, grant only the minimum permissions required to perform the task. For more information, see [Grant least privilege](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege). +* This code is not tested in every AWS Region. For more information, see [AWS Regional Services](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services). + + + + +## Code examples + +### Prerequisites + +For prerequisites, see the [README](../../README.md#Prerequisites) in the `kotlin` folder. + + + + + +### Get started + +- [Hello Amazon Location](src/main/java/com/example/location/HelloLocation.kt#L10) (`ListGeofencesPaginator`) + + +### Basics + +Code examples that show you how to perform the essential operations within a service. + +- [Learn the basics](src/main/java/com/example/location/scenario/LocationScenario.kt) + + +### Single actions + +Code excerpts that show you how to call individual service functions. + +- [BatchUpdateDevicePosition](src/main/java/com/example/location/scenario/LocationScenario.kt#L567) +- [CalculateRoute](src/main/java/com/example/location/scenario/LocationScenario.kt#L501) +- [CreateGeofenceCollection](src/main/java/com/example/location/scenario/LocationScenario.kt#L648) +- [CreateKey](src/main/java/com/example/location/scenario/LocationScenario.kt#L667) +- [CreateMap](src/main/java/com/example/location/scenario/LocationScenario.kt#L694) +- [CreateRouteCalculator](src/main/java/com/example/location/scenario/LocationScenario.kt#L528) +- [CreateTracker](src/main/java/com/example/location/scenario/LocationScenario.kt#L595) +- [DeleteGeofenceCollection](src/main/java/com/example/location/scenario/LocationScenario.kt#L335) +- [DeleteKey](src/main/java/com/example/location/scenario/LocationScenario.kt#L355) +- [DeleteMap](src/main/java/com/example/location/scenario/LocationScenario.kt#L373) +- [DeleteRouteCalculator](src/main/java/com/example/location/scenario/LocationScenario.kt#L300) +- [DeleteTracker](src/main/java/com/example/location/scenario/LocationScenario.kt#L317) +- [GetDevicePosition](src/main/java/com/example/location/scenario/LocationScenario.kt#L548) +- [PutGeofence](src/main/java/com/example/location/scenario/LocationScenario.kt#L616) + + + + + +## Run the examples + +### Instructions + + + + + +#### Hello Amazon Location + +This example shows you how to get started using Amazon Location. + + +#### Learn the basics + +This example shows you how to do the following: + +- Create an Amazon Location map. +- Create an Amazon Location API key. +- Display Map URL. +- Create a geofence collection. +- Store a geofence geometry. +- Create a tracker resource. +- Update the position of a device. +- Retrieve the most recent position update for a specified device. +- Create a route calculator. +- Determine the distance between Seattle and Vancouver. +- Use Amazon Location higher level APIs. +- Delete the Amazon Location Assets. + + + + + + + + + +### Tests + +⚠ Running tests might result in charges to your AWS account. + + +To find instructions for running these tests, see the [README](../../README.md#Tests) +in the `kotlin` folder. + + + + + + +## Additional resources + +- [Amazon Location Developer Guide](https://docs.aws.amazon.com/location/latest/developerguide/what-is.html) +- [Amazon Location API Reference](https://docs.aws.amazon.com/location/latest/APIReference/Welcome.html) +- [SDK for Kotlin Amazon Location reference](https://sdk.amazonaws.com/kotlin/api/latest/location/index.html) + + + + +--- + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 diff --git a/kotlin/services/location/build.gradle.kts b/kotlin/services/location/build.gradle.kts new file mode 100644 index 00000000000..b7809f8aa78 --- /dev/null +++ b/kotlin/services/location/build.gradle.kts @@ -0,0 +1,56 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + kotlin("jvm") version "1.9.0" + application +} + +group = "me.scmacdon" +version = "1.0-SNAPSHOT" + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +buildscript { + repositories { + maven("https://plugins.gradle.org/m2/") + } + + dependencies { + classpath("org.jlleitschuh.gradle:ktlint-gradle:10.3.0") + } +} + +repositories { + mavenCentral() +} +apply(plugin = "org.jlleitschuh.gradle.ktlint") + +dependencies { + implementation(platform("aws.sdk.kotlin:bom:1.3.112")) + implementation("aws.sdk.kotlin:location") + implementation("aws.sdk.kotlin:geoplaces") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") + implementation("com.google.code.gson:gson:2.10") + testImplementation("org.junit.jupiter:junit-jupiter:5.9.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + implementation("org.slf4j:slf4j-api:2.0.15") + implementation("org.slf4j:slf4j-simple:2.0.15") +} +tasks.withType { + kotlinOptions.jvmTarget = "17" +} + +tasks.test { + useJUnitPlatform() // Use JUnit 5 for running tests + testLogging { + events("passed", "skipped", "failed") + } + + // Define the test source set + testClassesDirs += files("build/classes/kotlin/test") + classpath += files("build/classes/kotlin/main", "build/resources/main") +} diff --git a/kotlin/services/location/settings.gradle.kts b/kotlin/services/location/settings.gradle.kts new file mode 100644 index 00000000000..cd2e7c94fa3 --- /dev/null +++ b/kotlin/services/location/settings.gradle.kts @@ -0,0 +1,9 @@ +pluginManagement { + plugins { + kotlin("jvm") version "2.1.10" + } +} +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" +} +rootProject.name = "location" diff --git a/kotlin/services/location/src/main/java/com/example/location/HelloLocation.kt b/kotlin/services/location/src/main/java/com/example/location/HelloLocation.kt new file mode 100644 index 00000000000..4b56727537b --- /dev/null +++ b/kotlin/services/location/src/main/java/com/example/location/HelloLocation.kt @@ -0,0 +1,64 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.example.location + +import aws.sdk.kotlin.services.location.LocationClient +import aws.sdk.kotlin.services.location.model.ListGeofencesRequest +import kotlin.system.exitProcess + +// snippet-start:[location.kotlin.hello.main] +/** +Before running this Kotlin code example, set up your development environment, +including your credentials. + +For more information, see the following documentation topic: +https://docs.aws.amazon.com/sdk-for-kotlin/latest/developer-guide/setup.html + +In addition, you need to create a collection using the AWS Management +console. For information, see the following documentation. + +https://docs.aws.amazon.com/location/latest/developerguide/geofence-gs.html + + */ +suspend fun main(args: Array) { + val usage = """ + + Usage: + + + Where: + colletionName - The Amazon location collection name. + """ + + if (args.size != 1) { + println(usage) + exitProcess(0) + } + val colletionName = args[0] + listGeofences(colletionName) +} + +/** + * Lists the geofences for the specified collection name. + * + * @param collectionName the name of the geofence collection + */ +suspend fun listGeofences(collectionName: String) { + val request = ListGeofencesRequest { + this.collectionName = collectionName + } + + LocationClient { region = "us-east-1" }.use { client -> + val response = client.listGeofences(request) + val geofences = response.entries + if (geofences.isNullOrEmpty()) { + println("No Geofences found") + } else { + geofences.forEach { geofence -> + println("Geofence ID: ${geofence.geofenceId}") + } + } + } +} +// snippet-end:[location.kotlin.hello.main] diff --git a/kotlin/services/location/src/main/java/com/example/location/scenario/LocationScenario.kt b/kotlin/services/location/src/main/java/com/example/location/scenario/LocationScenario.kt new file mode 100644 index 00000000000..2efed43a13a --- /dev/null +++ b/kotlin/services/location/src/main/java/com/example/location/scenario/LocationScenario.kt @@ -0,0 +1,729 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.example.location.scenario + +import aws.sdk.kotlin.services.geoplaces.GeoPlacesClient +import aws.sdk.kotlin.services.geoplaces.model.GetPlaceRequest +import aws.sdk.kotlin.services.geoplaces.model.ReverseGeocodeRequest +import aws.sdk.kotlin.services.geoplaces.model.SearchNearbyRequest +import aws.sdk.kotlin.services.geoplaces.model.SearchTextRequest +import aws.sdk.kotlin.services.location.LocationClient +import aws.sdk.kotlin.services.location.model.ApiKeyRestrictions +import aws.sdk.kotlin.services.location.model.BatchUpdateDevicePositionRequest +import aws.sdk.kotlin.services.location.model.CalculateRouteRequest +import aws.sdk.kotlin.services.location.model.CalculateRouteResponse +import aws.sdk.kotlin.services.location.model.CreateGeofenceCollectionRequest +import aws.sdk.kotlin.services.location.model.CreateKeyRequest +import aws.sdk.kotlin.services.location.model.CreateMapRequest +import aws.sdk.kotlin.services.location.model.CreateRouteCalculatorRequest +import aws.sdk.kotlin.services.location.model.CreateRouteCalculatorResponse +import aws.sdk.kotlin.services.location.model.CreateTrackerRequest +import aws.sdk.kotlin.services.location.model.DeleteGeofenceCollectionRequest +import aws.sdk.kotlin.services.location.model.DeleteKeyRequest +import aws.sdk.kotlin.services.location.model.DeleteMapRequest +import aws.sdk.kotlin.services.location.model.DeleteRouteCalculatorRequest +import aws.sdk.kotlin.services.location.model.DeleteTrackerRequest +import aws.sdk.kotlin.services.location.model.DevicePositionUpdate +import aws.sdk.kotlin.services.location.model.DistanceUnit +import aws.sdk.kotlin.services.location.model.GeofenceGeometry +import aws.sdk.kotlin.services.location.model.GetDevicePositionRequest +import aws.sdk.kotlin.services.location.model.GetDevicePositionResponse +import aws.sdk.kotlin.services.location.model.MapConfiguration +import aws.sdk.kotlin.services.location.model.PositionFiltering +import aws.sdk.kotlin.services.location.model.PutGeofenceRequest +import aws.sdk.kotlin.services.location.model.TravelMode +import java.util.Scanner +import kotlin.system.exitProcess + +// snippet-start:[location.kotlin.scenario.main] +/** +Before running this Kotlin code example, set up your development environment, +including your credentials. + +For more information, see the following documentation topic: +https://docs.aws.amazon.com/sdk-for-kotlin/latest/developer-guide/setup.html + */ + +val scanner = Scanner(System.`in`) +val DASHES = String(CharArray(80)).replace("\u0000", "-") +suspend fun main(args: Array) { + val usage = """ + + Usage: + + Where: + mapName - The name of the map to create (e.g., "AWSMap"). + keyName - The name of the API key to create (e.g., "AWSApiKey"). + collectionName - The name of the geofence collection (e.g., "AWSLocationCollection"). + geoId - The geographic identifier used for the geofence or map (e.g., "geoId"). + trackerName - The name of the tracker (e.g., "geoTracker"). + calculatorName - The name of the route calculator (e.g., "AWSRouteCalc"). + deviceId - The ID of the device (e.g., "iPhone-112356"). + """ + + if (args.size != 7) { + println(usage) + exitProcess(0) + } + + val mapName = args[0] + val keyName = args[1] + val collectionName = args[2] + val geoId = args[3] + val trackerName = args[4] + val calculatorName = args[5] + val deviceId = args[6] + + println( + """ + AWS Location Service is a fully managed service offered by Amazon Web Services (AWS) that + provides location-based services for developers. This service simplifies + the integration of location-based features into applications, making it + easier to build and deploy location-aware applications. + + The AWS Location Service offers a range of location-based services, + including: + + - Maps: The service provides access to high-quality maps, satellite imagery, + and geospatial data from various providers, allowing developers to + easily embed maps into their applications. + + - Tracking: The Location Service enables real-time tracking of mobile devices, + assets, or other entities, allowing developers to build applications + that can monitor the location of people, vehicles, or other objects. + + - Geocoding: The service provides the ability to convert addresses or + location names into geographic coordinates (latitude and longitude), + and vice versa, enabling developers to integrate location-based search + and routing functionality into their applications. + """.trimIndent(), + ) + + waitForInputToContinue(scanner) + println(DASHES) + println("1. Create an AWS Location Service map") + println( + """ + An AWS Location map can enhance the user experience of your + application by providing accurate and personalized location-based + features. For example, you could use the geocoding capabilities to + allow users to search for and locate businesses, landmarks, or + other points of interest within a specific region. + + """.trimIndent(), + ) + + waitForInputToContinue(scanner) + val mapArn = createMap(mapName) + println("The Map ARN is: $mapArn") + waitForInputToContinue(scanner) + println(DASHES) + + waitForInputToContinue(scanner) + println("2. Create an AWS Location API key") + println( + """ + When you embed a map in a web app or website, the API key is + included in the map tile URL to authenticate requests. You can + restrict API keys to specific AWS Location operations (e.g., only + maps, not geocoding). API keys can expire, ensuring temporary + access control. + + """.trimIndent(), + ) + val keyArn = createKey(keyName, mapArn) + println("The Key ARN is: $keyArn") + waitForInputToContinue(scanner) + println(DASHES) + + println(DASHES) + println("3. Display Map URL") + println( + """ + In order to get the MAP URL, you need to get the API Key value. + You can get the key value using the AWS Management Console under + Location Services. This operation cannot be completed using the + AWS SDK. For more information about getting the key value, see + the AWS Location Documentation. + """.trimIndent(), + ) + val mapUrl = "https://maps.geo.aws.amazon.com/maps/v0/maps/$mapName/tiles/{z}/{x}/{y}?key={KeyValue}" + println("Embed this URL in your Web app: $mapUrl") + println("") + waitForInputToContinue(scanner) + println(DASHES) + + println(DASHES) + println("4. Create a geofence collection, which manages and stores geofences.") + waitForInputToContinue(scanner) + val collectionArn: String = + createGeofenceCollection(collectionName) + println("The geofence collection was successfully created: $collectionArn") + waitForInputToContinue(scanner) + + println(DASHES) + println("5. Store a geofence geometry in a given geofence collection.") + println( + """ + An AWS Location geofence is a virtual boundary that defines a geographic area + on a map. It is a useful feature for tracking the location of + assets or monitoring the movement of objects within a specific region. + + To define a geofence, you need to specify the coordinates of a + polygon that represents the area of interest. The polygon must be + defined in a counter-clockwise direction, meaning that the points of + the polygon must be listed in a counter-clockwise order. + + This is a requirement for the AWS Location service to correctly + interpret the geofence and ensure that the location data is + accurately processed within the defined area. + """.trimIndent(), + ) + + waitForInputToContinue(scanner) + putGeofence(collectionName, geoId) + println("Successfully created geofence: $geoId") + waitForInputToContinue(scanner) + println(DASHES) + + println(DASHES) + println("6. Create a tracker resource which lets you retrieve current and historical location of devices.") + waitForInputToContinue(scanner) + val trackerArn: String = createTracker(trackerName) + println("Successfully created tracker. ARN: $trackerArn") + waitForInputToContinue(scanner) + println(DASHES) + + println(DASHES) + println("7. Update the position of a device in the location tracking system.") + println( + """ + The AWS location service does not enforce a strict format for deviceId, but it must: + - Be a string (case-sensitive). + - Be 1–100 characters long. + - Contain only: + - Alphanumeric characters (A-Z, a-z, 0-9) + - Underscores (_) + - Hyphens (-) + - Be the same ID used when sending and retrieving positions. + + """.trimIndent(), + ) + + waitForInputToContinue(scanner) + updateDevicePosition(trackerName, deviceId) + println("$deviceId was successfully updated in the location tracking system.") + waitForInputToContinue(scanner) + println(DASHES) + + println(DASHES) + println("8. Retrieve the most recent position update for a specified device.") + waitForInputToContinue(scanner) + val response = getDevicePosition(trackerName, deviceId) + println("Successfully fetched device position: ${response.position}") + waitForInputToContinue(scanner) + println(DASHES) + + println(DASHES) + println("9. Create a route calculator.") + waitForInputToContinue(scanner) + val routeResponse = createRouteCalculator(calculatorName) + println("Route calculator created successfully: ${routeResponse.calculatorArn}") + waitForInputToContinue(scanner) + println(DASHES) + + println(DASHES) + println("10. Determine the distance in kilometers between Seattle and Vancouver using the route calculator.") + waitForInputToContinue(scanner) + val responseDis = calcDistance(calculatorName) + println("Successfully calculated route. The distance in kilometers is ${responseDis.summary?.distance}") + waitForInputToContinue(scanner) + println(DASHES) + + println(DASHES) + println("11. Use the GeoPlacesClient to perform additional operations.") + println( + """ + This scenario will show use of the GeoPlacesClient that enables + location search and geocoding capabilities for your applications. + + We are going to use this client to perform these AWS Location tasks: + - Reverse Geocoding (reverseGeocode): Converts geographic coordinates into addresses. + - Place Search (searchText): Finds places based on search queries. + - Nearby Search (searchNearby): Finds places near a specific location. + + """.trimIndent(), + ) + + waitForInputToContinue(scanner) + println("First we will perform a Reverse Geocoding operation") + waitForInputToContinue(scanner) + reverseGeocode() + + println("Now we are going to perform a text search using coffee shop.") + waitForInputToContinue(scanner) + searchText("coffee shop") + waitForInputToContinue(scanner) + + println("Now we are going to perform a nearby Search.") + waitForInputToContinue(scanner) + searchNearby() + waitForInputToContinue(scanner) + println(DASHES) + + println(DASHES) + println("12. Delete the AWS Location Services resources.") + println("Would you like to delete the AWS Location Services resources? (y/n)") + val delAns = scanner.nextLine().trim { it <= ' ' } + if (delAns.equals("y", ignoreCase = true)) { + deleteMap(mapName) + deleteKey(keyName) + deleteGeofenceCollection(collectionName) + deleteTracker(trackerName) + deleteRouteCalculator(calculatorName) + } else { + println("The AWS resources will not be deleted.") + } + waitForInputToContinue(scanner) + println(DASHES) + + println(DASHES) + println(" This concludes the AWS Location Service scenario.") + println(DASHES) +} + +// snippet-start:[location.kotlin.delete.calculator.main] +/** + * Deletes a route calculator from the system. + * @param calcName the name of the route calculator to delete + */ +suspend fun deleteRouteCalculator(calcName: String) { + val calculatorRequest = DeleteRouteCalculatorRequest { + this.calculatorName = calcName + } + + LocationClient { region = "us-east-1" }.use { client -> + client.deleteRouteCalculator(calculatorRequest) + println("The route calculator $calcName was deleted.") + } +} +// snippet-end:[location.kotlin.delete.calculator.main] + +// snippet-start:[location.kotlin.delete.tracker.main] + +/** + * Deletes a tracker with the specified name. + * @param trackerName the name of the tracker to be deleted + */ +suspend fun deleteTracker(trackerName: String) { + val trackerRequest = DeleteTrackerRequest { + this.trackerName = trackerName + } + + LocationClient { region = "us-east-1" }.use { client -> + client.deleteTracker(trackerRequest) + println("The tracker $trackerName was deleted.") + } +} +// snippet-end:[location.kotlin.delete.tracker.main] + +// snippet-start:[location.kotlin.delete.collection.main] + +/** + * Deletes a geofence collection. + * + * @param collectionName the name of the geofence collection to be deleted + * @return a {@link CompletableFuture} that completes when the geofence collection has been deleted + */ +suspend fun deleteGeofenceCollection(collectionName: String) { + val collectionRequest = DeleteGeofenceCollectionRequest { + this.collectionName = collectionName + } + + LocationClient { region = "us-east-1" }.use { client -> + client.deleteGeofenceCollection(collectionRequest) + println("The geofence collection $collectionName was deleted.") + } +} +// snippet-end:[location.kotlin.delete.collection.main] + +// snippet-start:[location.kotlin.delete.key.main] +/** + * Deletes the specified key from the key-value store. + * + * @param keyName the name of the key to be deleted + */ +suspend fun deleteKey(keyName: String) { + val keyRequest = DeleteKeyRequest { + this.keyName = keyName + } + + LocationClient { region = "us-east-1" }.use { client -> + client.deleteKey(keyRequest) + println("The key $keyName was deleted.") + } +} +// snippet-end:[location.kotlin.delete.key.main] + +// snippet-start:[location.kotlin.delete.map.main] +/** + * Deletes the specified key from the key-value store. + * + * @param keyName the name of the key to be deleted + */ +suspend fun deleteMap(mapName: String) { + val mapRequest = DeleteMapRequest { + this.mapName = mapName + } + + LocationClient { region = "us-east-1" }.use { client -> + client.deleteMap(mapRequest) + println("The map $mapName was deleted.") + } +} +// snippet-end:[location.kotlin.delete.map.main] + +// snippet-start:[geoplaces.kotlin.search.near.main] + +/** + * Performs a nearby places search based on the provided geographic coordinates (latitude and longitude). + * The method sends an asynchronous request to search for places within a 1-kilometer radius of the specified location. + * The results are processed and printed once the search completes successfully. + */ +suspend fun searchNearby() { + val latitude = 37.7749 + val longitude = -122.4194 + val queryPosition = listOf(longitude, latitude) + + // Set up the request for searching nearby places. + val request = SearchNearbyRequest { + this.queryPosition = queryPosition + this.queryRadius = 1000L + } + + GeoPlacesClient { region = "us-east-1" }.use { client -> + val response = client.searchNearby(request) + + // Process the response and print the results. + response.resultItems?.forEach { result -> + println("Title: ${result.title}") + println("Address: ${result.address?.label}") + println("Distance: ${result.distance} meters") + println("-------------------------") + } + } +} +// snippet-end:[geoplaces.kotlin.search.near.main] + +// snippet-start:[geoplaces.kotlin.search.text.main] + +/** + * Searches for a place using the provided search query and prints the detailed information of the first result. + * + * @param searchQuery the search query to be used for the place search (ex, coffee shop) + */ +suspend fun searchText(searchQuery: String) { + val latitude = 37.7749 + val longitude = -122.4194 + val queryPosition = listOf(longitude, latitude) + + val request = SearchTextRequest { + this.queryText = searchQuery + this.biasPosition = queryPosition + } + + GeoPlacesClient { region = "us-east-1" }.use { client -> + val response = client.searchText(request) + + response.resultItems?.firstOrNull()?.let { result -> + val placeId = result.placeId // Get Place ID + println("Found Place with id: $placeId") + + // Fetch detailed info using getPlace. + val getPlaceRequest = GetPlaceRequest { + this.placeId = placeId + } + + val placeResponse = client.getPlace(getPlaceRequest) + + // Print detailed place information. + println("Detailed Place Information:") + println("Title: ${placeResponse.title}") + println("Address: ${placeResponse.address?.label}") + + // Print each food type (if any). + placeResponse.foodTypes?.takeIf { it.isNotEmpty() }?.let { + println("Food Types:") + it.forEach { foodType -> + println(" - $foodType") + } + } ?: run { + println("No food types available.") + } + + println("-------------------------") + } + } +} +// snippet-end:[geoplaces.kotlin.search.text.main] + +// snippet-start:[geoplaces.kotlin.geocode.main] +/** + * Performs reverse geocoding using the AWS Geo Places API. + * Reverse geocoding is the process of converting geographic coordinates (latitude and longitude) to a human-readable address. + * This method uses the latitude and longitude of San Francisco as the input, and prints the resulting address. + */ +suspend fun reverseGeocode() { + val latitude = 37.7749 + val longitude = -122.4194 + println("Use latitude 37.7749 and longitude -122.4194") + + // AWS expects [longitude, latitude]. + val queryPosition = listOf(longitude, latitude) + val request = ReverseGeocodeRequest { + this.queryPosition = queryPosition + } + + GeoPlacesClient { region = "us-east-1" }.use { client -> + val response = client.reverseGeocode(request) + response.resultItems?.forEach { result -> + println("The address is: ${result.address?.label}") + } + } +} +// snippet-end:[geoplaces.kotlin.geocode.main] + +// snippet-start:[location.kotlin.calc.distance.main] + +/** + * Calculates the distance between two locations. + * + * @param routeCalcName the name of the route calculator to use + * @return a {@link CompletableFuture} that will complete with a {@link CalculateRouteResponse} containing the distance and estimated duration of the route + */ +suspend fun calcDistance(routeCalcName: String): CalculateRouteResponse { + // Define coordinates for Seattle, WA and Vancouver, BC. + val departurePosition = listOf(-122.3321, 47.6062) + val arrivePosition = listOf(-123.1216, 49.2827) + + val request = CalculateRouteRequest { + this.calculatorName = routeCalcName + this.departurePosition = departurePosition + this.destinationPosition = arrivePosition + this.travelMode = TravelMode.Car // Options: Car, Truck, Walking, Bicycle + this.distanceUnit = DistanceUnit.Kilometers // Options: Meters, Kilometers, Miles + } + + LocationClient { region = "us-east-1" }.use { client -> + return client.calculateRoute(request) + } +} +// snippet-end:[location.kotlin.calc.distance.main] + +// snippet-start:[location.kotlin.create.calculator.main] +/** + * Creates a new route calculator with the specified name and data source. + * + * @param routeCalcName the name of the route calculator to be created + */ +suspend fun createRouteCalculator(routeCalcName: String): CreateRouteCalculatorResponse { + val dataSource = "Esri" + + val request = CreateRouteCalculatorRequest { + this.calculatorName = routeCalcName + this.dataSource = dataSource + } + + LocationClient { region = "us-east-1" }.use { client -> + return client.createRouteCalculator(request) + } +} +// snippet-end:[location.kotlin.create.calculator.main] + +// snippet-start:[location.kotlin.get.device.position.main] +/** + * Retrieves the position of a device using the provided LocationClient. + * + * @param trackerName The name of the tracker associated with the device. + * @param deviceId The ID of the device to retrieve the position for. + */ +suspend fun getDevicePosition(trackerName: String, deviceId: String): GetDevicePositionResponse { + val request = GetDevicePositionRequest { + this.trackerName = trackerName + this.deviceId = deviceId + } + + LocationClient { region = "us-east-1" }.use { client -> + return client.getDevicePosition(request) + } +} +// snippet-end:[location.kotlin.get.device.position.main] + +// snippet-start:[location.kotlin.update.device.position.main] +/** + * Updates the position of a device in the location tracking system. + * + * @param trackerName the name of the tracker associated with the device + * @param deviceId the unique identifier of the device + */ +suspend fun updateDevicePosition(trackerName: String, deviceId: String) { + val latitude = 37.7749 + val longitude = -122.4194 + + val positionUpdate = DevicePositionUpdate { + this.deviceId = deviceId + sampleTime = aws.smithy.kotlin.runtime.time.Instant.now() // Timestamp of position update. + position = listOf(longitude, latitude) // AWS requires [longitude, latitude] + } + + val request = BatchUpdateDevicePositionRequest { + this.trackerName = trackerName + updates = listOf(positionUpdate) + } + + LocationClient { region = "us-east-1" }.use { client -> + client.batchUpdateDevicePosition(request) + } +} +// snippet-end:[location.kotlin.update.device.position.main] + +// snippet-start:[location.kotlin.create.tracker.main] +/** + * Creates a new tracker resource in your AWS account, which you can use to track the location of devices. + * + * @param trackerName the name of the tracker to be created + * @return a {@link CompletableFuture} that, when completed, will contain the Amazon Resource Name (ARN) of the created tracker + */ +suspend fun createTracker(trackerName: String): String { + val trackerRequest = CreateTrackerRequest { + description = "Created using the Kotlin SDK" + this.trackerName = trackerName + positionFiltering = PositionFiltering.TimeBased // Options: TimeBased, DistanceBased, AccuracyBased + } + + LocationClient { region = "us-east-1" }.use { client -> + val response = client.createTracker(trackerRequest) + return response.trackerArn + } +} +// snippet-end:[location.kotlin.create.tracker.main] + +// snippet-start:[location.kotlin.put.geo.main] +/** + * Adds a new geofence to the specified collection. + * + * @param collectionName the name of the geofence collection to add the geofence to + * @param geoId the unique identifier for the geofence + */ +suspend fun putGeofence(collectionName: String, geoId: String) { + val geofenceGeometry = GeofenceGeometry { + polygon = listOf( + listOf( + listOf(-122.3381, 47.6101), + listOf(-122.3281, 47.6101), + listOf(-122.3281, 47.6201), + listOf(-122.3381, 47.6201), + listOf(-122.3381, 47.6101), + ), + ) + } + + val geofenceRequest = PutGeofenceRequest { + this.collectionName = collectionName + this.geofenceId = geoId + this.geometry = geofenceGeometry + } + + LocationClient { region = "us-east-1" }.use { client -> + client.putGeofence(geofenceRequest) + } +} +// snippet-end:[location.kotlin.put.geo.main] + +// snippet-start:[location.kotlin.create.collection.main] +/** + * Creates a new geofence collection. + * + * @param collectionName the name of the geofence collection to be created + */ +suspend fun createGeofenceCollection(collectionName: String): String { + val collectionRequest = CreateGeofenceCollectionRequest { + this.collectionName = collectionName + description = "Created by using the AWS SDK for Kotlin" + } + + LocationClient { region = "us-east-1" }.use { client -> + val response = client.createGeofenceCollection(collectionRequest) + return response.collectionArn + } +} +// snippet-end:[location.kotlin.create.collection.main] + +// snippet-start:[location.kotlin.create.key.main] +/** + * Creates a new API key with the specified name and restrictions. + * + * @param keyName the name of the API key to be created + * @param mapArn the Amazon Resource Name (ARN) of the map resource to which the API key will be associated + * @return the Amazon Resource Name (ARN) of the created API key + */ +suspend fun createKey(keyName: String, mapArn: String): String { + val keyRestrictions = ApiKeyRestrictions { + allowActions = listOf("geo:GetMap*") + allowResources = listOf(mapArn) + } + + val request = CreateKeyRequest { + this.keyName = keyName + this.restrictions = keyRestrictions + noExpiry = true + } + + LocationClient { region = "us-east-1" }.use { client -> + val response = client.createKey(request) + return response.keyArn + } +} +// snippet-end:[location.kotlin.create.key.main] + +// snippet-start:[location.kotlin.create.map.main] +/** + * Creates a new map with the specified name and configuration. + * + * @param mapName the name of the map to be created + * @return he Amazon Resource Name (ARN) of the created map + */ +suspend fun createMap(mapName: String): String { + val configuration = MapConfiguration { + style = "VectorEsriNavigation" + } + + val mapRequest = CreateMapRequest { + this.mapName = mapName + this.configuration = configuration + description = "A map created using the Kotlin SDK" + } + + LocationClient { region = "us-east-1" }.use { client -> + val response = client.createMap(mapRequest) + return response.mapArn + } +} +// snippet-end:[location.kotlin.create.map.main] + +fun waitForInputToContinue(scanner: Scanner) { + while (true) { + println("") + println("Enter 'c' followed by to continue:") + val input = scanner.nextLine() + if (input.trim { it <= ' ' }.equals("c", ignoreCase = true)) { + println("Continuing with the program...") + println("") + break + } else { + println("Invalid input. Please try again.") + } + } +} +// snippet-end:[location.kotlin.scenario.main] diff --git a/kotlin/services/location/src/test/java/LocationTest.kt b/kotlin/services/location/src/test/java/LocationTest.kt new file mode 100644 index 00000000000..473d5f82ebd --- /dev/null +++ b/kotlin/services/location/src/test/java/LocationTest.kt @@ -0,0 +1,152 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import com.example.location.listGeofences +import com.example.location.scenario.calcDistance +import com.example.location.scenario.createGeofenceCollection +import com.example.location.scenario.createKey +import com.example.location.scenario.createMap +import com.example.location.scenario.createRouteCalculator +import com.example.location.scenario.createTracker +import com.example.location.scenario.deleteGeofenceCollection +import com.example.location.scenario.deleteKey +import com.example.location.scenario.deleteMap +import com.example.location.scenario.deleteRouteCalculator +import com.example.location.scenario.deleteTracker +import com.example.location.scenario.getDevicePosition +import com.example.location.scenario.putGeofence +import com.example.location.scenario.reverseGeocode +import com.example.location.scenario.searchNearby +import com.example.location.scenario.searchText +import com.example.location.scenario.updateDevicePosition +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.MethodOrderer +import org.junit.jupiter.api.Order +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.TestMethodOrder +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@TestMethodOrder(MethodOrderer.OrderAnnotation::class) +class LocationTest { + private val logger: Logger = LoggerFactory.getLogger(LocationTest::class.java) + private val mapName = "TestMap" + private val keyName = "TestKey" + private val collectionName = "TestCollection" + private val existingCollectione = "Collection100" + private val geoId = "TestGeo" + private val trackerName = "TestTracker" + private var mapArn = "" + private var keyArn = "" + var calculatorName = "TestCalc" + var deviceId = "iPhone-112359" // Use the iPhone's identifier from Swift + + @Test + @Order(1) + fun testScenario() = runBlocking { + println("===== Starting Full Location Service Scenario Test =====") + try { + // Step 1: Create Map + mapArn = runCatching { createMap(mapName) } + .onSuccess { Assertions.assertNotNull(it, "Expected map ARN to be non-null") } + .onFailure { it.printStackTrace() } + .getOrThrow() + + logger.info("Created Map: $mapArn") + + // Step 2: Create Key + keyArn = runCatching { createKey(keyName, mapArn) } + .onSuccess { Assertions.assertNotNull(it, "Expected key ARN to be non-null") } + .onFailure { it.printStackTrace() } + .getOrThrow() + + logger.info("Created Key: $keyArn") + + // Step 3: Geofencing + runCatching { createGeofenceCollection(collectionName) } + .onSuccess { println("Created Geofence Collection: $collectionName") } + .onFailure { it.printStackTrace() } + .getOrThrow() + + runCatching { putGeofence(collectionName, geoId) } + .onSuccess { println("Added Geofence: $geoId") } + .onFailure { it.printStackTrace() } + .getOrThrow() + + // Step 4: Tracking + runCatching { createTracker(trackerName) } + .onSuccess { println(" Created Tracker: $trackerName") } + .onFailure { it.printStackTrace() } + .getOrThrow() + + runCatching { updateDevicePosition(trackerName, deviceId) } + .onSuccess { println("Updated Device Position: $deviceId") } + .onFailure { it.printStackTrace() } + .getOrThrow() + + runCatching { getDevicePosition(trackerName, deviceId) } + .onSuccess { println("Retrieved Device Position for: $deviceId") } + .onFailure { it.printStackTrace() } + .getOrThrow() + + // Step 5: Route Calculation + runCatching { createRouteCalculator(calculatorName) } + .onSuccess { println("Created Route Calculator: $calculatorName") } + .onFailure { it.printStackTrace() } + .getOrThrow() + + runCatching { calcDistance(calculatorName) } + .onSuccess { println("Calculated Distance") } + .onFailure { it.printStackTrace() } + .getOrThrow() + + // Step 6: Search Operations + runCatching { reverseGeocode() } + .onSuccess { println("Reverse Geocode Successful") } + .onFailure { it.printStackTrace() } + .getOrThrow() + + runCatching { searchText("coffee shop") } + .onSuccess { println("Search for 'coffee shop' Successful") } + .onFailure { it.printStackTrace() } + .getOrThrow() + + runCatching { searchNearby() } + .onSuccess { println(" Nearby Search Successful") } + .onFailure { it.printStackTrace() } + .getOrThrow() + } finally { + // Cleanup + println("===== Starting Cleanup =====") + val cleanupResults = listOf( + runCatching { deleteMap(mapName) }.onFailure { it.printStackTrace() }, + runCatching { deleteKey(keyName) }.onFailure { it.printStackTrace() }, + runCatching { deleteGeofenceCollection(collectionName) }.onFailure { it.printStackTrace() }, + runCatching { deleteTracker(trackerName) }.onFailure { it.printStackTrace() }, + runCatching { deleteRouteCalculator(calculatorName) }.onFailure { it.printStackTrace() }, + ) + + // Ensure cleanup didn't fail completely + val cleanupSuccess = cleanupResults.all { it.isSuccess } + Assertions.assertTrue(cleanupSuccess, "Some resources failed to delete") + + logger.info("===== Cleanup Completed Successfully =====") + } + + logger.info("🎉 Test 1 Passed Successfully!") + } + + @Test + @Order(2) + fun testHello() = runBlocking { + runCatching { listGeofences(existingCollectione) } + .onSuccess { println("Hello passed") } + .onFailure { it.printStackTrace() } + .getOrThrow() + + logger.info("🎉 Test 2 Passed Successfully!") + } +}