Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.awssdk.v1_11;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class BedrockJsonParser {

// Prevent instantiation
private BedrockJsonParser() {
throw new UnsupportedOperationException("Utility class");
}

public static LlmJson parse(String jsonString) {
JsonParser parser = new JsonParser(jsonString);
Map<String, Object> jsonBody = parser.parse();
return new LlmJson(jsonBody);
}

static class JsonParser {
private final String json;
private int position;

public JsonParser(String json) {
this.json = json.trim();
this.position = 0;
}

private void skipWhitespace() {
while (position < json.length() && Character.isWhitespace(json.charAt(position))) {
position++;
}
}

private char currentChar() {
return json.charAt(position);
}

private static boolean isHexDigit(char c) {
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
}

private void expect(char c) {
skipWhitespace();
if (currentChar() != c) {
throw new IllegalArgumentException(
"Expected '" + c + "' but found '" + currentChar() + "'");
}
position++;
}

private String readString() {
skipWhitespace();
expect('"'); // Ensure the string starts with a quote
StringBuilder result = new StringBuilder();
while (currentChar() != '"') {
// Handle escape sequences
if (currentChar() == '\\') {
position++; // Move past the backslash
if (position >= json.length()) {
throw new IllegalArgumentException("Unexpected end of input in string escape sequence");
}
char escapeChar = currentChar();
switch (escapeChar) {
case '"':
case '\\':
case '/':
result.append(escapeChar);
break;
case 'b':
result.append('\b');
break;
case 'f':
result.append('\f');
break;
case 'n':
result.append('\n');
break;
case 'r':
result.append('\r');
break;
case 't':
result.append('\t');
break;
case 'u': // Unicode escape sequence
if (position + 4 >= json.length()) {
throw new IllegalArgumentException("Invalid unicode escape sequence in string");
}
char[] hexChars = new char[4];
for (int i = 0; i < 4; i++) {
position++; // Move to the next character
char hexChar = json.charAt(position);
if (!isHexDigit(hexChar)) {
throw new IllegalArgumentException(
"Invalid hexadecimal digit in unicode escape sequence");
}
hexChars[i] = hexChar;
}
int unicodeValue = Integer.parseInt(new String(hexChars), 16);
result.append((char) unicodeValue);
break;
default:
throw new IllegalArgumentException("Invalid escape character: \\" + escapeChar);
}
position++;
} else {
result.append(currentChar());
position++;
}
}
position++; // Skip closing quote
return result.toString();
}

private Object readValue() {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found an open-source mini-json parser without requiring Dep, which has very similar Impl like yours which is good. :) Could double check test a few things? Will you impl be able to handle the attribute value with special chars such as \n?

https://github.com/ralfstx/minimal-json/blob/master/com.eclipsesource.json/src/main/java/com/eclipsesource/json/JsonParser.java#L159

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add the unit tests for the json utils? We test against different bedrock response format pattern and edge cases

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added unit tests to cover special chars and unicode that may appear in the input/output prompts. Also added some test cases to cover empty objects and arrays.

skipWhitespace();
char c = currentChar();

if (c == '"') {
return readString();
} else if (Character.isDigit(c)) {
return readScopedNumber();
} else if (c == '{') {
return readObject(); // JSON Objects
} else if (c == '[') {
return readArray(); // JSON Arrays
} else if (json.startsWith("true", position)) {
position += 4;
return true;
} else if (json.startsWith("false", position)) {
position += 5;
return false;
} else if (json.startsWith("null", position)) {
position += 4;
return null; // JSON null
} else {
throw new IllegalArgumentException("Unexpected character: " + c);
}
}

private Number readScopedNumber() {
int start = position;

// Consume digits and the optional decimal point
while (position < json.length()
&& (Character.isDigit(json.charAt(position)) || json.charAt(position) == '.')) {
position++;
}

String number = json.substring(start, position);

if (number.contains(".")) {
double value = Double.parseDouble(number);
if (value < 0.0 || value > 1.0) {
throw new IllegalArgumentException(
"Value out of bounds for Bedrock Floating Point Attribute: " + number);
}
return value;
} else {
return Integer.parseInt(number);
}
}

private Map<String, Object> readObject() {
Map<String, Object> map = new HashMap<>();
expect('{');
skipWhitespace();
while (currentChar() != '}') {
String key = readString();
expect(':');
Object value = readValue();
map.put(key, value);
skipWhitespace();
if (currentChar() == ',') {
position++;
}
}
position++; // Skip closing brace
return map;
}

private List<Object> readArray() {
List<Object> list = new ArrayList<>();
expect('[');
skipWhitespace();
while (currentChar() != ']') {
list.add(readValue());
skipWhitespace();
if (currentChar() == ',') {
position++;
}
}
position++;
return list;
}

public Map<String, Object> parse() {
return readObject();
}
}

// Resolves paths in a JSON structure
static class JsonPathResolver {

// Private constructor to prevent instantiation
private JsonPathResolver() {
throw new UnsupportedOperationException("Utility class");
}

public static Object resolvePath(LlmJson llmJson, String... paths) {
for (String path : paths) {
Object value = resolvePath(llmJson.getJsonBody(), path);
if (value != null) {
return value;
}
}
return null;
}

private static Object resolvePath(Map<String, Object> json, String path) {
String[] keys = path.split("/");
Object current = json;

for (String key : keys) {
if (key.isEmpty()) {
continue;
}

if (current instanceof Map) {
current = ((Map<?, ?>) current).get(key);
} else if (current instanceof List) {
try {
int index = Integer.parseInt(key);
current = ((List<?>) current).get(index);
} catch (NumberFormatException | IndexOutOfBoundsException e) {
return null;
}
} else {
return null;
}

if (current == null) {
return null;
}
}
return current;
}
}

public static class LlmJson {
private final Map<String, Object> jsonBody;

public LlmJson(Map<String, Object> jsonBody) {
this.jsonBody = jsonBody;
}

public Map<String, Object> getJsonBody() {
return jsonBody;
}
}
}
Loading