From 8fe6f589bd15c5b16ff07e24700164061e2f9d4d Mon Sep 17 00:00:00 2001 From: Adam Bien Date: Thu, 19 Dec 2024 20:31:13 +0100 Subject: [PATCH] jlama-sentiment added -> thank you for attending airhacks.live --- samples/jlama-sentiment-v2/.gitignore | 1 + samples/jlama-sentiment-v2/README.md | 2 + samples/jlama-sentiment-v2/pom.xml | 79 +++++++++++++++++++ .../src/main/java/airhacks/App.java | 16 ++++ .../java/airhacks/logging/control/Log.java | 12 +++ .../boundary/SentimentAnalysis.java | 51 ++++++++++++ .../sentimental/control/ModelLoader.java | 38 +++++++++ .../entity/HallucinationException.java | 9 +++ .../airhacks/sentimental/entity/Result.java | 33 ++++++++ .../boundary/SentimentAnalysisTest.java | 31 ++++++++ .../sentimental/entity/ResultTest.java | 28 +++++++ 11 files changed, 300 insertions(+) create mode 100644 samples/jlama-sentiment-v2/.gitignore create mode 100644 samples/jlama-sentiment-v2/README.md create mode 100644 samples/jlama-sentiment-v2/pom.xml create mode 100644 samples/jlama-sentiment-v2/src/main/java/airhacks/App.java create mode 100644 samples/jlama-sentiment-v2/src/main/java/airhacks/logging/control/Log.java create mode 100644 samples/jlama-sentiment-v2/src/main/java/airhacks/sentimental/boundary/SentimentAnalysis.java create mode 100644 samples/jlama-sentiment-v2/src/main/java/airhacks/sentimental/control/ModelLoader.java create mode 100644 samples/jlama-sentiment-v2/src/main/java/airhacks/sentimental/entity/HallucinationException.java create mode 100644 samples/jlama-sentiment-v2/src/main/java/airhacks/sentimental/entity/Result.java create mode 100644 samples/jlama-sentiment-v2/src/test/java/airhacks/sentimental/boundary/SentimentAnalysisTest.java create mode 100644 samples/jlama-sentiment-v2/src/test/java/airhacks/sentimental/entity/ResultTest.java diff --git a/samples/jlama-sentiment-v2/.gitignore b/samples/jlama-sentiment-v2/.gitignore new file mode 100644 index 00000000..a3920537 --- /dev/null +++ b/samples/jlama-sentiment-v2/.gitignore @@ -0,0 +1 @@ +models/* \ No newline at end of file diff --git a/samples/jlama-sentiment-v2/README.md b/samples/jlama-sentiment-v2/README.md new file mode 100644 index 00000000..0373e35b --- /dev/null +++ b/samples/jlama-sentiment-v2/README.md @@ -0,0 +1,2 @@ +Models are located in the `models` directory and will be automatically downloaded. +The model files are larger than 1GB. \ No newline at end of file diff --git a/samples/jlama-sentiment-v2/pom.xml b/samples/jlama-sentiment-v2/pom.xml new file mode 100644 index 00000000..b253ccc7 --- /dev/null +++ b/samples/jlama-sentiment-v2/pom.xml @@ -0,0 +1,79 @@ + + + 4.0.0 + airhacks + jlama-sentiment + 0.0.1-SNAPSHOT + jar + + + + maven-assembly-plugin + 3.7.1 + + sentimental + false + + + true + airhacks.App + + + + jar-with-dependencies + + + + + package-everything + package + + single + + + + + + maven-surefire-plugin + 3.5.1 + + + + + + com.github.tjake + jlama-core + 0.8.3 + + + com.github.tjake + jlama-native + 0.8.3 + + + org.slf4j + slf4j-nop + 2.0.16 + + + org.junit.jupiter + junit-jupiter + 5.11.2 + test + + + org.assertj + assertj-core + 3.26.3 + test + + + + UTF-8 + 21 + 21 + 21 + + \ No newline at end of file diff --git a/samples/jlama-sentiment-v2/src/main/java/airhacks/App.java b/samples/jlama-sentiment-v2/src/main/java/airhacks/App.java new file mode 100644 index 00000000..8f9430b3 --- /dev/null +++ b/samples/jlama-sentiment-v2/src/main/java/airhacks/App.java @@ -0,0 +1,16 @@ +package airhacks; + +import airhacks.sentimental.boundary.SentimentAnalysis; + +/** + * + * @author airhacks.com + */ +interface App { + + static void main(String... args) { + var message = args.length > 0 ? args[0]: "java is great"; + var result = SentimentAnalysis.analyze(message); + System.out.println(result); + } +} diff --git a/samples/jlama-sentiment-v2/src/main/java/airhacks/logging/control/Log.java b/samples/jlama-sentiment-v2/src/main/java/airhacks/logging/control/Log.java new file mode 100644 index 00000000..ce9a9a6a --- /dev/null +++ b/samples/jlama-sentiment-v2/src/main/java/airhacks/logging/control/Log.java @@ -0,0 +1,12 @@ +package airhacks.logging.control; + +public interface Log { + + public static void info(String message){ + System.out.println(message); + } + + public static void info(Object message){ + info(message.toString()); + } +} diff --git a/samples/jlama-sentiment-v2/src/main/java/airhacks/sentimental/boundary/SentimentAnalysis.java b/samples/jlama-sentiment-v2/src/main/java/airhacks/sentimental/boundary/SentimentAnalysis.java new file mode 100644 index 00000000..e02ef14b --- /dev/null +++ b/samples/jlama-sentiment-v2/src/main/java/airhacks/sentimental/boundary/SentimentAnalysis.java @@ -0,0 +1,51 @@ +package airhacks.sentimental.boundary; + +import java.io.IOException; +import java.time.Duration; +import java.time.Instant; +import java.util.UUID; + +import airhacks.logging.control.Log; +import airhacks.sentimental.control.ModelLoader; +import airhacks.sentimental.entity.Result; + +public interface SentimentAnalysis { + + String systemPrompt = """ + Your task is to analyze whether a given statement is positive or negative. + Only respond with either: "positive", "negative" or "neutral". + + Analyze the following statement: + + """; + + static String invoke(String message) throws IOException { + var model = ModelLoader.load(); + var promptContext = model.promptSupport() + .get() + .builder() + .addSystemMessage(systemPrompt) + .addUserMessage(message) + .build(); + + var response = model.generate(UUID.randomUUID(), promptContext, 0.0f, 256, (s, f) -> {}); + var duration = response.promptTimeMs; + Log.info("responded in: %d ms".formatted(duration)); + return response.responseText; + } + + public static Result analyze(String message){ + var start = Instant.now(); + String response; + try { + response = invoke(message); + + } catch (IOException e) { + throw new RuntimeException("cannot invoke model. Reason: " + e); + } + var duration = Duration.between(start, Instant.now()); + return Result.fromLLMResponse(duration,response); + } + + +} diff --git a/samples/jlama-sentiment-v2/src/main/java/airhacks/sentimental/control/ModelLoader.java b/samples/jlama-sentiment-v2/src/main/java/airhacks/sentimental/control/ModelLoader.java new file mode 100644 index 00000000..eb34ac98 --- /dev/null +++ b/samples/jlama-sentiment-v2/src/main/java/airhacks/sentimental/control/ModelLoader.java @@ -0,0 +1,38 @@ +package airhacks.sentimental.control; + +import java.io.IOException; + +import com.github.tjake.jlama.model.AbstractModel; +import com.github.tjake.jlama.model.ModelSupport; +import com.github.tjake.jlama.safetensors.DType; +import com.github.tjake.jlama.safetensors.SafeTensorSupport; + +public interface ModelLoader { + enum Model{ + TINY("tjake/TinyLlama-1.1B-Chat-v1.0-Jlama-Q4"), + MISTRAL7B("tjake/Mistral-7B-Instruct-v0.3-JQ4"), + MIXTRAL("tjake/Mixtral-8x7B-Instruct-v0.1-JQ4"), + LLAMA("tjake/Llama-3.2-1B-Instruct-Jlama-Q4"); + + final String modelName; + + private Model(String modelName) { + this.modelName = modelName; + } + + public String modelName(){ + return this.modelName; + } + + + } + String workingDirectory = "./models"; + + + static AbstractModel load() throws IOException{ + var localModelPath = SafeTensorSupport + .maybeDownloadModel(workingDirectory, Model.LLAMA.modelName()); + return ModelSupport.loadModel(localModelPath, DType.F32, DType.I8); + } + +} diff --git a/samples/jlama-sentiment-v2/src/main/java/airhacks/sentimental/entity/HallucinationException.java b/samples/jlama-sentiment-v2/src/main/java/airhacks/sentimental/entity/HallucinationException.java new file mode 100644 index 00000000..17bb6d01 --- /dev/null +++ b/samples/jlama-sentiment-v2/src/main/java/airhacks/sentimental/entity/HallucinationException.java @@ -0,0 +1,9 @@ +package airhacks.sentimental.entity; + +public class HallucinationException extends IllegalStateException{ + + public HallucinationException(String response) { + super("unexpected: " + response); + } + +} diff --git a/samples/jlama-sentiment-v2/src/main/java/airhacks/sentimental/entity/Result.java b/samples/jlama-sentiment-v2/src/main/java/airhacks/sentimental/entity/Result.java new file mode 100644 index 00000000..865bf67e --- /dev/null +++ b/samples/jlama-sentiment-v2/src/main/java/airhacks/sentimental/entity/Result.java @@ -0,0 +1,33 @@ +package airhacks.sentimental.entity; + +import java.time.Duration; + +public record Result(Duration duration,Sentiment sentiment) { + public enum Sentiment { + POSITIVE, NEGATIVE, NEUTRAL; + } + + public static Result fromLLMResponse(Duration duration,String answer) { + var normalized = answer + .trim() + .toUpperCase(); + try{ + var sentiment = Sentiment.valueOf(normalized); + return new Result(duration,sentiment); + }catch(IllegalArgumentException ex){ + throw new HallucinationException(answer); + } + } + + public boolean isPositive(){ + return Sentiment.POSITIVE.equals(this.sentiment); + } + + public boolean isNegative(){ + return Sentiment.NEGATIVE.equals(this.sentiment); + } + + public boolean isNeutral(){ + return Sentiment.NEUTRAL.equals(this.sentiment); + } +} diff --git a/samples/jlama-sentiment-v2/src/test/java/airhacks/sentimental/boundary/SentimentAnalysisTest.java b/samples/jlama-sentiment-v2/src/test/java/airhacks/sentimental/boundary/SentimentAnalysisTest.java new file mode 100644 index 00000000..fc676686 --- /dev/null +++ b/samples/jlama-sentiment-v2/src/test/java/airhacks/sentimental/boundary/SentimentAnalysisTest.java @@ -0,0 +1,31 @@ +package airhacks.sentimental.boundary; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +import airhacks.logging.control.Log; + +public class SentimentAnalysisTest { + + @Test + void analyze() throws IOException { + var result = SentimentAnalysis.analyze("java is hot and tastes really good"); + Log.info(result); + assertThat(result.isPositive()).isTrue(); + + result = SentimentAnalysis.analyze("python is dangerous, unpredictable and slow"); + Log.info(result); + assertThat(result.isNegative()).isTrue(); + + result = SentimentAnalysis.analyze("old rusty engine"); + Log.info(result); + assertThat(result.isNegative()).isTrue(); + + result = SentimentAnalysis.analyze("where should I GO?"); + Log.info(result); + assertThat(result.isNeutral()).isTrue(); + } +} diff --git a/samples/jlama-sentiment-v2/src/test/java/airhacks/sentimental/entity/ResultTest.java b/samples/jlama-sentiment-v2/src/test/java/airhacks/sentimental/entity/ResultTest.java new file mode 100644 index 00000000..47fe420c --- /dev/null +++ b/samples/jlama-sentiment-v2/src/test/java/airhacks/sentimental/entity/ResultTest.java @@ -0,0 +1,28 @@ +package airhacks.sentimental.entity; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +public class ResultTest { + @Test + void hallucination() { + var exception = assertThrows(HallucinationException.class, ()-> Result.fromLLMResponse(null, "incredible")); + assertThat(exception).hasMessageContaining("incredible"); + + } + + @Test + void positive() { + var result = Result.fromLLMResponse(null, "positive"); + assertThat(result.isPositive()).isTrue(); + + } + + @Test + void negative() { + var result = Result.fromLLMResponse(null, "negative"); + assertThat(result.isNegative()).isTrue(); + } +}