├── .gitignore ├── LICENSE ├── README.md ├── gemini-api ├── pom.xml └── src │ └── main │ └── java │ └── swiss │ └── ameri │ └── gemini │ ├── api │ ├── Content.java │ ├── FunctionCall.java │ ├── FunctionDeclaration.java │ ├── FunctionResponse.java │ ├── GeminiException.java │ ├── GenAi.java │ ├── GenerationConfig.java │ ├── GenerativeModel.java │ ├── ModelVariant.java │ ├── SafetySetting.java │ ├── Schema.java │ └── TaskType.java │ └── spi │ └── JsonParser.java ├── gemini-gson ├── pom.xml └── src │ └── main │ └── java │ └── swiss │ └── ameri │ └── gemini │ └── gson │ └── GsonJsonParser.java ├── gemini-tester ├── pom.xml └── src │ └── main │ ├── java │ └── swiss │ │ └── ameri │ │ └── gemini │ │ └── tester │ │ └── GeminiTester.java │ └── resources │ └── scones.png └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | pom.xml.tag 3 | pom.xml.releaseBackup 4 | pom.xml.versionsBackup 5 | pom.xml.next 6 | release.properties 7 | dependency-reduced-pom.xml 8 | buildNumber.properties 9 | .mvn/timing.properties 10 | # https://github.com/takari/maven-wrapper#usage-without-binary-jar 11 | .mvn/wrapper/maven-wrapper.jar 12 | 13 | # Eclipse m2e generated files 14 | # Eclipse Core 15 | .project 16 | # JDT-specific (Eclipse Java Development Tools) 17 | .classpath 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | Java library for Gemini API. 4 | See the documentation here: https://ai.google.dev/gemini-api 5 | 6 | # Getting started 7 | 8 | - Generate an API key: https://aistudio.google.com/app/apikey 9 | 10 | # Usage 11 | 12 | ## Dependency 13 | 14 | Add the `gemini-api` dependency. Maven example (replace ${gemini.version} with the latest version): 15 | 16 | 17 | swiss.ameri 18 | gemini-api 19 | ${gemini.version} 20 | 21 | 22 | ## JsonParser 23 | 24 | In order for this library to stay free of dependencies, the user must provide an implementation of 25 | the `swiss.ameri.gemini.spi.JsonParser` interface. 26 | Alternatively, an example Gson implementation can be used with the following dependency (which includes a Gson 27 | dependency) 28 | 29 | 30 | swiss.ameri 31 | gemini-gson 32 | ${gemini.version} 33 | 34 | 35 | ## Example code 36 | 37 | See [gemini-tester](https://github.com/michael-ameri/gemini-api/blob/1beta.0.1.0/gemini-tester/src/main/java/swiss/ameri/gemini/tester/GeminiTester.java) 38 | for some examples. 39 | 40 | JsonParser parser = new GsonJsonParser(); // or some custom implementation 41 | String apiKey = ...; 42 | GenAi genAi = new GenAi( 43 | apiKey, 44 | parser 45 | ); 46 | 47 | // list available models 48 | genAi.listModels().forEach(System.out::println); 49 | 50 | // create a prompt 51 | var model = GenerativeModel.builder() 52 | .modelName(ModelVariant.GEMINI_1_0_PRO) 53 | .addContent(new Content.TextContent( 54 | Content.Role.USER.roleName(), 55 | "Write a 300 word story about a magic backpack." 56 | )) 57 | .build(); 58 | 59 | // execute the prompt, wait for the full response 60 | genAi.generateContent(model) 61 | .thenAccept(System.out::println) 62 | .get(20, TimeUnit.SECONDS); // block here until the response arrives. Probably not a good idea in production code. 63 | 64 | // execute the prompt, process the chunks of responses as they arrive 65 | genAi.generateContentStream(model) 66 | .forEach(System.out::println) 67 | 68 | # Versioning 69 | 70 | The library versioning follows the scheme: 71 | `...` 72 | 73 | Example: 74 | `1beta.0.0.1` 75 | 76 | # Requirements 77 | 78 | - \>= Java 17 79 | 80 | # Modules 81 | 82 | The project is composed of the following maven modules, which are deployed to maven central. 83 | 84 | ## gemini-api 85 | 86 | 87 | swiss.ameri 88 | gemini-api 89 | ${gemini.version} 90 | 91 | 92 | Main module to be used. Must not contain any dependencies to other modules. 93 | 94 | ## gemini-gson 95 | 96 | 97 | swiss.ameri 98 | gemini-gson 99 | ${gemini.version} 100 | 101 | 102 | Provides an example implementation of the `swiss.ameri.gemini.spi.JsonParser` class using `Gson`. 103 | Contains a maven dependency to `Gson` 104 | 105 | ## gemini-tester 106 | 107 | 108 | swiss.ameri 109 | gemini-tester 110 | ${gemini.version} 111 | 112 | 113 | Contains some example code of how the API can be used. 114 | -------------------------------------------------------------------------------- /gemini-api/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | swiss.ameri 8 | gemini 9 | 1beta.0.2.8-SNAPSHOT 10 | 11 | 12 | gemini-api 13 | 14 | Java library to access the gemini API. 15 | See swiss.ameri:gemini-gson for an example JsonParser implementation using gson 16 | 17 | 18 | 19 | 17 20 | swiss.ameri.gemini.api 21 | 22 | 23 | -------------------------------------------------------------------------------- /gemini-api/src/main/java/swiss/ameri/gemini/api/Content.java: -------------------------------------------------------------------------------- 1 | package swiss.ameri.gemini.api; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.Locale; 6 | 7 | /** 8 | * Content which can be sent to gemini API. 9 | */ 10 | public sealed interface Content { 11 | 12 | /** 13 | * Role belonging to this turn in the conversation. 14 | * 15 | * @return string value of a {@link Role} 16 | */ 17 | String role(); 18 | 19 | /** 20 | * Create a {@link FunctionCallContent}. 21 | * 22 | * @param role belonging to this turn in the conversation. 23 | * @param functionCall by the role 24 | * @return a {@link FunctionCallContent} 25 | */ 26 | static FunctionCallContent functionCallContent( 27 | Role role, 28 | FunctionCall functionCall 29 | ) { 30 | return new FunctionCallContent(role == null ? null : role.roleName(), functionCall); 31 | } 32 | 33 | /** 34 | * Create a {@link FunctionCallContent}. 35 | * 36 | * @param role belonging to this turn in the conversation. 37 | * @param functionResponse by the role 38 | * @return a {@link FunctionResponseContent} 39 | */ 40 | static FunctionResponseContent functionResponseContent( 41 | Role role, 42 | FunctionResponse functionResponse 43 | ) { 44 | return new FunctionResponseContent(role == null ? null : role.roleName(), functionResponse); 45 | } 46 | 47 | /** 48 | * Create a {@link TextContent}. 49 | * 50 | * @param role belonging to this turn in the conversation. 51 | * @param text by the role 52 | * @return a {@link TextContent} 53 | */ 54 | static TextContent textContent( 55 | Role role, 56 | String text 57 | ) { 58 | return new TextContent(role == null ? null : role.roleName(), text); 59 | } 60 | 61 | /** 62 | * Create a {@link MediaContent}. 63 | * 64 | * @param role belonging to this turn in the conversation. 65 | * @param mimeType see {@link MediaData} 66 | * @param mediaBase64 see {@link MediaData} 67 | * @return a {@link MediaContent} 68 | */ 69 | static MediaContent mediaContent( 70 | Role role, 71 | String mimeType, 72 | String mediaBase64 73 | 74 | ) { 75 | return new MediaContent(role == null ? null : role.roleName(), new MediaData(mimeType, mediaBase64)); 76 | } 77 | 78 | /** 79 | * For combined text and media content in one turn 80 | * 81 | * @return a {@link TextAndMediaContent.TextAndMediaContentBuilder} 82 | */ 83 | static TextAndMediaContent.TextAndMediaContentBuilder textAndMediaContentBuilder() { 84 | return TextAndMediaContent.builder(); 85 | } 86 | 87 | /** 88 | * A predicted FunctionCall returned from the model that contains a string representing the FunctionDeclaration.name 89 | * with the arguments and their values. 90 | * 91 | * @param role belonging to this turn in the conversation. see {@link Role} 92 | * @param functionCall returned form the model 93 | */ 94 | record FunctionCallContent( 95 | String role, 96 | FunctionCall functionCall 97 | ) implements Content { 98 | } 99 | 100 | /** 101 | * The result output from a FunctionCall that contains a string representing the FunctionDeclaration.name and 102 | * a structured JSON object containing any output from the function is used as context to the model. 103 | * This should contain the result of aFunctionCall made based on model prediction. 104 | * 105 | * @param role belonging to this turn in the conversation. see {@link Role} 106 | * @param functionResponse response to a function call 107 | */ 108 | record FunctionResponseContent( 109 | String role, 110 | FunctionResponse functionResponse 111 | ) implements Content { 112 | } 113 | 114 | /** 115 | * A part of a conversation that contains text. 116 | * 117 | * @param role belonging to this turn in the conversation. see {@link Role} 118 | * @param text by the role 119 | */ 120 | record TextContent( 121 | String role, 122 | String text 123 | ) implements Content { 124 | } 125 | 126 | /** 127 | * A part of a conversation that contains media. 128 | * 129 | * @param role belonging to this turn in the conversation. see {@link Role} 130 | * @param media data 131 | */ 132 | record MediaContent( 133 | String role, 134 | MediaData media 135 | ) implements Content { 136 | } 137 | 138 | /** 139 | * A part of a conversation that contains text and media. 140 | * 141 | * @param role belonging to this turn in the conversation. see {@link Role} 142 | * @param text of the conversation part 143 | * @param media the medias 144 | */ 145 | record TextAndMediaContent( 146 | String role, 147 | String text, 148 | List media 149 | ) implements Content { 150 | 151 | /** 152 | * Create a {@link TextAndMediaContentBuilder}. 153 | * 154 | * @return a new builder 155 | */ 156 | public static TextAndMediaContentBuilder builder() { 157 | return new TextAndMediaContentBuilder(); 158 | } 159 | 160 | 161 | /** 162 | * Builder for {@link TextAndMediaContent}. 163 | */ 164 | public static class TextAndMediaContentBuilder { 165 | private String role; 166 | private String text; 167 | private final List media = new ArrayList<>(); 168 | 169 | private TextAndMediaContentBuilder() { 170 | } 171 | 172 | /** 173 | * Set the role 174 | * 175 | * @param role to set 176 | * @return this 177 | * @see #role(Role) 178 | */ 179 | public TextAndMediaContentBuilder role(String role) { 180 | this.role = role; 181 | return this; 182 | } 183 | 184 | /** 185 | * Set the role 186 | * 187 | * @param role to set 188 | * @return this 189 | */ 190 | public TextAndMediaContentBuilder role(Role role) { 191 | return role(role == null ? null : role.roleName()); 192 | } 193 | 194 | /** 195 | * Set the text 196 | * 197 | * @param text to set 198 | * @return this 199 | */ 200 | public TextAndMediaContentBuilder text(String text) { 201 | this.text = text; 202 | return this; 203 | } 204 | 205 | /** 206 | * Add media 207 | * 208 | * @param media to add 209 | * @return this 210 | */ 211 | public TextAndMediaContentBuilder addMedia(MediaData media) { 212 | this.media.add(media); 213 | return this; 214 | } 215 | 216 | /** 217 | * Create the result. It is not validated. 218 | * 219 | * @return a newly built {@link TextAndMediaContent} 220 | */ 221 | public TextAndMediaContent build() { 222 | return new TextAndMediaContent(role, text, media); 223 | } 224 | } 225 | 226 | } 227 | 228 | /** 229 | * Media used during a conversion. 230 | * 231 | * @param mimeType e.g. image/jpeg 232 | * @param mediaBase64 the media, base64 encoded 233 | */ 234 | record MediaData( 235 | String mimeType, 236 | String mediaBase64 237 | ) { 238 | } 239 | 240 | /** 241 | * The producer of the content. Mainly relevant for multi turn conversations. 242 | */ 243 | enum Role { 244 | /** 245 | * Content by user. 246 | */ 247 | USER, 248 | /** 249 | * Content by model. 250 | */ 251 | MODEL; 252 | 253 | private final String roleName; 254 | 255 | Role() { 256 | this.roleName = name().toLowerCase(Locale.ROOT); 257 | } 258 | 259 | /** 260 | * Role name for the api 261 | * 262 | * @return Role name for the api 263 | */ 264 | public String roleName() { 265 | return roleName; 266 | } 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /gemini-api/src/main/java/swiss/ameri/gemini/api/FunctionCall.java: -------------------------------------------------------------------------------- 1 | package swiss.ameri.gemini.api; 2 | 3 | import java.util.Map; 4 | 5 | /** 6 | * A predicted FunctionCall returned from the model that contains a string representing the FunctionDeclaration.name 7 | * with the arguments and their values. 8 | * 9 | * @param name Required. The name of the function to call. Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length of 63. 10 | * @param args Optional. The function parameters and values in JSON object format. 11 | */ 12 | public record FunctionCall( 13 | String name, 14 | Map args 15 | ) { 16 | } 17 | -------------------------------------------------------------------------------- /gemini-api/src/main/java/swiss/ameri/gemini/api/FunctionDeclaration.java: -------------------------------------------------------------------------------- 1 | package swiss.ameri.gemini.api; 2 | 3 | /** 4 | * Structured representation of a function declaration as defined by the OpenAPI 3.03 specification. 5 | * Included in this declaration are the function name and parameters. 6 | * This FunctionDeclaration is a representation of a block of code that can be used as a Tool by the model and executed by the client. 7 | * 8 | * @param name Required. The name of the function. Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length of 63. 9 | * @param description Required. A brief description of the function. 10 | * @param parameters Optional. Describes the parameters to this function. 11 | * Reflects the Open API 3.03 Parameter Object string Key: the name of the parameter. 12 | * Parameter names are case-sensitive. 13 | * Schema Value: the Schema defining the type used for the parameter. 14 | */ 15 | public record FunctionDeclaration( 16 | String name, 17 | String description, 18 | Schema parameters 19 | ) { 20 | } 21 | -------------------------------------------------------------------------------- /gemini-api/src/main/java/swiss/ameri/gemini/api/FunctionResponse.java: -------------------------------------------------------------------------------- 1 | package swiss.ameri.gemini.api; 2 | 3 | import java.util.Map; 4 | 5 | /** 6 | * The result output from a FunctionCall that contains a string representing the FunctionDeclaration.name and 7 | * a structured JSON object containing any output from the function is used as context to the model. 8 | * This should contain the result of aFunctionCall made based on model prediction. 9 | * 10 | * @param name Required. The name of the function to call. Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length of 63. 11 | * @param response Required. The function response in JSON object format. 12 | */ 13 | public record FunctionResponse( 14 | String name, 15 | Map response 16 | ) { 17 | } 18 | -------------------------------------------------------------------------------- /gemini-api/src/main/java/swiss/ameri/gemini/api/GeminiException.java: -------------------------------------------------------------------------------- 1 | package swiss.ameri.gemini.api; 2 | 3 | import java.util.OptionalInt; 4 | 5 | /** 6 | * Thrown if we get an unexpected body or response code from Gemini API. 7 | */ 8 | public class GeminiException extends RuntimeException { 9 | 10 | private final Integer code; 11 | 12 | 13 | /** 14 | * Create a new exception. 15 | * 16 | * @param message of the exception 17 | */ 18 | public GeminiException(String message) { 19 | this(message, null, null); 20 | } 21 | 22 | /** 23 | * Create a new exception. 24 | * 25 | * @param message of the exception 26 | * @param code optional http response code 27 | */ 28 | public GeminiException(String message, int code) { 29 | this(message, code, null); 30 | } 31 | 32 | /** 33 | * Create a new exception. 34 | * 35 | * @param message of the exception 36 | * @param cause the cause of the exception 37 | */ 38 | public GeminiException(String message, Throwable cause) { 39 | this(message, null, cause); 40 | } 41 | 42 | /** 43 | * Create a new exception. 44 | * 45 | * @param message of the exception 46 | * @param code optional http response code 47 | * @param cause the cause of the exception 48 | */ 49 | public GeminiException(String message, Integer code, Throwable cause) { 50 | super(message, cause); 51 | this.code = code; 52 | } 53 | 54 | /** 55 | * Get the optional http response code. 56 | * 57 | * @return the response code, if present 58 | */ 59 | public OptionalInt getCode() { 60 | return code == null ? OptionalInt.empty() : OptionalInt.of(code); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /gemini-api/src/main/java/swiss/ameri/gemini/api/GenAi.java: -------------------------------------------------------------------------------- 1 | package swiss.ameri.gemini.api; 2 | 3 | import swiss.ameri.gemini.spi.JsonParser; 4 | 5 | import java.io.IOException; 6 | import java.io.UncheckedIOException; 7 | import java.net.URI; 8 | import java.net.http.HttpClient; 9 | import java.net.http.HttpRequest; 10 | import java.net.http.HttpResponse; 11 | import java.util.ArrayList; 12 | import java.util.Collection; 13 | import java.util.List; 14 | import java.util.Map; 15 | import java.util.Objects; 16 | import java.util.Optional; 17 | import java.util.UUID; 18 | import java.util.concurrent.CompletableFuture; 19 | import java.util.concurrent.ConcurrentHashMap; 20 | import java.util.stream.Collectors; 21 | import java.util.stream.Stream; 22 | 23 | import static java.util.Collections.emptyList; 24 | 25 | 26 | /** 27 | * Entry point for all interactions with Gemini API. 28 | * Note that some methods store state (e.g. {@link #generateContent(GenerativeModel)} or ${@link #generateContentStream(GenerativeModel)}). 29 | * Call the {@link #close()} method to clean up the state. 30 | * This class is thread safe. 31 | */ 32 | public class GenAi implements AutoCloseable { 33 | 34 | private static final String STREAM_LINE_PREFIX = "data: "; 35 | private static final int STREAM_LINE_PREFIX_LENGTH = STREAM_LINE_PREFIX.length(); 36 | 37 | private final String urlPrefix = "https://generativelanguage.googleapis.com/v1beta"; 38 | 39 | private final String apiKey; 40 | private final HttpClient client; 41 | private final JsonParser jsonParser; 42 | private final Map responseById = new ConcurrentHashMap<>(); 43 | 44 | /** 45 | * Create a new instance with a default {@link HttpClient} 46 | * 47 | * @param apiKey to be used for all communications with Gemini API 48 | * @param jsonParser used to (de-)serialize JSON objects 49 | */ 50 | public GenAi( 51 | String apiKey, 52 | JsonParser jsonParser 53 | ) { 54 | this( 55 | apiKey, 56 | jsonParser, 57 | HttpClient.newBuilder().build() 58 | ); 59 | } 60 | 61 | /** 62 | * Create a new instance 63 | * 64 | * @param apiKey to be used for all communications with Gemini API 65 | * @param jsonParser used to (de-)serialize JSON objects 66 | * @param client a custom {@link HttpClient} for communication with Gemini API 67 | */ 68 | public GenAi( 69 | String apiKey, 70 | JsonParser jsonParser, 71 | HttpClient client 72 | ) { 73 | this.apiKey = apiKey; 74 | this.jsonParser = jsonParser; 75 | this.client = client; 76 | } 77 | 78 | /** 79 | * List models that are currently available. 80 | * 81 | * @return available models 82 | */ 83 | public List listModels() { 84 | return execute(() -> { 85 | HttpResponse response = client.send( 86 | HttpRequest.newBuilder() 87 | .GET() 88 | .uri(URI.create("%s/models?key=%s".formatted(urlPrefix, apiKey))) 89 | .build(), 90 | HttpResponse.BodyHandlers.ofString() 91 | ); 92 | 93 | return jsonParser.fromJson(response.body(), ModelResponse.class) 94 | .models(); 95 | }); 96 | } 97 | 98 | /** 99 | * Get information of a model. Can be used to create a {@link GenerativeModel}. 100 | * 101 | * @param model of which the information is wanted. 102 | * @return information of a model 103 | * @see #listModels() 104 | */ 105 | public Model getModel(ModelVariant model) { 106 | return getModel(model.variant()); 107 | } 108 | 109 | /** 110 | * Get model information. 111 | * 112 | * @param model must start with "models/" 113 | * @return information of a model 114 | */ 115 | public Model getModel(String model) { 116 | return execute(() -> { 117 | HttpResponse response = client.send( 118 | HttpRequest.newBuilder() 119 | .GET() 120 | .uri(URI.create("%s/%s?key=%s".formatted(urlPrefix, model, apiKey))) 121 | .build(), 122 | HttpResponse.BodyHandlers.ofString() 123 | ); 124 | return jsonParser.fromJson(response.body(), Model.class); 125 | }); 126 | } 127 | 128 | /** 129 | * Get the usage metadata of a {@link GeneratedContent#id()}. 130 | * 131 | * @param id of the corresponding {@link GeneratedContent} 132 | * @return the corresponding metadata, or an empty optional 133 | */ 134 | public Optional usageMetadata(UUID id) { 135 | return Optional.ofNullable(responseById.get(id)) 136 | .map(GenerateContentResponse::usageMetadata); 137 | } 138 | 139 | /** 140 | * Get the safety ratings of a {@link GeneratedContent#id()}. 141 | * 142 | * @param id of the corresponding {@link GeneratedContent} 143 | * @return the corresponding safety ratings, or an empty optional 144 | */ 145 | public List safetyRatings(UUID id) { 146 | GenerateContentResponse response = responseById.get(id); 147 | if (response == null) { 148 | return emptyList(); 149 | } 150 | return response.candidates().stream() 151 | // when streaming, we sometimes don't get a safety rating... (with 1.5 pro) 152 | .map(ResponseCandidate::safetyRatings) 153 | .filter(Objects::nonNull) 154 | .flatMap(Collection::stream) 155 | .toList(); 156 | } 157 | 158 | 159 | /** 160 | * Runs a model's tokenizer on input content and returns the token count. 161 | * When using long prompts, it might be useful to count tokens before sending any content to the model. 162 | * 163 | * @param model to be analyzed 164 | * @return the token count 165 | */ 166 | public CompletableFuture countTokens(GenerativeModel model) { 167 | return execute(() -> { 168 | CompletableFuture> response = client.sendAsync( 169 | HttpRequest.newBuilder() 170 | .POST(HttpRequest.BodyPublishers.ofString( 171 | jsonParser.toJson(new CountTokenRequest(convert(model))) 172 | )) 173 | .uri(URI.create("%s/%s:countTokens?key=%s".formatted(urlPrefix, model.modelName(), apiKey))) 174 | .build(), 175 | HttpResponse.BodyHandlers.ofString() 176 | ); 177 | return response 178 | .thenApply(HttpResponse::body) 179 | .thenApply(body -> { 180 | try { 181 | var ctr = jsonParser.fromJson(body, CountTokenResponse.class); 182 | if (ctr.totalTokens() == null) { 183 | throw new GeminiException("No token field in response"); 184 | } 185 | return ctr.totalTokens(); 186 | } catch (Exception e) { 187 | throw new GeminiException("Unexpected body:\n" + body, e); 188 | } 189 | }); 190 | }); 191 | } 192 | 193 | /** 194 | * Generates a response from Gemini API based on the given {@code model}. The response is streamed in chunks of text. The 195 | * stream items are delivered as they arrive. 196 | * Once the call has been completed, metadata and safety ratings can be obtained by calling 197 | * {@link #usageMetadata(UUID)} or {@link #safetyRatings(UUID)}. If those methods are called while the stream is still 198 | * active, the last available statistics are returned. 199 | * 200 | * @param model with the necessary information for Gemini API to generate content 201 | * @return A live stream of the response, as it arrives 202 | * @see #generateContent(GenerativeModel) which returns the whole response at once (asynchronously) 203 | */ 204 | public Stream generateContentStream(GenerativeModel model) { 205 | return execute(() -> { 206 | UUID uuid = UUID.randomUUID(); 207 | HttpRequest request = HttpRequest.newBuilder() 208 | .POST(HttpRequest.BodyPublishers.ofString( 209 | jsonParser.toJson(convert(model)) 210 | )) 211 | .uri(URI.create("%s/%s:streamGenerateContent?alt=sse&key=%s".formatted(urlPrefix, model.modelName(), apiKey))) 212 | .build(); 213 | 214 | HttpResponse> response = client.send( 215 | request, 216 | HttpResponse.BodyHandlers.ofLines() 217 | ); 218 | // e.g. Response code: 503 (Service Unavailable); Time: 5813ms (5 s 813 ms) 219 | // 220 | //{ 221 | // "error": { 222 | // "code": 503, 223 | // "message": "The model is overloaded. Please try again later.", 224 | // "status": "UNAVAILABLE" 225 | // } 226 | //} 227 | 228 | if (response.statusCode() != 200) { 229 | // in case of an error, we don't stream, but block and give the whole response, because 230 | // we don't want to parse it and potentially cause more errors 231 | String error = response.body() 232 | .collect(Collectors.joining("\n")); 233 | throw new GeminiException( 234 | "Unexpected stream response:\n%s".formatted(error), 235 | response.statusCode() 236 | ); 237 | } 238 | 239 | return response.body() 240 | .filter(l -> l.length() > STREAM_LINE_PREFIX_LENGTH) 241 | .map(line -> parse(line.substring(STREAM_LINE_PREFIX_LENGTH), uuid)); 242 | }); 243 | } 244 | 245 | /** 246 | * Generates a response from Gemini API based on the given {@code model}. 247 | * Once the call has been completed, metadata and safety ratings can be obtained by calling 248 | * {@link #usageMetadata(UUID)} or {@link #safetyRatings(UUID)} 249 | * 250 | * @param model with the necessary information for Gemini API to generate content 251 | * @return a {@link CompletableFuture} which completes once the response from Gemini API has arrived. The {@link CompletableFuture} 252 | * fails, if an unexpected response returns (e.g. invalid token or parameters are used) 253 | * @see #generateContentStream(GenerativeModel) to stream the response in chunks, instead of receiving all at once 254 | */ 255 | public CompletableFuture generateContent(GenerativeModel model) { 256 | return execute(() -> { 257 | UUID uuid = UUID.randomUUID(); 258 | CompletableFuture> response = client.sendAsync( 259 | HttpRequest.newBuilder() 260 | .POST(HttpRequest.BodyPublishers.ofString( 261 | jsonParser.toJson(convert(model)) 262 | )) 263 | .uri(URI.create("%s/%s:generateContent?key=%s".formatted(urlPrefix, model.modelName(), apiKey))) 264 | .build(), 265 | HttpResponse.BodyHandlers.ofString() 266 | ); 267 | return response 268 | .thenApply(HttpResponse::body) 269 | .thenApply(body -> parse(body, uuid)); 270 | }); 271 | } 272 | 273 | /** 274 | * Embedding is a technique used to represent information as a list of floating point numbers in an array. 275 | * With Gemini, you can represent text (words, sentences, and blocks of text) in a vectorized form, 276 | * making it easier to compare and contrast embeddings. 277 | * For example, two texts that share a similar subject or sentiment should have similar embeddings, 278 | * which can be identified through mathematical comparison techniques such as cosine similarity. 279 | * 280 | * @param model to use. Currently, only {@link ModelVariant#TEXT_EMBEDDING_004} is allowed. 281 | * @param taskType Optional. Optional task type for which the embeddings will be used. For possible values, see {@link TaskType} 282 | * @param title Optional. An optional title for the text. Only applicable when TaskType is RETRIEVAL_DOCUMENT. 283 | * Note: Specifying a title for RETRIEVAL_DOCUMENT provides better quality embeddings for retrieval. 284 | * @param outputDimensionality Optional. Optional reduced dimension for the output embedding. 285 | * If set, excessive values in the output embedding are truncated from the end. 286 | * Supported by newer models since 2024, and the earlier model (models/embedding-001) cannot specify this value. 287 | * @return List of values 288 | * @apiNote Only {@link swiss.ameri.gemini.api.Content.TextContent} are allowed. 289 | */ 290 | public CompletableFuture> embedContents( 291 | GenerativeModel model, 292 | String taskType, 293 | String title, 294 | Long outputDimensionality 295 | ) { 296 | return execute(() -> { 297 | 298 | var requests = convertGenerationContents(model) 299 | .stream() 300 | .map(generationContent -> new EmbedContentRequest( 301 | model.modelName(), 302 | generationContent, 303 | taskType, 304 | title, 305 | outputDimensionality 306 | )) 307 | .toList(); 308 | 309 | var request = new BatchEmbedContentRequest(requests); 310 | 311 | CompletableFuture> response = client.sendAsync( 312 | HttpRequest.newBuilder() 313 | .POST(HttpRequest.BodyPublishers.ofString( 314 | jsonParser.toJson(request) 315 | )) 316 | .uri(URI.create("%s/%s:batchEmbedContents?key=%s".formatted(urlPrefix, model.modelName(), apiKey))) 317 | .build(), 318 | HttpResponse.BodyHandlers.ofString() 319 | ); 320 | return response 321 | .thenApply(HttpResponse::body) 322 | .thenApply(body -> { 323 | try { 324 | BatchEmbedContentResponse becr = jsonParser.fromJson(body, BatchEmbedContentResponse.class); 325 | if (becr.embeddings() == null) { 326 | throw new GeminiException("No embeddings field in response:\n" + body); 327 | } 328 | return becr 329 | .embeddings(); 330 | } catch (Exception e) { 331 | throw new GeminiException("Unexpected body:\n" + body, e); 332 | } 333 | }); 334 | 335 | }); 336 | } 337 | 338 | private static GenerateContentRequest convert(GenerativeModel model) { 339 | List generationContents = convertGenerationContents(model); 340 | List tools = new ArrayList<>(); 341 | if (!model.functionDeclarations().isEmpty()) { 342 | tools.add(new Tool(model.functionDeclarations())); 343 | } 344 | return new GenerateContentRequest( 345 | model.modelName(), 346 | generationContents, 347 | model.safetySettings(), 348 | model.generationConfig(), 349 | model.systemInstruction().isEmpty() ? null : 350 | new SystemInstruction( 351 | model.systemInstruction().stream() 352 | .map(SystemInstructionPart::new) 353 | .toList() 354 | ), 355 | tools.isEmpty() ? null : 356 | List.of(new Tool(model.functionDeclarations())) 357 | ); 358 | } 359 | 360 | private static List convertGenerationContents(GenerativeModel model) { 361 | return model.contents().stream() 362 | .map(content -> { 363 | // change to "switch" over sealed type with jdk 21 364 | if (content instanceof Content.TextContent textContent) { 365 | return new GenerationContent( 366 | textContent.role(), 367 | List.of( 368 | new GenerationPart( 369 | textContent.text(), 370 | null, 371 | null, 372 | null 373 | ) 374 | ) 375 | ); 376 | } else if (content instanceof Content.MediaContent imageContent) { 377 | return new GenerationContent( 378 | imageContent.role(), 379 | List.of( 380 | new GenerationPart( 381 | null, 382 | new InlineData( 383 | imageContent.media().mimeType(), 384 | imageContent.media().mediaBase64() 385 | ), 386 | null, 387 | null 388 | ) 389 | ) 390 | ); 391 | } else if (content instanceof Content.TextAndMediaContent textAndImagesContent) { 392 | return new GenerationContent( 393 | textAndImagesContent.role(), 394 | Stream.concat( 395 | Stream.of( 396 | new GenerationPart( 397 | textAndImagesContent.text(), 398 | null, 399 | null, 400 | null 401 | ) 402 | ), 403 | textAndImagesContent.media().stream() 404 | .map(imageData -> new GenerationPart( 405 | null, 406 | new InlineData( 407 | imageData.mimeType(), 408 | imageData.mediaBase64() 409 | ), 410 | null, 411 | null 412 | )) 413 | ).toList() 414 | ); 415 | } else if (content instanceof Content.FunctionCallContent functionCallContent) { 416 | return new GenerationContent( 417 | functionCallContent.role(), 418 | List.of( 419 | new GenerationPart( 420 | null, 421 | null, 422 | functionCallContent.functionCall(), 423 | null 424 | ) 425 | ) 426 | ); 427 | } else if (content instanceof Content.FunctionResponseContent functionResponseContent) { 428 | return new GenerationContent( 429 | functionResponseContent.role(), 430 | List.of( 431 | new GenerationPart( 432 | null, 433 | null, 434 | null, 435 | functionResponseContent.functionResponse() 436 | ) 437 | ) 438 | ); 439 | } else { 440 | throw new GeminiException("Unexpected content:\n" + content); 441 | } 442 | }) 443 | .toList(); 444 | } 445 | 446 | private T execute(ThrowingSupplier supplier) { 447 | try { 448 | return supplier.get(); 449 | } catch (IOException e) { 450 | throw new UncheckedIOException(e); 451 | } catch (InterruptedException e) { 452 | Thread.currentThread().interrupt(); 453 | throw new GeminiException("Thread was interrupted.", e); 454 | } 455 | } 456 | 457 | /** 458 | * Clears the internal state. 459 | */ 460 | @Override 461 | public void close() { 462 | responseById.clear(); 463 | } 464 | 465 | /** 466 | * Content generated by Gemini API. 467 | * 468 | * @param id the id of the request, for subsequent queries regarding metadata of the query 469 | * @param text of the generated content 470 | * @param functionCall Optional. if the model wants to call a function 471 | * @param finishReason the reason generation was finished, according to FinishReason 472 | */ 473 | public record GeneratedContent( 474 | UUID id, 475 | String text, 476 | FunctionCall functionCall, 477 | String finishReason 478 | ) { 479 | } 480 | 481 | /** 482 | * Usage metadata for a given request. 483 | * 484 | * @param promptTokenCount Number of tokens in the prompt. 485 | * @param candidatesTokenCount Total number of tokens for the generated response. 486 | * @param totalTokenCount Total token count for the generation request (prompt + candidates). 487 | */ 488 | public record UsageMetadata( 489 | int promptTokenCount, 490 | int candidatesTokenCount, 491 | int totalTokenCount 492 | ) { 493 | } 494 | 495 | /** 496 | * Safety rating for a given response. 497 | * 498 | * @param category The category for this rating. see {@link swiss.ameri.gemini.api.SafetySetting.HarmCategory} 499 | * @param probability The probability of harm for this content. see {@link swiss.ameri.gemini.api.SafetySetting.HarmProbability} 500 | */ 501 | public record SafetyRating( 502 | String category, 503 | String probability 504 | ) { 505 | 506 | /** 507 | * Convert the safety rating to a typed safety rating. 508 | * Might crash if Gemini API changes, and an enum value is missing. 509 | * 510 | * @return the TypedSafetyRating 511 | */ 512 | public TypedSafetyRating toTypedSafetyRating() { 513 | return new TypedSafetyRating( 514 | SafetySetting.HarmCategory.valueOf(category()), 515 | SafetySetting.HarmProbability.valueOf(probability()) 516 | ); 517 | } 518 | 519 | /** 520 | * Typed values. This is done separately, since enum values might be missing compared to Gemini API 521 | * 522 | * @param harmCategory of this rating 523 | * @param probability of this rating 524 | */ 525 | public record TypedSafetyRating( 526 | SafetySetting.HarmCategory harmCategory, 527 | SafetySetting.HarmProbability probability 528 | ) { 529 | } 530 | 531 | } 532 | 533 | /** 534 | * A list of floats representing an embedding. 535 | * 536 | * @param values A list of floats representing an embedding. 537 | */ 538 | public record ContentEmbedding( 539 | List values 540 | ) { 541 | } 542 | 543 | /** 544 | * Information on a model 545 | * 546 | * @param name The resource name of the Model. 547 | *

548 | * Format: models/{model} with a {model} naming convention of: 549 | *

550 | * "{baseModelId}-{version}" 551 | * Examples: 552 | *

553 | * models/chat-bison-001 554 | * @param baseModelId The name of the base model, pass this to the generation request. 555 | * @param version The version number of the model. 556 | * @param displayName The human-readable name of the model. E.g. "Chat Bison". 557 | *

558 | * The name can be up to 128 characters long and can consist of any UTF-8 characters. 559 | * @param description A short description of the model. 560 | * @param inputTokenLimit Maximum number of input tokens allowed for this model. 561 | * @param outputTokenLimit Maximum number of output tokens available for this model. 562 | * @param supportedGenerationMethods The model's supported generation methods. 563 | *

564 | * The method names are defined as Pascal case strings, such as generateMessage which correspond to API methods. 565 | * @param temperature Controls the randomness of the output. 566 | *

567 | * Values can range over [0.0,2.0], inclusive. 568 | * A higher value will produce responses that are more varied, 569 | * while a value closer to 0.0 will typically result in less surprising responses from the model. 570 | * This value specifies default to be used by the backend while making the call to the model. 571 | * @param topP For Nucleus sampling. 572 | *

573 | * Nucleus sampling considers the smallest set of tokens whose probability sum is at least topP. 574 | * This value specifies default to be used by the backend while making the call to the model. 575 | * @param topK For Top-k sampling. 576 | *

577 | * Top-k sampling considers the set of topK most probable tokens. 578 | * This value specifies default to be used by the backend while making the call to the model. 579 | * If empty, indicates the model doesn't use top-k sampling, and topK isn't allowed as a generation parameter. 580 | */ 581 | public record Model( 582 | String name, 583 | String baseModelId, 584 | String version, 585 | String displayName, 586 | String description, 587 | int inputTokenLimit, 588 | int outputTokenLimit, 589 | List supportedGenerationMethods, 590 | double temperature, 591 | double topP, 592 | int topK 593 | ) { 594 | } 595 | 596 | private GeneratedContent parse(String body, UUID uuid) { 597 | try { 598 | var gcr = jsonParser.fromJson(body, GenerateContentResponse.class); 599 | // each element can just replace the previous one 600 | this.responseById.put(uuid, gcr); 601 | // we assume we always get a candidate. Otherwise, there is probably something wrong with the input 602 | var candidate = gcr.candidates().get(0); 603 | if (candidate.content() == null) { 604 | return new GeneratedContent(uuid, "", null, candidate.finishReason()); 605 | } 606 | GenerationPart firstPart = candidate.content().parts().get(0); 607 | return new GeneratedContent(uuid, firstPart.text(), firstPart.functionCall(), candidate.finishReason()); 608 | } catch (Exception e) { 609 | throw new GeminiException("Unexpected body:\n" + body, e); 610 | } 611 | } 612 | 613 | private record BatchEmbedContentRequest( 614 | List requests 615 | ) { 616 | } 617 | 618 | private record EmbedContentRequest( 619 | String model, 620 | GenerationContent content, 621 | String taskType, 622 | String title, 623 | Long outputDimensionality 624 | ) { 625 | } 626 | 627 | private record BatchEmbedContentResponse( 628 | List embeddings 629 | ) { 630 | } 631 | 632 | private record CountTokenRequest( 633 | GenerateContentRequest generateContentRequest 634 | ) { 635 | } 636 | 637 | private record CountTokenResponse( 638 | Long totalTokens 639 | ) { 640 | } 641 | 642 | private record GenerateContentResponse( 643 | UsageMetadata usageMetadata, 644 | List candidates 645 | ) { 646 | } 647 | 648 | private record ResponseCandidate( 649 | GenerationContent content, 650 | String finishReason, 651 | int index, 652 | List safetyRatings 653 | ) { 654 | } 655 | 656 | private record GenerateContentRequest( 657 | // for some reason, model is required for countToken, but not for the others. 658 | // But it seems to be acceptable for the others, so we just add it to all for now 659 | String model, 660 | List contents, 661 | List safetySettings, 662 | GenerationConfig generationConfig, 663 | SystemInstruction systemInstruction, 664 | List tools 665 | ) { 666 | } 667 | 668 | /** 669 | * See Tool 670 | */ 671 | private record Tool( 672 | List functionDeclarations 673 | // still missing CodeExecution and GoogleSearchRetrieval 674 | ) { 675 | } 676 | 677 | private record SystemInstruction( 678 | List parts 679 | ) { 680 | } 681 | 682 | private record SystemInstructionPart( 683 | String text 684 | ) { 685 | } 686 | 687 | private record GenerationContent( 688 | String role, 689 | List parts 690 | ) { 691 | } 692 | 693 | /** 694 | * See Part 695 | */ 696 | private record GenerationPart( 697 | // contains one of these 698 | String text, 699 | InlineData inline_data, 700 | FunctionCall functionCall, 701 | FunctionResponse functionResponse 702 | ) { 703 | } 704 | 705 | private record InlineData( 706 | String mime_type, 707 | String data 708 | ) { 709 | } 710 | 711 | private record ModelResponse(List models) { 712 | } 713 | 714 | private interface ThrowingSupplier { 715 | T get() throws IOException, InterruptedException; 716 | } 717 | } 718 | -------------------------------------------------------------------------------- /gemini-api/src/main/java/swiss/ameri/gemini/api/GenerationConfig.java: -------------------------------------------------------------------------------- 1 | package swiss.ameri.gemini.api; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | /** 7 | * Generation configuration. 8 | * 9 | * @param stopSequences Optional. The set of character sequences (up to 5) that will stop output generation. 10 | * If specified, the API will stop at the first appearance of a stop sequence. 11 | * The stop sequence will not be included as part of the response. 12 | * @param responseMimeType Optional. Output response mimetype of the generated candidate text. 13 | * Supported mimetype: text/plain: (default) Text output. 14 | * application/json: JSON response in the candidates. 15 | * @param responseSchema Optional. Output response schema of the generated candidate text when response mime type can have schema. 16 | * Schema can be objects, primitives or arrays and is a subset of OpenAPI schema. 17 | * If set, a compatible responseMimeType must also be set. Compatible mimetypes: application/json: Schema for JSON response. 18 | * @param maxOutputTokens Optional. The maximum number of tokens to include in a candidate. 19 | * Note: The default value varies by model, see the Model.output_token_limit attribute of the Model returned from the getModel function. 20 | * @param temperature Optional. Controls the randomness of the output. 21 | * Note: The default value varies by model, see the Model. temperature attribute of the Model returned from the getModel function. 22 | * Values can range from [0.0, 2.0]. 23 | * @param topP Optional. The maximum cumulative probability of tokens to consider when sampling. 24 | * The model uses combined Top-k and nucleus sampling. 25 | * Tokens are sorted based on their assigned probabilities so that only the most likely tokens are considered. 26 | * Top-k sampling directly limits the maximum number of tokens to consider, while Nucleus sampling limits number of tokens based on the cumulative probability. 27 | * Note: The default value varies by model, see the Model.top_p attribute of the Model returned from the getModel function. 28 | * @param topK Optional. The maximum number of tokens to consider when sampling. 29 | * Models use nucleus sampling or combined Top-k and nucleus sampling. 30 | * Top-k sampling considers the set of topK most probable tokens. 31 | * Models running with nucleus sampling don't allow topK setting. 32 | * Note: The default value varies by model, see the Model.top_k attribute of the Model returned from the getModel function. 33 | * Empty topK field in Model indicates the model doesn't apply top-k sampling and doesn't allow setting topK on requests. 34 | * @see GenerationConfig 35 | */ 36 | public record GenerationConfig( 37 | List stopSequences, 38 | String responseMimeType, 39 | Schema responseSchema, 40 | Integer maxOutputTokens, 41 | Double temperature, 42 | Double topP, 43 | Integer topK 44 | ) { 45 | 46 | /** 47 | * Builder for {@link GenerationConfig}. 48 | */ 49 | public static GenerationConfigBuilder builder() { 50 | return new GenerationConfigBuilder(); 51 | } 52 | 53 | public static class GenerationConfigBuilder { 54 | private final List stopSequences = new ArrayList<>(); 55 | private String responseMimeType; 56 | private Schema responseSchema; 57 | private Integer maxOutputTokens; 58 | private Double temperature; 59 | private Double topP; 60 | private Integer topK; 61 | 62 | public GenerationConfigBuilder addStopSequence(String stopSequence) { 63 | this.stopSequences.add(stopSequence); 64 | return this; 65 | } 66 | 67 | public GenerationConfigBuilder responseMimeType(String responseMimeType) { 68 | this.responseMimeType = responseMimeType; 69 | return this; 70 | } 71 | 72 | public GenerationConfigBuilder responseSchema(Schema responseSchema) { 73 | this.responseSchema = responseSchema; 74 | return this; 75 | } 76 | 77 | public GenerationConfigBuilder maxOutputTokens(Integer maxOutputTokens) { 78 | this.maxOutputTokens = maxOutputTokens; 79 | return this; 80 | } 81 | 82 | public GenerationConfigBuilder temperature(Double temperature) { 83 | this.temperature = temperature; 84 | return this; 85 | } 86 | 87 | public GenerationConfigBuilder topP(Double topP) { 88 | this.topP = topP; 89 | return this; 90 | } 91 | 92 | public GenerationConfigBuilder topK(Integer topK) { 93 | this.topK = topK; 94 | return this; 95 | } 96 | 97 | public GenerationConfig build() { 98 | return new GenerationConfig( 99 | stopSequences.isEmpty() ? null : stopSequences, 100 | responseMimeType, 101 | responseSchema, 102 | maxOutputTokens, 103 | temperature, 104 | topP, 105 | topK 106 | ); 107 | } 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /gemini-api/src/main/java/swiss/ameri/gemini/api/GenerativeModel.java: -------------------------------------------------------------------------------- 1 | package swiss.ameri.gemini.api; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | /** 7 | * Contains all the information needed for Gemini API to generate new content. 8 | * 9 | * @param modelName to be used. see {@link ModelVariant}. Must start with "models/" 10 | * @param contents given as input to Gemini API 11 | * @param safetySettings optional, to adjust safety settings 12 | * @param generationConfig optional, to configure the prompt 13 | * @param systemInstruction optional, system instruction 14 | * @param functionDeclarations optional, functions the model may call 15 | */ 16 | public record GenerativeModel( 17 | String modelName, 18 | List contents, 19 | List safetySettings, 20 | GenerationConfig generationConfig, 21 | List systemInstruction, 22 | List functionDeclarations 23 | ) { 24 | 25 | /** 26 | * Create a {@link GenerativeModelBuilder}. 27 | * 28 | * @return an empty {@link GenerativeModelBuilder} 29 | */ 30 | public static GenerativeModelBuilder builder() { 31 | return new GenerativeModelBuilder(); 32 | } 33 | 34 | /** 35 | * A builder for {@link GenerativeModel}. Currently, does not validate the fields when building the model. Not thread-safe. 36 | */ 37 | public static class GenerativeModelBuilder { 38 | private String modelName; 39 | private GenerationConfig generationConfig; 40 | private final List contents = new ArrayList<>(); 41 | private final List safetySettings = new ArrayList<>(); 42 | private final List systemInstructions = new ArrayList<>(); 43 | private final List functionDeclarations = new ArrayList<>(); 44 | 45 | private GenerativeModelBuilder() { 46 | } 47 | 48 | /** 49 | * Set the model name. 50 | * 51 | * @param modelName to be set 52 | * @return this 53 | */ 54 | public GenerativeModelBuilder modelName(String modelName) { 55 | this.modelName = modelName; 56 | return this; 57 | } 58 | 59 | /** 60 | * Set the model name. 61 | * 62 | * @param modelVariant to be set 63 | * @return this 64 | */ 65 | public GenerativeModelBuilder modelName(ModelVariant modelVariant) { 66 | return modelName(modelVariant == null ? null : modelVariant.variant()); 67 | } 68 | 69 | /** 70 | * Add content 71 | * 72 | * @param content to be added 73 | * @return this 74 | */ 75 | public GenerativeModelBuilder addContent(Content content) { 76 | this.contents.add(content); 77 | return this; 78 | } 79 | 80 | /** 81 | * Add system instruction 82 | * 83 | * @param systemInstruction to be added 84 | * @return this 85 | */ 86 | public GenerativeModelBuilder addSystemInstruction(String systemInstruction) { 87 | this.systemInstructions.add(systemInstruction); 88 | return this; 89 | } 90 | 91 | /** 92 | * Add safety setting 93 | * 94 | * @param safetySetting to be added 95 | * @return this 96 | */ 97 | public GenerativeModelBuilder addSafetySetting(SafetySetting safetySetting) { 98 | this.safetySettings.add(safetySetting); 99 | return this; 100 | } 101 | 102 | /** 103 | * Add function declarations 104 | * 105 | * @param functionDeclaration to be added 106 | * @return this 107 | */ 108 | public GenerativeModelBuilder addFunctionDeclaration(FunctionDeclaration functionDeclaration) { 109 | this.functionDeclarations.add(functionDeclaration); 110 | return this; 111 | } 112 | 113 | /** 114 | * Set the generation config 115 | * 116 | * @param generationConfig to be set 117 | * @return this 118 | */ 119 | public GenerativeModelBuilder generationConfig(GenerationConfig generationConfig) { 120 | this.generationConfig = generationConfig; 121 | return this; 122 | } 123 | 124 | /** 125 | * Build the model based on this builder. 126 | * 127 | * @return a completed (not necessarily validated) {@link GenerativeModel} 128 | */ 129 | public GenerativeModel build() { 130 | return new GenerativeModel( 131 | modelName, 132 | contents, 133 | safetySettings, 134 | generationConfig, 135 | systemInstructions, 136 | functionDeclarations 137 | ); 138 | } 139 | } 140 | 141 | } 142 | -------------------------------------------------------------------------------- /gemini-api/src/main/java/swiss/ameri/gemini/api/ModelVariant.java: -------------------------------------------------------------------------------- 1 | package swiss.ameri.gemini.api; 2 | 3 | /** 4 | * (Potentially non-exhaustive) list of supported models. 5 | * 6 | * @see Gemini Models 7 | */ 8 | public enum ModelVariant { 9 | /** 10 | * Next generation features, speed, and multimodal generation for a diverse variety of tasks. 11 | *

    12 | *
  • Input: Audio, images, videos, and text
  • 13 | *
  • Output: Text, images (coming soon), and audio (coming soon)
  • 14 | *
15 | */ 16 | GEMINI_2_0_FLASH_EXP("gemini-2.0-flash-exp"), 17 | /** 18 | * Complex reasoning tasks such as code and text generation, text editing, problem-solving, data extraction and generation. 19 | */ 20 | GEMINI_1_5_PRO("gemini-1.5-pro"), 21 | /** 22 | * Fast and versatile performance across a diverse variety of tasks. 23 | */ 24 | GEMINI_1_5_FLASH("gemini-1.5-flash"), 25 | /** 26 | * High volume and lower intelligence tasks. 27 | */ 28 | GEMINI_1_5_FLASH_8B("gemini-1.5-flash-8b"), 29 | /** 30 | * Natural language tasks, multi-turn text and code chat, and code generation. 31 | * 32 | * @deprecated on 2/15/2025 33 | */ 34 | @Deprecated 35 | GEMINI_1_0_PRO("gemini-1.0-pro"), 36 | /** 37 | * Measuring the relatedness of text strings. 38 | */ 39 | TEXT_EMBEDDING_004("text-embedding-004"), 40 | /** 41 | * Providing source-grounded answers to questions. 42 | */ 43 | AQA("aqa"), 44 | ; 45 | 46 | private final String variant; 47 | 48 | ModelVariant(String variant) { 49 | this.variant = variant; 50 | } 51 | 52 | /** 53 | * Model variant name. 54 | * 55 | * @return Model variant name as needed by Gemini API 56 | */ 57 | public String variant() { 58 | return "models/" + variant; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /gemini-api/src/main/java/swiss/ameri/gemini/api/SafetySetting.java: -------------------------------------------------------------------------------- 1 | package swiss.ameri.gemini.api; 2 | 3 | import java.util.Arrays; 4 | import java.util.List; 5 | 6 | import static swiss.ameri.gemini.api.SafetySetting.HarmCategoryType.*; 7 | 8 | /** 9 | * Safety settings according to SafetySetting. 10 | * 11 | * @param category the harm category, see {@link HarmCategory} 12 | * @param threshold the threshold, see {@link HarmBlockThreshold} 13 | */ 14 | public record SafetySetting( 15 | String category, 16 | String threshold 17 | ) { 18 | 19 | /** 20 | * Create a SafetySetting by using the provided enums. Use the constructor for custom string values that might 21 | * be missing in the enums. 22 | * 23 | * @param category the harm category, see {@link HarmCategory} 24 | * @param threshold the threshold, see {@link HarmBlockThreshold} 25 | * @return the new {@link SafetySetting} 26 | */ 27 | public static SafetySetting of( 28 | HarmCategory category, 29 | HarmBlockThreshold threshold 30 | ) { 31 | return new SafetySetting( 32 | category == null ? null : category.name(), 33 | threshold == null ? null : threshold.name() 34 | ); 35 | } 36 | 37 | public enum HarmCategoryType { 38 | GEMINI, 39 | PALM, 40 | UNKNOWN 41 | } 42 | 43 | /** 44 | * According to HarmCategory. 45 | * See {@link #harmCategoryType} for which can be used as input to a model. 46 | */ 47 | public enum HarmCategory { 48 | 49 | /** 50 | * Harasment content. 51 | */ 52 | HARM_CATEGORY_HARASSMENT(GEMINI), 53 | /** 54 | * Hate speech and content. 55 | */ 56 | HARM_CATEGORY_HATE_SPEECH(GEMINI), 57 | /** 58 | * Sexually explicit content. 59 | */ 60 | HARM_CATEGORY_SEXUALLY_EXPLICIT(GEMINI), 61 | /** 62 | * Dangerous content. 63 | */ 64 | HARM_CATEGORY_DANGEROUS_CONTENT(GEMINI), 65 | /** 66 | * Content that may be used to harm civic integrity. 67 | */ 68 | HARM_CATEGORY_CIVIC_INTEGRITY(GEMINI), 69 | /** 70 | * Category is unspecified. 71 | */ 72 | HARM_CATEGORY_UNSPECIFIED(UNKNOWN), 73 | /** 74 | * Negative or harmful comments targeting identity and/or protected attribute. 75 | */ 76 | HARM_CATEGORY_DEROGATORY(PALM), 77 | /** 78 | * Content that is rude, disrespectful, or profane. 79 | */ 80 | HARM_CATEGORY_TOXICITY(PALM), 81 | /** 82 | * Describes scenarios depicting violence against an individual or group, or general descriptions of gore. 83 | */ 84 | HARM_CATEGORY_VIOLENCE(PALM), 85 | /** 86 | * Contains references to sexual acts or other lewd content. 87 | */ 88 | HARM_CATEGORY_SEXUAL(PALM), 89 | /** 90 | * Promotes unchecked medical advice. 91 | */ 92 | HARM_CATEGORY_MEDICAL(PALM), 93 | /** 94 | * Dangerous content that promotes, facilitates, or encourages harmful acts. 95 | */ 96 | HARM_CATEGORY_DANGEROUS(PALM); 97 | private final HarmCategoryType harmCategoryType; 98 | 99 | HarmCategory(HarmCategoryType harmCategoryType) { 100 | this.harmCategoryType = harmCategoryType; 101 | } 102 | 103 | public HarmCategoryType harmCategoryType() { 104 | return harmCategoryType; 105 | } 106 | 107 | public static List harmCategoriesFor(HarmCategoryType type) { 108 | return Arrays.stream(values()) 109 | .filter(category -> category.harmCategoryType == type) 110 | .toList(); 111 | } 112 | } 113 | 114 | /** 115 | * According to SafetySetting 116 | */ 117 | public enum HarmBlockThreshold { 118 | /** 119 | * Threshold is unspecified. 120 | */ 121 | HARM_BLOCK_THRESHOLD_UNSPECIFIED, 122 | /** 123 | * Content with NEGLIGIBLE will be allowed. 124 | */ 125 | BLOCK_LOW_AND_ABOVE, 126 | /** 127 | * Content with NEGLIGIBLE and LOW will be allowed. 128 | */ 129 | BLOCK_MEDIUM_AND_ABOVE, 130 | /** 131 | * Content with NEGLIGIBLE, LOW, and MEDIUM will be allowed. 132 | */ 133 | BLOCK_ONLY_HIGH, 134 | /** 135 | * All content will be allowed. 136 | */ 137 | BLOCK_NONE 138 | } 139 | 140 | /** 141 | * The probability that a piece of content is harmful. 142 | * The classification system gives the probability of the content being unsafe. 143 | * This does not indicate the severity of harm for a piece of content. 144 | */ 145 | public enum HarmProbability { 146 | 147 | /** 148 | * Probability is unspecified. 149 | */ 150 | HARM_PROBABILITY_UNSPECIFIED, 151 | 152 | /** 153 | * Content has a negligible chance of being unsafe. 154 | */ 155 | NEGLIGIBLE, 156 | 157 | /** 158 | * Content has a low chance of being unsafe. 159 | */ 160 | LOW, 161 | 162 | /** 163 | * Content has a medium chance of being unsafe. 164 | */ 165 | MEDIUM, 166 | 167 | /** 168 | * Content has a high chance of being unsafe. 169 | */ 170 | HIGH 171 | } 172 | 173 | } 174 | -------------------------------------------------------------------------------- /gemini-api/src/main/java/swiss/ameri/gemini/api/Schema.java: -------------------------------------------------------------------------------- 1 | package swiss.ameri.gemini.api; 2 | 3 | 4 | import java.util.List; 5 | import java.util.Map; 6 | 7 | /** 8 | * The Schema object allows the definition of input and output data types. 9 | * These types can be objects, but also primitives and arrays. 10 | * Represents a select subset of an OpenAPI 3.0 schema object. 11 | * 12 | * @param type Required. Data type. 13 | * @param format Optional. The format of the data. This is used only for primitive datatypes. 14 | * Supported formats: 15 | * for NUMBER type: float, double 16 | * for INTEGER type: int32, int64 17 | * for STRING type: enum 18 | * @param description Optional. A brief description of the parameter. This could contain examples of use. 19 | * Parameter description may be formatted as Markdown. 20 | * @param nullable Optional. Indicates if the value may be null. 21 | * @param ameri_swiss_enum Optional. Note: the ameri_swiss prefix must be removed by the {@link swiss.ameri.gemini.spi.JsonParser}. 22 | * Possible values of the element of Type.STRING with enum format. 23 | * For example we can define an Enum Direction as : 24 | * {type:STRING, format:enum, enum:["EAST", NORTH", "SOUTH", "WEST"]} 25 | * @param maxItems Optional. Maximum number of the elements for Type.ARRAY. 26 | * @param minItems Optional. Minimum number of the elements for Type.ARRAY. 27 | * @param properties Optional. Properties of Type.OBJECT. 28 | * An object containing a list of "key": value pairs. Example: 29 | * { "name": "wrench", "mass": "1.3kg", "count": "3" }. 30 | * @param required Optional. Required properties of Type.OBJECT. 31 | * @param items Optional. Schema of the elements of Type.ARRAY. 32 | * @see Schema for further information. 33 | */ 34 | public record Schema( 35 | Type type, 36 | String format, 37 | String description, 38 | Boolean nullable, 39 | List ameri_swiss_enum, 40 | String maxItems, 41 | String minItems, 42 | Map properties, 43 | List required, 44 | Schema items 45 | ) { 46 | 47 | 48 | /** 49 | * Create a {@link SchemaBuilder}. 50 | * 51 | * @return an empty {@link SchemaBuilder} 52 | */ 53 | public static SchemaBuilder builder() { 54 | return new SchemaBuilder(); 55 | } 56 | 57 | /** 58 | * A builder for {@link Schema}. Currently, does not validate the fields when building the model. Not thread-safe. 59 | */ 60 | public static class SchemaBuilder { 61 | private Type type; 62 | private String format; 63 | private String description; 64 | private Boolean nullable; 65 | private List ameri_swiss_enum; 66 | private String maxItems; 67 | private String minItems; 68 | private Map properties; 69 | private List required; 70 | private Schema items; 71 | 72 | 73 | private SchemaBuilder() { 74 | } 75 | 76 | public Schema build() { 77 | return new Schema( 78 | this.type, 79 | this.format, 80 | this.description, 81 | this.nullable, 82 | this.ameri_swiss_enum, 83 | this.maxItems, 84 | this.minItems, 85 | this.properties, 86 | this.required, 87 | this.items 88 | ); 89 | } 90 | 91 | public SchemaBuilder type(Type type) { 92 | this.type = type; 93 | return this; 94 | } 95 | 96 | public SchemaBuilder format(String format) { 97 | this.format = format; 98 | return this; 99 | } 100 | 101 | public SchemaBuilder description(String description) { 102 | this.description = description; 103 | return this; 104 | } 105 | 106 | public SchemaBuilder nullable(Boolean nullable) { 107 | this.nullable = nullable; 108 | return this; 109 | } 110 | 111 | public SchemaBuilder ameri_swiss_enum(List ameri_swiss_enum) { 112 | this.ameri_swiss_enum = ameri_swiss_enum; 113 | return this; 114 | } 115 | 116 | public SchemaBuilder maxItems(String maxItems) { 117 | this.maxItems = maxItems; 118 | return this; 119 | } 120 | 121 | public SchemaBuilder minItems(String minItems) { 122 | this.minItems = minItems; 123 | return this; 124 | } 125 | 126 | public SchemaBuilder properties(Map properties) { 127 | this.properties = properties; 128 | return this; 129 | } 130 | 131 | public SchemaBuilder required(List required) { 132 | this.required = required; 133 | return this; 134 | } 135 | 136 | public SchemaBuilder items(Schema items) { 137 | this.items = items; 138 | return this; 139 | } 140 | } 141 | 142 | /** 143 | * Type contains the list of OpenAPI data types. 144 | * 145 | * @see Data types 146 | */ 147 | public enum Type { 148 | /** 149 | * Not specified, should not be used. 150 | */ 151 | TYPE_UNSPECIFIED, 152 | /** 153 | * String type. 154 | */ 155 | STRING, 156 | /** 157 | * Number type. 158 | */ 159 | NUMBER, 160 | /** 161 | * Integer type. 162 | */ 163 | INTEGER, 164 | /** 165 | * Boolean type. 166 | */ 167 | BOOLEAN, 168 | /** 169 | * Array type. 170 | */ 171 | ARRAY, 172 | /** 173 | * Object type. 174 | */ 175 | OBJECT 176 | } 177 | } 178 | 179 | -------------------------------------------------------------------------------- /gemini-api/src/main/java/swiss/ameri/gemini/api/TaskType.java: -------------------------------------------------------------------------------- 1 | package swiss.ameri.gemini.api; 2 | 3 | /** 4 | * Type of task for which the embedding will be used. 5 | */ 6 | public enum TaskType { 7 | /** 8 | * Unset value, which will default to one of the other enum values. 9 | */ 10 | TASK_TYPE_UNSPECIFIED, 11 | 12 | /** 13 | * Specifies the given text is a query in a search/retrieval setting. 14 | */ 15 | RETRIEVAL_QUERY, 16 | 17 | /** 18 | * Specifies the given text is a document from the corpus being searched. 19 | */ 20 | RETRIEVAL_DOCUMENT, 21 | 22 | /** 23 | * Specifies the given text will be used for Semantic Textual Similarity (STS). 24 | */ 25 | SEMANTIC_SIMILARITY, 26 | 27 | /** 28 | * Specifies that the given text will be classified. 29 | */ 30 | CLASSIFICATION, 31 | 32 | /** 33 | * Specifies that the embeddings will be used for clustering. 34 | */ 35 | CLUSTERING, 36 | 37 | /** 38 | * Specifies that the given text will be used for question answering. 39 | */ 40 | QUESTION_ANSWERING, 41 | 42 | /** 43 | * Specifies that the given text will be used for fact verification. 44 | */ 45 | FACT_VERIFICATION 46 | } 47 | -------------------------------------------------------------------------------- /gemini-api/src/main/java/swiss/ameri/gemini/spi/JsonParser.java: -------------------------------------------------------------------------------- 1 | package swiss.ameri.gemini.spi; 2 | 3 | /** 4 | * Used to (un-) marshal java objects (mainly {@code record}s) to JSON Strings. 5 | * To keep this library dependency free, no implementation is provided directly. 6 | * {@code swiss.ameri:gemini-gson} provides an example implementation using a gson dependency. 7 | */ 8 | public interface JsonParser { 9 | 10 | /** 11 | * This method serializes the specified object into its equivalent JSON representation. 12 | * 13 | * @param object to be serialized 14 | * @return the serialized object 15 | */ 16 | String toJson(Object object); 17 | 18 | /** 19 | * This method deserializes the specified JSON into an object of the specified class. 20 | * 21 | * @param json to be deserialized 22 | * @param clazz to be created from the json 23 | * @param type of the class 24 | * @return the deserialized object 25 | */ 26 | T fromJson(String json, Class clazz); 27 | 28 | } 29 | -------------------------------------------------------------------------------- /gemini-gson/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | swiss.ameri 9 | gemini 10 | 1beta.0.2.8-SNAPSHOT 11 | 12 | 13 | gemini-gson 14 | 15 | Provides an example GSON implementation for JSON operations. 16 | 17 | 18 | 19 | 17 20 | 21 | ${project.version} 22 | 2.11.0 23 | swiss.ameri.gemini.gson 24 | 25 | 26 | 27 | 28 | 29 | swiss.ameri 30 | gemini-api 31 | ${gemini.version} 32 | 33 | 34 | 35 | com.google.code.gson 36 | gson 37 | ${gson.version} 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /gemini-gson/src/main/java/swiss/ameri/gemini/gson/GsonJsonParser.java: -------------------------------------------------------------------------------- 1 | package swiss.ameri.gemini.gson; 2 | 3 | import com.google.gson.FieldNamingStrategy; 4 | import com.google.gson.Gson; 5 | import com.google.gson.GsonBuilder; 6 | import swiss.ameri.gemini.api.Schema; 7 | import swiss.ameri.gemini.spi.JsonParser; 8 | 9 | /** 10 | * Reference implementation of {@link JsonParser} using {@link Gson} dependency. 11 | */ 12 | public class GsonJsonParser implements JsonParser { 13 | 14 | /** 15 | * Field naming strategy to avoid usage of illegal field names in java. 16 | * See e.g. {@link Schema#ameri_swiss_enum()}, which cannot be named {@code enum}. 17 | */ 18 | public static final FieldNamingStrategy FIELD_NAMING_STRATEGY = field -> { 19 | if (field.getName().startsWith("ameri_swiss_")) { 20 | return field.getName().substring("ameri_swiss_".length()); 21 | } 22 | return field.getName(); 23 | }; 24 | 25 | private final Gson gson; 26 | 27 | /** 28 | * Create a {@link JsonParser} with a custom {@link Gson}. 29 | * 30 | * @param gson instance to use 31 | */ 32 | public GsonJsonParser(Gson gson) { 33 | this.gson = gson; 34 | } 35 | 36 | /** 37 | * Create a default {@link JsonParser} instance. 38 | */ 39 | public GsonJsonParser() { 40 | this(new GsonBuilder().setFieldNamingStrategy(FIELD_NAMING_STRATEGY).create()); 41 | } 42 | 43 | @Override 44 | public String toJson(Object object) { 45 | return gson.toJson(object); 46 | } 47 | 48 | @Override 49 | public T fromJson(String json, Class clazz) { 50 | return gson.fromJson(json, clazz); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /gemini-tester/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | swiss.ameri 9 | gemini 10 | 1beta.0.2.8-SNAPSHOT 11 | 12 | gemini-tester 13 | 14 | 15 | 17 16 | 17 | ${project.version} 18 | swiss.ameri.gemini.tester 19 | 20 | 21 | 22 | 23 | 24 | swiss.ameri 25 | gemini-api 26 | ${gemini.version} 27 | 28 | 29 | swiss.ameri 30 | gemini-gson 31 | ${gemini.version} 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /gemini-tester/src/main/java/swiss/ameri/gemini/tester/GeminiTester.java: -------------------------------------------------------------------------------- 1 | package swiss.ameri.gemini.tester; 2 | 3 | import swiss.ameri.gemini.api.Content; 4 | import swiss.ameri.gemini.api.FunctionCall; 5 | import swiss.ameri.gemini.api.FunctionDeclaration; 6 | import swiss.ameri.gemini.api.FunctionResponse; 7 | import swiss.ameri.gemini.api.GenAi; 8 | import swiss.ameri.gemini.api.GenerationConfig; 9 | import swiss.ameri.gemini.api.GenerativeModel; 10 | import swiss.ameri.gemini.api.ModelVariant; 11 | import swiss.ameri.gemini.api.SafetySetting; 12 | import swiss.ameri.gemini.api.Schema; 13 | import swiss.ameri.gemini.gson.GsonJsonParser; 14 | import swiss.ameri.gemini.spi.JsonParser; 15 | 16 | import java.io.IOException; 17 | import java.io.InputStream; 18 | import java.util.Base64; 19 | import java.util.List; 20 | import java.util.Map; 21 | import java.util.concurrent.ExecutionException; 22 | import java.util.concurrent.TimeUnit; 23 | import java.util.concurrent.TimeoutException; 24 | 25 | /** 26 | * Example program to test the {@link GenAi} functionality. 27 | */ 28 | public class GeminiTester { 29 | 30 | private GeminiTester() { 31 | throw new AssertionError("Not instantiable"); 32 | } 33 | 34 | /** 35 | * Entry point. takes the Gemini API key as argument. See aistuio.google.com to generate a new API key. 36 | * 37 | * @param args should receive the API key as argument 38 | * @throws Exception if something goes wrong 39 | */ 40 | public static void main(String[] args) throws Exception { 41 | JsonParser parser = new GsonJsonParser(); 42 | String apiKey = args[0]; 43 | 44 | try (var genAi = new GenAi(apiKey, parser)) { 45 | // each method represents an example usage 46 | listModels(genAi); 47 | getModel(genAi); 48 | countTokens(genAi); 49 | generateContent(genAi); 50 | generateContentStream(genAi); 51 | generateWithResponseSchema(genAi); 52 | generateContentStreamWithResponseSchema(genAi); 53 | multiChatTurn(genAi); 54 | textAndImage(genAi); 55 | embedContents(genAi); 56 | functionCall(genAi); 57 | functionResponse(genAi); 58 | } 59 | 60 | 61 | } 62 | 63 | private static void embedContents(GenAi genAi) { 64 | System.out.println("----- embed contents"); 65 | var model = GenerativeModel.builder() 66 | .modelName(ModelVariant.TEXT_EMBEDDING_004) 67 | .addContent(Content.textContent( 68 | Content.Role.USER, 69 | "Write a 50 word story about a magic backpack." 70 | )) 71 | .addContent(Content.textContent( 72 | Content.Role.MODEL, 73 | "bla bla bla bla" 74 | )) 75 | .addSafetySetting(SafetySetting.of( 76 | SafetySetting.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, 77 | SafetySetting.HarmBlockThreshold.BLOCK_ONLY_HIGH 78 | )) 79 | .generationConfig(new GenerationConfig( 80 | null, 81 | null, 82 | null, 83 | null, 84 | null, 85 | null, 86 | null 87 | )) 88 | .build(); 89 | 90 | List embeddings = genAi.embedContents(model, null, null, null).join(); 91 | System.out.println("Embedding count: " + embeddings.size()); 92 | System.out.println("Values per embedding: " + embeddings.stream().map(GenAi.ContentEmbedding::values).map(List::size).toList()); 93 | 94 | } 95 | 96 | private static void countTokens(GenAi genAi) { 97 | System.out.println("----- count tokens"); 98 | var model = createStoryModel(); 99 | Long result = genAi.countTokens(model) 100 | .join(); 101 | System.out.println("Tokens: " + result); 102 | } 103 | 104 | private static void multiChatTurn(GenAi genAi) { 105 | System.out.println("----- multi turn chat"); 106 | GenerativeModel chatModel = GenerativeModel.builder() 107 | .modelName(ModelVariant.GEMINI_1_5_PRO) 108 | .addContent(new Content.TextContent( 109 | Content.Role.USER.roleName(), 110 | "Write the first line of a story about a magic backpack." 111 | )) 112 | .addContent(new Content.TextContent( 113 | Content.Role.MODEL.roleName(), 114 | "In the bustling city of Meadow brook, lived a young girl named Sophie. She was a bright and curious soul with an imaginative mind." 115 | )) 116 | .addContent(new Content.TextContent( 117 | Content.Role.USER.roleName(), 118 | "Can you set it in a quiet village in 1600s France? Max 30 words" 119 | )) 120 | .build(); 121 | genAi.generateContentStream(chatModel) 122 | .forEach(System.out::println); 123 | } 124 | 125 | private static void functionCall(GenAi genAi) throws ExecutionException, InterruptedException, TimeoutException { 126 | System.out.println("----- Function call"); 127 | GenerativeModel chatModel = GenerativeModel.builder() 128 | .modelName(ModelVariant.GEMINI_1_5_PRO) 129 | .addContent(new Content.TextContent( 130 | Content.Role.USER.roleName(), 131 | "What is the current weather in Zurich?" 132 | )) 133 | .addFunctionDeclaration(new FunctionDeclaration( 134 | "getCurrentWeather", 135 | "Get the current weather for a city.", 136 | Schema.builder() 137 | .type(Schema.Type.OBJECT) 138 | .properties(Map.of("city", Schema.builder() 139 | .type(Schema.Type.STRING) 140 | .build())) 141 | .build() 142 | )) 143 | .build(); 144 | genAi.generateContent(chatModel) 145 | .thenAccept(generatedContent -> { 146 | System.out.println(generatedContent); 147 | if (generatedContent.functionCall() == null) { 148 | throw new RuntimeException("Expected a function call..."); 149 | } 150 | }) 151 | .get(20, TimeUnit.SECONDS); 152 | } 153 | 154 | private static void functionResponse(GenAi genAi) throws ExecutionException, InterruptedException, TimeoutException { 155 | System.out.println("----- Function response"); 156 | GenerativeModel chatModel = GenerativeModel.builder() 157 | .modelName(ModelVariant.GEMINI_1_5_PRO) 158 | .addContent(new Content.TextContent( 159 | Content.Role.USER.roleName(), 160 | "What is the current weather in Zurich?" 161 | )) 162 | .addContent(Content.functionCallContent( 163 | Content.Role.MODEL, 164 | new FunctionCall( 165 | "getCurrentWeather", 166 | null 167 | ) 168 | )) 169 | .addContent(Content.functionResponseContent( 170 | Content.Role.USER, 171 | new FunctionResponse( 172 | "getCurrentWeather", 173 | Map.of("temperatureCelsius", "13") 174 | ) 175 | )) 176 | .addFunctionDeclaration(new FunctionDeclaration( 177 | "getCurrentWeather", 178 | "Get the current weather for a city.", 179 | Schema.builder() 180 | .type(Schema.Type.OBJECT) 181 | .properties(Map.of("city", Schema.builder() 182 | .type(Schema.Type.STRING) 183 | .build())) 184 | .build() 185 | )) 186 | .build(); 187 | genAi.generateContent(chatModel) 188 | .thenAccept(generatedContent -> { 189 | System.out.println(generatedContent); 190 | if (generatedContent.text() == null) { 191 | throw new RuntimeException("Expected a text..."); 192 | } 193 | }) 194 | .get(20, TimeUnit.SECONDS); 195 | } 196 | 197 | private static void generateContentStream(GenAi genAi) { 198 | System.out.println("----- Generate content (streaming) -- with usage meta data"); 199 | var model = createStoryModel(); 200 | genAi.generateContentStream(model) 201 | .forEach(x -> { 202 | System.out.println(x); 203 | // note that the usage metadata is updated as it arrives 204 | System.out.println(genAi.usageMetadata(x.id())); 205 | System.out.println(genAi.safetyRatings(x.id())); 206 | }); 207 | } 208 | 209 | private static void generateContent(GenAi genAi) throws InterruptedException, ExecutionException, TimeoutException { 210 | var model = createStoryModel(); 211 | System.out.println("----- Generate content (blocking)"); 212 | genAi.generateContent(model) 213 | .thenAccept(gcr -> { 214 | System.out.println(gcr); 215 | System.out.println("----- Generate content (blocking) usage meta data & safety ratings"); 216 | System.out.println(genAi.usageMetadata(gcr.id())); 217 | System.out.println(genAi.safetyRatings(gcr.id()).stream().map(GenAi.SafetyRating::toTypedSafetyRating).toList()); 218 | }) 219 | .get(20, TimeUnit.SECONDS); 220 | } 221 | 222 | 223 | private static void generateContentStreamWithResponseSchema(GenAi genAi) { 224 | System.out.println("----- Generate content (streaming) with response schema -- with usage meta data"); 225 | var model = createResponseSchemaModel(); 226 | genAi.generateContentStream(model) 227 | .forEach(x -> { 228 | System.out.println(x); 229 | // note that the usage metadata is updated as it arrives 230 | System.out.println(genAi.usageMetadata(x.id())); 231 | System.out.println(genAi.safetyRatings(x.id())); 232 | }); 233 | } 234 | 235 | private static void generateWithResponseSchema(GenAi genAi) throws InterruptedException, ExecutionException, TimeoutException { 236 | var model = createResponseSchemaModel(); 237 | System.out.println("----- Generate with response schema (blocking)"); 238 | genAi.generateContent(model) 239 | .thenAccept(gcr -> { 240 | System.out.println(gcr); 241 | System.out.println("----- Generate with response schema (blocking) usage meta data & safety ratings"); 242 | System.out.println(genAi.usageMetadata(gcr.id())); 243 | System.out.println(genAi.safetyRatings(gcr.id()).stream().map(GenAi.SafetyRating::toTypedSafetyRating).toList()); 244 | }) 245 | .get(20, TimeUnit.SECONDS); 246 | } 247 | 248 | private static GenerativeModel createResponseSchemaModel() { 249 | return GenerativeModel.builder() 250 | .modelName(ModelVariant.GEMINI_1_5_FLASH) 251 | .addContent(Content.textContent( 252 | Content.Role.USER, 253 | "List 3 popular cookie recipes." 254 | )) 255 | .addSafetySetting(SafetySetting.of( 256 | SafetySetting.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, 257 | SafetySetting.HarmBlockThreshold.BLOCK_ONLY_HIGH 258 | )) 259 | .generationConfig(new GenerationConfig( 260 | null, 261 | "application/json", 262 | Schema.builder() 263 | .type(Schema.Type.ARRAY) 264 | .items(Schema.builder() 265 | .type(Schema.Type.OBJECT) 266 | .properties(Map.of( 267 | "recipe_name", Schema.builder() 268 | .type(Schema.Type.STRING) 269 | .build() 270 | )) 271 | .build()) 272 | .build(), 273 | null, 274 | null, 275 | null, 276 | null 277 | )) 278 | .build(); 279 | } 280 | 281 | private static GenerativeModel createStoryModel() { 282 | return GenerativeModel.builder() 283 | .modelName(ModelVariant.GEMINI_2_0_FLASH_EXP) 284 | .addContent(Content.textContent( 285 | Content.Role.USER, 286 | "Write a 50 word story about a magic backpack." 287 | )) 288 | .addSafetySetting(SafetySetting.of( 289 | SafetySetting.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, 290 | SafetySetting.HarmBlockThreshold.BLOCK_ONLY_HIGH 291 | )) 292 | .generationConfig(new GenerationConfig( 293 | null, 294 | null, 295 | null, 296 | null, 297 | null, 298 | null, 299 | null 300 | )) 301 | .build(); 302 | } 303 | 304 | private static void getModel(GenAi genAi) { 305 | System.out.println("----- Get Model"); 306 | System.out.println( 307 | genAi.getModel(ModelVariant.GEMINI_1_5_PRO) 308 | ); 309 | } 310 | 311 | private static void listModels(GenAi genAi) { 312 | System.out.println("----- List models"); 313 | genAi.listModels() 314 | .forEach(System.out::println); 315 | } 316 | 317 | private static void textAndImage(GenAi genAi) throws IOException { 318 | System.out.println("----- text and image"); 319 | var model = GenerativeModel.builder() 320 | .modelName(ModelVariant.GEMINI_1_5_FLASH) 321 | .addContent( 322 | Content.textAndMediaContentBuilder() 323 | .role(Content.Role.USER) 324 | .text("What is in this image?") 325 | .addMedia(new Content.MediaData( 326 | "image/png", 327 | loadSconesImage() 328 | )) 329 | .build() 330 | ).build(); 331 | genAi.generateContent(model) 332 | .thenAccept(System.out::println) 333 | .join(); 334 | } 335 | 336 | private static String loadSconesImage() throws IOException { 337 | try (InputStream is = GeminiTester.class.getClassLoader().getResourceAsStream("scones.png")) { 338 | if (is == null) { 339 | throw new IllegalStateException("Image not found! "); 340 | } 341 | return Base64.getEncoder().encodeToString(is.readAllBytes()); 342 | } 343 | } 344 | } 345 | -------------------------------------------------------------------------------- /gemini-tester/src/main/resources/scones.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michael-ameri/gemini-api/261f87c56e1a3b02a06611bdac5dedaeee759812/gemini-tester/src/main/resources/scones.png -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | swiss.ameri 8 | gemini 9 | 1beta.0.2.8-SNAPSHOT 10 | pom 11 | 12 | 13 | 14 | The Apache License, Version 2.0 15 | https://www.apache.org/licenses/LICENSE-2.0.txt 16 | 17 | 18 | 19 | 20 | 21 | Michael Ameri 22 | michael.ameri+gemini@gmail.com 23 | 24 | 25 | 26 | 27 | scm:git:git://github.com/michael-ameri/gemini-api.git 28 | scm:git:ssh://github.com:michael-ameri/gemini-api.git 29 | https://github.com/michael-ameri/gemini-api/tree/master 30 | 31 | 32 | https://github.com/michael-ameri/gemini-api 33 | 34 | Java library to access the gemini API. 35 | 36 | 37 | 38 | gemini-api 39 | gemini-gson 40 | gemini-tester 41 | 42 | 43 | 44 | 17 45 | UTF-8 46 | 47 | 0.4.0 48 | 3.2.4 49 | 3.3.1 50 | 3.4.2 51 | 3.7.0 52 | 53 | swiss.ameri.gemini 54 | 55 | 56 | 57 | 58 | 59 | org.apache.maven.plugins 60 | maven-jar-plugin 61 | ${maven-jar-plugin.version} 62 | 63 | 64 | 65 | ${module-name} 66 | 67 | 68 | 69 | 70 | 71 | org.apache.maven.plugins 72 | maven-source-plugin 73 | ${maven-source-plugin.version} 74 | 75 | 76 | attach-sources 77 | deploy 78 | 79 | jar-no-fork 80 | 81 | 82 | 83 | 84 | 85 | org.apache.maven.plugins 86 | maven-javadoc-plugin 87 | ${maven-javadoc-plugin.version} 88 | 89 | 90 | attach-javadocs 91 | deploy 92 | 93 | jar 94 | 95 | 96 | 97 | 98 | apiNote 99 | a 100 | API Note 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | org.apache.maven.plugins 109 | maven-gpg-plugin 110 | ${maven-gpg-plugin.version} 111 | 112 | 113 | sign-artifacts 114 | deploy 115 | 116 | sign 117 | 118 | 119 | 120 | 121 | 122 | 123 | org.sonatype.central 124 | central-publishing-maven-plugin 125 | ${central-publishing-maven-plugin.version} 126 | true 127 | 128 | central 129 | true 130 | 131 | 132 | 133 | 134 | 135 | --------------------------------------------------------------------------------