diff --git a/.github/workflows/java-server-sdk-ai.yml b/.github/workflows/java-server-sdk-ai.yml new file mode 100644 index 0000000..1462e3d --- /dev/null +++ b/.github/workflows/java-server-sdk-ai.yml @@ -0,0 +1,23 @@ +name: java-server-sdk-ai + +on: + push: + branches: [main, 'feat/**'] + paths-ignore: + - '**.md' #Do not need to run CI for markdown changes. + pull_request: + branches: [main, 'feat/**', 'lc/**'] + paths-ignore: + - '**.md' + +jobs: + build-test-java-server-sdk-ai: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Shared CI Steps + uses: ./.github/actions/ci + with: + workspace_path: 'lib/sdk/server-ai' + java_version: 8 diff --git a/lib/sdk/server-ai/src/main/java/com/launchdarkly/sdk/server/ai/LDAiClient.java b/lib/sdk/server-ai/src/main/java/com/launchdarkly/sdk/server/ai/LDAiClient.java index 6b557f9..e3bda4a 100644 --- a/lib/sdk/server-ai/src/main/java/com/launchdarkly/sdk/server/ai/LDAiClient.java +++ b/lib/sdk/server-ai/src/main/java/com/launchdarkly/sdk/server/ai/LDAiClient.java @@ -1,28 +1,179 @@ package com.launchdarkly.sdk.server.ai; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Optional; + import com.launchdarkly.logging.LDLogger; import com.launchdarkly.sdk.server.interfaces.LDClientInterface; +import com.launchdarkly.sdk.LDValue; +import com.launchdarkly.sdk.LDValueType; +import com.launchdarkly.sdk.server.ai.datamodel.AiConfig; +import com.launchdarkly.sdk.server.ai.datamodel.Message; +import com.launchdarkly.sdk.server.ai.datamodel.Meta; +import com.launchdarkly.sdk.server.ai.datamodel.Model; +import com.launchdarkly.sdk.server.ai.datamodel.Provider; import com.launchdarkly.sdk.server.ai.interfaces.LDAiClientInterface; /** - * The LaunchDarkly AI client. The client is capable of retrieving AI Configs from LaunchDarkly, - * and generating events specific to usage of the AI Config when interacting with model providers. + * The LaunchDarkly AI client. The client is capable of retrieving AI Configs + * from LaunchDarkly, + * and generating events specific to usage of the AI Config when interacting + * with model providers. */ -public class LDAiClient implements LDAiClientInterface { +public final class LDAiClient implements LDAiClientInterface { private LDClientInterface client; private LDLogger logger; /** * Creates a {@link LDAiClient} * - * @param client LaunchDarkly Java Server SDK + * @param client LaunchDarkly Java Server SDK */ public LDAiClient(LDClientInterface client) { - if(client == null) { - //Error + if (client == null) { + // Error } else { this.client = client; this.logger = client.getLogger(); } } + + /** + * Method to convert the JSON variable into the AiConfig object + * + * If the parsing failed, the code will log an error and + * return a well formed but with nullable value nulled and disabled AIConfig + * + * Doing all the error checks, so if somehow LD backend return incorrect value + * types, there is logging + * This also opens up the possibility of allowing customer to build this using a + * JSON string in the future + * + * @param value + * @param key + */ + protected AiConfig parseAiConfig(LDValue value, String key) { + boolean enabled = false; + + // Verify the whole value is a JSON object + if (!checkValueWithFailureLogging(value, LDValueType.OBJECT, logger, + "Input to parseAiConfig must be a JSON object")) { + return AiConfig.builder().enabled(enabled).build(); + } + + // Convert the _meta JSON object into Meta + LDValue valueMeta = value.get("_ldMeta"); + if (!checkValueWithFailureLogging(valueMeta, LDValueType.OBJECT, logger, "_ldMeta must be a JSON object")) { + // Q: If we can't read _meta, enabled by spec would be defaulted to false. Does + // it even matter the rest of the values? + return AiConfig.builder().enabled(enabled).build(); + } + + // The booleanValue will get false if that value is something that we are not expecting + enabled = valueMeta.get("enabled").booleanValue(); + + Meta meta = null; + + if (checkValueWithFailureLogging(valueMeta.get("variationKey"), LDValueType.STRING, logger, + "variationKey should be a string")) { + String variationKey = valueMeta.get("variationKey").stringValue(); + + meta = Meta.builder() + .variationKey(variationKey) + .version(Optional.of(valueMeta.get("version").intValue())) + .build(); + } + + // Convert the optional model from an JSON object of with parameters and custom + // into Model + Model model = null; + + LDValue valueModel = value.get("model"); + if (checkValueWithFailureLogging(valueModel, LDValueType.OBJECT, logger, + "model if exists must be a JSON object")) { + if (checkValueWithFailureLogging(valueModel.get("name"), LDValueType.STRING, logger, + "model name must be a string and is required")) { + String modelName = valueModel.get("name").stringValue(); + + // Prepare parameters and custom maps for Model + HashMap parameters = null; + HashMap custom = null; + + LDValue valueParameters = valueModel.get("parameters"); + if (checkValueWithFailureLogging(valueParameters, LDValueType.OBJECT, logger, + "non-null parameters must be a JSON object")) { + parameters = new HashMap<>(); + for (String k : valueParameters.keys()) { + parameters.put(k, valueParameters.get(k)); + } + } + + LDValue valueCustom = valueModel.get("custom"); + if (checkValueWithFailureLogging(valueCustom, LDValueType.OBJECT, logger, + "non-null custom must be a JSON object")) { + + custom = new HashMap<>(); + for (String k : valueCustom.keys()) { + custom.put(k, valueCustom.get(k)); + } + } + + model = Model.builder() + .name(modelName) + .parameters(parameters) + .custom(custom) + .build(); + } + } + + // Convert the optional messages from an JSON array of JSON objects into Message + // Q: Does it even make sense to have 0 messages? + List messages = null; + + LDValue valueMessages = value.get("messages"); + if (checkValueWithFailureLogging(valueMessages, LDValueType.ARRAY, logger, + "messages if exists must be a JSON array")) { + messages = new ArrayList(); + valueMessages.valuesAs(new Message.MessageConverter()); + for (Message message : valueMessages.valuesAs(new Message.MessageConverter())) { + messages.add(message); + } + } + + // Convert the optional provider from an JSON object of with name into Provider + LDValue valueProvider = value.get("provider"); + Provider provider = null; + + if (checkValueWithFailureLogging(valueProvider, LDValueType.OBJECT, logger, + "non-null provider must be a JSON object")) { + if (checkValueWithFailureLogging(valueProvider.get("name"), LDValueType.STRING, logger, + "provider name must be a String")) { + String providerName = valueProvider.get("name").stringValue(); + + provider = Provider.builder().name(providerName).build(); + } + } + + return AiConfig.builder() + .enabled(enabled) + .meta(meta) + .model(model) + .messages(messages) + .provider(provider) + .build(); + } + + protected boolean checkValueWithFailureLogging(LDValue ldValue, LDValueType expectedType, LDLogger logger, + String message) { + if (ldValue.getType() != expectedType) { + // TODO: In the next PR, make this required with some sort of default logger + if (logger != null) { + logger.error(message); + } + return false; + } + return true; + } } diff --git a/lib/sdk/server-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/AiConfig.java b/lib/sdk/server-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/AiConfig.java new file mode 100644 index 0000000..8fdcb78 --- /dev/null +++ b/lib/sdk/server-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/AiConfig.java @@ -0,0 +1,86 @@ +package com.launchdarkly.sdk.server.ai.datamodel; + +import java.util.List; + +public final class AiConfig { + private final boolean enabled; + + private final Meta meta; + + private final Model model; + + private final List messages; + + private final Provider provider; + + AiConfig(boolean enabled, Meta meta, Model model, List messages, Provider provider) { + this.enabled = enabled; + this.meta = meta; + this.model = model; + this.messages = messages; + this.provider = provider; + } + + public boolean isEnabled() { + return enabled; + } + + public List getMessages() { + return messages; + } + + public Meta getMeta() { + return meta; + } + + public Model getModel() { + return model; + } + + public Provider getProvider() { + return provider; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private boolean enabled; + private Meta meta; + private Model model; + private List messages; + private Provider provider; + + private Builder() {} + + public Builder enabled(boolean enabled) { + this.enabled = enabled; + return this; + } + + public Builder meta(Meta meta) { + this.meta = meta; + return this; + } + + public Builder model(Model model) { + this.model = model; + return this; + } + + public Builder messages(List messages) { + this.messages = messages; + return this; + } + + public Builder provider(Provider provider) { + this.provider = provider; + return this; + } + + public AiConfig build() { + return new AiConfig(enabled, meta, model, messages, provider); + } + } +} diff --git a/lib/sdk/server-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/Message.java b/lib/sdk/server-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/Message.java new file mode 100644 index 0000000..8ac89ec --- /dev/null +++ b/lib/sdk/server-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/Message.java @@ -0,0 +1,65 @@ +package com.launchdarkly.sdk.server.ai.datamodel; + +import com.launchdarkly.sdk.LDValue; + +public final class Message { + public static class MessageConverter extends LDValue.Converter { + @Override + public LDValue fromType(Message message) { + return LDValue.buildObject() + .put("content", message.getContent()) + .put("role", message.getRole().toString()) + .build(); + } + + @Override + public Message toType(LDValue ldValue) { + return Message.builder() + .content(ldValue.get("content").stringValue()) + .role(Role.getRole(ldValue.get("role").stringValue())) + .build(); + } + } + + private final String content; + + private final Role role; + + Message(String content, Role role) { + this.content = content; + this.role = role; + } + + public String getContent() { + return content; + } + + public Role getRole() { + return role; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String content; + private Role role; + + private Builder() {} + + public Builder content(String content) { + this.content = content; + return this; + } + + public Builder role(Role role) { + this.role = role; + return this; + } + + public Message build() { + return new Message(content, role); + } + } +} diff --git a/lib/sdk/server-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/Meta.java b/lib/sdk/server-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/Meta.java new file mode 100644 index 0000000..55af783 --- /dev/null +++ b/lib/sdk/server-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/Meta.java @@ -0,0 +1,67 @@ +package com.launchdarkly.sdk.server.ai.datamodel; + +import java.util.Optional; + +public final class Meta { + /** + * The variation key. + */ + private final String variationKey; + + /** + * The variation version. + */ + private final Optional version; + + // The enable paramter is taken outside of the Meta to be a top level value of + // AiConfig + + /** + * Constructor for Meta with all required fields. + * + * @param variationKey the variation key + * @param version the version + */ + Meta(String variationKey, Optional version) { + this.variationKey = variationKey; + this.version = version != null ? version : Optional.empty(); + } + + public String getVariationKey() { + return variationKey; + } + + public Optional getVersion() { + return version; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String variationKey; + private Optional version = Optional.empty(); + + private Builder() {} + + public Builder variationKey(String variationKey) { + this.variationKey = variationKey; + return this; + } + + public Builder version(Optional version) { + this.version = version; + return this; + } + + public Builder version(int version) { + this.version = Optional.of(version); + return this; + } + + public Meta build() { + return new Meta(variationKey, version); + } + } +} diff --git a/lib/sdk/server-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/Model.java b/lib/sdk/server-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/Model.java new file mode 100644 index 0000000..ee2f7f5 --- /dev/null +++ b/lib/sdk/server-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/Model.java @@ -0,0 +1,72 @@ +package com.launchdarkly.sdk.server.ai.datamodel; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import com.launchdarkly.sdk.LDValue; + +public final class Model { + private final String name; + + private final Map parameters; + + private final Map custom; + + /** + * Constructor for Model with all required fields. + * + * @param name the model name + * @param parameters the parameters map + * @param custom the custom map + */ + Model(String name, Map parameters, Map custom) { + this.name = name; + this.parameters = parameters != null ? Collections.unmodifiableMap(new HashMap<>(parameters)) + : Collections.emptyMap(); + this.custom = custom != null ? Collections.unmodifiableMap(new HashMap<>(custom)) : Collections.emptyMap(); + } + + public String getName() { + return name; + } + + public Map getParameters() { + return parameters; + } + + public Map getCustom() { + return custom; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String name; + private Map parameters; + private Map custom; + + private Builder() {} + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder parameters(Map parameters) { + this.parameters = parameters; + return this; + } + + public Builder custom(Map custom) { + this.custom = custom; + return this; + } + + public Model build() { + return new Model(name, parameters, custom); + } + } +} diff --git a/lib/sdk/server-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/Provider.java b/lib/sdk/server-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/Provider.java new file mode 100644 index 0000000..614408d --- /dev/null +++ b/lib/sdk/server-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/Provider.java @@ -0,0 +1,32 @@ +package com.launchdarkly.sdk.server.ai.datamodel; + +public final class Provider { + private final String name; + + Provider(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String name; + + private Builder() {} + + public Builder name(String name) { + this.name = name; + return this; + } + + public Provider build() { + return new Provider(name); + } + } +} diff --git a/lib/sdk/server-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/Role.java b/lib/sdk/server-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/Role.java index 524cac3..fd7f4aa 100644 --- a/lib/sdk/server-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/Role.java +++ b/lib/sdk/server-ai/src/main/java/com/launchdarkly/sdk/server/ai/datamodel/Role.java @@ -7,13 +7,37 @@ public enum Role { /** * User Role */ - USER, + USER("user"), /** * System Role */ - SYSTEM, + SYSTEM("system"), /** * Assistant Role */ - ASSISTANT + ASSISTANT("assistant"); + + private final String role; + + private Role(String role) { + this.role = role; + } + + public static Role getRole(String role) { + switch (role) { + case "user": + return USER; + case "system": + return SYSTEM; + case "assistant": + return ASSISTANT; + default: + return null; + } + } + + @Override + public String toString() { + return role; + } } diff --git a/lib/sdk/server-ai/src/test/java/com/launchdarkly/sdk/server/ai/LDAiClientTest.java b/lib/sdk/server-ai/src/test/java/com/launchdarkly/sdk/server/ai/LDAiClientTest.java new file mode 100644 index 0000000..05a1f65 --- /dev/null +++ b/lib/sdk/server-ai/src/test/java/com/launchdarkly/sdk/server/ai/LDAiClientTest.java @@ -0,0 +1,397 @@ +package com.launchdarkly.sdk.server.ai; + +import org.junit.Test; + +import com.launchdarkly.sdk.LDValue; +import com.launchdarkly.sdk.server.ai.datamodel.AiConfig; +import com.launchdarkly.sdk.server.ai.datamodel.Role; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; + +public class LDAiClientTest { + /** + * Tests that a complete valid JSON is properly converted to an AiConfig object + */ + @Test + public void testCompleteAiConfig() { + String rawJson = "{\n" + // + " \"_ldMeta\": {\n" + // + " \"variationKey\" : \"1234\",\n" + // + " \"enabled\": true,\n" + // + " \"version\": 1\n" + // + " },\n" + // + " \"messages\": [\n" + // + " {\n" + // + " \"content\": \"This is an {{ adjective }} message.\",\n" + // + " \"role\": \"user\"\n" + // + " },\n" + // + " {\n" + // + " \"content\": \"{{ greeting}}, this is another message!\",\n" + // + " \"role\": \"system\"\n" + // + " },\n" + // + " {\n" + // + " \"content\": \"This is the final {{ noun }}.\",\n" + // + " \"role\": \"assistant\"\n" + // + " }\n" + // + " ],\n" + // + " \"model\": {\n" + // + " \"name\": \"my-cool-custom-model\",\n" + // + " \"parameters\": {\n" + // + " \"foo\" : \"bar\",\n" + // + " \"baz\" : 23,\n" + // + " \"qux\" : true,\n" + // + " \"whatever\" : [],\n" + // + " \"another\" : {}\n" + // + " }\n" + // + " },\n" + // + " \"provider\": {\n" + // + " \"name\" : \"provider-name\"\n" + // + " }\n" + // + "}"; + + LDValue input = LDValue.parse(rawJson); + LDAiClient client = new LDAiClient(null); + AiConfig result = client.parseAiConfig(input, "Whatever"); + + assertEquals(Role.USER, result.getMessages().get(0).getRole()); + assertEquals("This is the final {{ noun }}.", result.getMessages().get(2).getContent()); + assertEquals(Integer.valueOf(1), result.getMeta().getVersion().orElse(0)); + assertEquals(LDValue.of(true), result.getModel().getParameters().get("qux")); + assertEquals("provider-name", result.getProvider().getName()); + assertTrue(result.isEnabled()); + } + + /** + * Tests that a valid Meta object is correctly parsed + */ + @Test + public void testValidMeta() { + String json = "{\n" + // + " \"_ldMeta\": {\n" + // + " \"variationKey\" : \"key-123\",\n" + // + " \"enabled\": true,\n" + // + " \"version\": 42\n" + // + " },\n" + // + " \"messages\": [{\n" + // + " \"content\": \"content\",\n" + // + " \"role\": \"user\"\n" + // + " }],\n" + // + " \"model\": {\n" + // + " \"name\": \"model-name\"\n" + // + " },\n" + // + " \"provider\": {\n" + // + " \"name\" : \"provider-name\"\n" + // + " }\n" + // + "}"; + + LDValue input = LDValue.parse(json); + LDAiClient client = new LDAiClient(null); + AiConfig result = client.parseAiConfig(input, "Whatever"); + + assertNotNull(result.getMeta()); + assertEquals("key-123", result.getMeta().getVariationKey()); + assertEquals(Integer.valueOf(42), result.getMeta().getVersion().orElse(0)); + assertTrue(result.isEnabled()); + } + + /** + * Tests that invalid Meta with number as variationKey is handled properly + */ + @Test + public void testInvalidMetaNumberAsVariationKey() { + String json = "{\n" + // + " \"_ldMeta\": {\n" + // + " \"variationKey\" : 123,\n" + // + " \"enabled\": true,\n" + // + " \"version\": 1\n" + // + " },\n" + // + " \"messages\": [{\n" + // + " \"content\": \"content\",\n" + // + " \"role\": \"user\"\n" + // + " }],\n" + // + " \"model\": {\n" + // + " \"name\": \"model-name\"\n" + // + " },\n" + // + " \"provider\": {\n" + // + " \"name\" : \"provider-name\"\n" + // + " }\n" + // + "}"; + + LDValue input = LDValue.parse(json); + LDAiClient client = new LDAiClient(null); + AiConfig result = client.parseAiConfig(input, "Whatever"); + + assertNull(result.getMeta()); + } + + /** + * Tests that valid Messages are correctly parsed + */ + @Test + public void testValidMessages() { + String json = "{\n" + // + " \"_ldMeta\": {\n" + // + " \"variationKey\" : \"key-123\",\n" + // + " \"enabled\": true,\n" + // + " \"version\": 1\n" + // + " },\n" + // + " \"messages\": [\n" + // + " {\n" + // + " \"content\": \"User message\",\n" + // + " \"role\": \"user\"\n" + // + " },\n" + // + " {\n" + // + " \"content\": \"System message\",\n" + // + " \"role\": \"system\"\n" + // + " },\n" + // + " {\n" + // + " \"content\": \"Assistant message\",\n" + // + " \"role\": \"assistant\"\n" + // + " }\n" + // + " ],\n" + // + " \"model\": {\n" + // + " \"name\": \"model-name\"\n" + // + " },\n" + // + " \"provider\": {\n" + // + " \"name\" : \"provider-name\"\n" + // + " }\n" + // + "}"; + + LDValue input = LDValue.parse(json); + LDAiClient client = new LDAiClient(null); + AiConfig result = client.parseAiConfig(input, "Whatever"); + + assertNotNull(result.getMessages()); + assertEquals(3, result.getMessages().size()); + + assertEquals(Role.USER, result.getMessages().get(0).getRole()); + assertEquals("User message", result.getMessages().get(0).getContent()); + + assertEquals(Role.SYSTEM, result.getMessages().get(1).getRole()); + assertEquals("System message", result.getMessages().get(1).getContent()); + + assertEquals(Role.ASSISTANT, result.getMessages().get(2).getRole()); + assertEquals("Assistant message", result.getMessages().get(2).getContent()); + } + + /** + * Tests that invalid Messages with wrong role type are handled properly + */ + @Test + public void testInvalidMessagesInvalidRole() { + String json = "{\n" + // + " \"_ldMeta\": {\n" + // + " \"variationKey\" : \"key\",\n" + // + " \"enabled\": true,\n" + // + " \"version\": 1\n" + // + " },\n" + // + " \"messages\": [\n" + // + " {\n" + // + " \"content\": \"Invalid role\",\n" + // + " \"role\": \"invalid_role_value\"\n" + // + " }\n" + // + " ],\n" + // + " \"model\": {\n" + // + " \"name\": \"model-name\"\n" + // + " },\n" + // + " \"provider\": {\n" + // + " \"name\" : \"provider-name\"\n" + // + " }\n" + // + "}"; + + LDValue input = LDValue.parse(json); + LDAiClient client = new LDAiClient(null); + AiConfig result = client.parseAiConfig(input, "Whatever"); + + assertNotNull(result.getMessages()); + assertEquals(1, result.getMessages().size()); + assertNull(result.getMessages().get(0).getRole()); + } + + /** + * Tests that a valid Model object is correctly parsed + */ + @Test + public void testValidModel() { + String json = "{\n" + // + " \"_ldMeta\": {\n" + // + " \"variationKey\" : \"key\",\n" + // + " \"enabled\": true,\n" + // + " \"version\": 1\n" + // + " },\n" + // + " \"messages\": [{\n" + // + " \"content\": \"content\",\n" + // + " \"role\": \"user\"\n" + // + " }],\n" + // + " \"model\": {\n" + // + " \"name\": \"test-model\",\n" + // + " \"parameters\": {\n" + // + " \"string_param\" : \"value\",\n" + // + " \"number_param\" : 42,\n" + // + " \"bool_param\" : true,\n" + // + " \"array_param\" : [1, 2, 3],\n" + // + " \"object_param\" : {\"key\": \"value\"}\n" + // + " },\n" + // + " \"custom\": {\n" + // + " \"custom_key\": \"custom_value\"\n" + // + " }\n" + // + " },\n" + // + " \"provider\": {\n" + // + " \"name\" : \"provider-name\"\n" + // + " }\n" + // + "}"; + + LDValue input = LDValue.parse(json); + LDAiClient client = new LDAiClient(null); + AiConfig result = client.parseAiConfig(input, "Whatever"); + + assertNotNull(result.getModel()); + assertEquals("test-model", result.getModel().getName()); + + assertNotNull(result.getModel().getParameters()); + assertEquals(5, result.getModel().getParameters().size()); + assertEquals(LDValue.of("value"), result.getModel().getParameters().get("string_param")); + assertEquals(LDValue.of(42), result.getModel().getParameters().get("number_param")); + assertEquals(LDValue.of(true), result.getModel().getParameters().get("bool_param")); + + assertNotNull(result.getModel().getCustom()); + assertEquals(1, result.getModel().getCustom().size()); + assertEquals(LDValue.of("custom_value"), result.getModel().getCustom().get("custom_key")); + } + + /** + * Tests that invalid Model with name as non-string is handled properly + */ + @Test + public void testInvalidModelNameType() { + String json = "{\n" + // + " \"_ldMeta\": {\n" + // + " \"variationKey\" : \"key\",\n" + // + " \"enabled\": true,\n" + // + " \"version\": 1\n" + // + " },\n" + // + " \"messages\": [{\n" + // + " \"content\": \"content\",\n" + // + " \"role\": \"user\"\n" + // + " }],\n" + // + " \"model\": {\n" + // + " \"name\": 123,\n" + // + " \"parameters\": {}\n" + // + " },\n" + // + " \"provider\": {\n" + // + " \"name\" : \"provider-name\"\n" + // + " }\n" + // + "}"; + + LDValue input = LDValue.parse(json); + LDAiClient client = new LDAiClient(null); + AiConfig result = client.parseAiConfig(input, "Whatever"); + + assertNull(result.getModel()); + } + + /** + * Tests that a valid Provider object is correctly parsed + */ + @Test + public void testValidProvider() { + String json = "{\n" + // + " \"_ldMeta\": {\n" + // + " \"variationKey\" : \"key\",\n" + // + " \"enabled\": true,\n" + // + " \"version\": 1\n" + // + " },\n" + // + " \"messages\": [{\n" + // + " \"content\": \"content\",\n" + // + " \"role\": \"user\"\n" + // + " }],\n" + // + " \"model\": {\n" + // + " \"name\": \"model-name\"\n" + // + " },\n" + // + " \"provider\": {\n" + // + " \"name\" : \"test-provider\"\n" + // + " }\n" + // + "}"; + + LDValue input = LDValue.parse(json); + LDAiClient client = new LDAiClient(null); + AiConfig result = client.parseAiConfig(input, "Whatever"); + + assertNotNull(result.getProvider()); + assertEquals("test-provider", result.getProvider().getName()); + } + + /** + * Tests that invalid Provider with name as non-string is handled properly + */ + @Test + public void testInvalidProviderNameType() { + String json = "{\n" + // + " \"_ldMeta\": {\n" + // + " \"variationKey\" : \"key\",\n" + // + " \"enabled\": true,\n" + // + " \"version\": 1\n" + // + " },\n" + // + " \"messages\": [{\n" + // + " \"content\": \"content\",\n" + // + " \"role\": \"user\"\n" + // + " }],\n" + // + " \"model\": {\n" + // + " \"name\": \"model-name\"\n" + // + " },\n" + // + " \"provider\": {\n" + // + " \"name\" : 123\n" + // + " }\n" + // + "}"; + + LDValue input = LDValue.parse(json); + LDAiClient client = new LDAiClient(null); + AiConfig result = client.parseAiConfig(input, "Whatever"); + + assertNull(result.getProvider()); + } + + /** + * Tests that a completely invalid JSON input (not an object) is handled properly + */ + @Test + public void testInvalidJsonNotObject() { + String json = "[]"; // Array instead of object + + LDValue input = LDValue.parse(json); + LDAiClient client = new LDAiClient(null); + AiConfig result = client.parseAiConfig(input, "Whatever"); + + assertFalse(result.isEnabled()); + assertNull(result.getMeta()); + assertNull(result.getModel()); + assertNull(result.getMessages()); + assertNull(result.getProvider()); + } + + /** + * Tests that a JSON with missing required properties is handled properly + */ + @Test + public void testMissingRequiredProperties() { + String json = "{\n" + // + " \"_ldMeta\": {\n" + // + " \"enabled\": true\n" + // + " }\n" + // + "}"; + + LDValue input = LDValue.parse(json); + LDAiClient client = new LDAiClient(null); + AiConfig result = client.parseAiConfig(input, "Whatever"); + + assertTrue(result.isEnabled()); // enabled is present and true + assertNull(result.getMeta()); // Meta should be null due to missing variationKey + assertNull(result.getModel()); + assertNull(result.getMessages()); + assertNull(result.getProvider()); + } +}