refactor: rename generateAsync method to generate and update image handling in OllamaAPI

- Renamed `generateAsync` to `generate` for clarity.
- Consolidated image handling in `generateWithImages` to accept multiple image types (File, byte[], String).
- Updated request format handling in `OllamaCommonRequest` and related classes to use a more flexible format property.
- Adjusted integration and unit tests to reflect changes in method signatures and functionality.
This commit is contained in:
Amith Koujalgi 2025-09-15 23:35:53 +05:30
parent 51501cf5e1
commit 44c6236243
9 changed files with 102 additions and 188 deletions

View File

@ -1091,7 +1091,7 @@ public class OllamaAPI {
* @return an {@link OllamaAsyncResultStreamer} handle for polling and * @return an {@link OllamaAsyncResultStreamer} handle for polling and
* retrieving streamed results * retrieving streamed results
*/ */
public OllamaAsyncResultStreamer generateAsync(String model, String prompt, boolean raw, boolean think) { public OllamaAsyncResultStreamer generate(String model, String prompt, boolean raw, boolean think) {
OllamaGenerateRequest ollamaRequestModel = new OllamaGenerateRequest(model, prompt); OllamaGenerateRequest ollamaRequestModel = new OllamaGenerateRequest(model, prompt);
ollamaRequestModel.setRaw(raw); ollamaRequestModel.setRaw(raw);
ollamaRequestModel.setThink(think); ollamaRequestModel.setThink(think);
@ -1103,147 +1103,57 @@ public class OllamaAPI {
} }
/** /**
* With one or more image files, ask a question to a model running on Ollama * Generates a response from a model running on the Ollama server using one or more images as input.
* server. This is a
* sync/blocking call.
*
* @param model the ollama model to ask the question to
* @param prompt the prompt/question text
* @param imageFiles the list of image files to use for the question
* @param options the Options object - <a
* href=
* "https://github.com/jmorganca/ollama/blob/main/docs/modelfile.md#valid-parameters-and-values">More
* details on the options</a>
* @param streamHandler optional callback consumer that will be applied every
* time a streamed response is received. If not set, the
* stream parameter of the request is set to false.
* @return OllamaResult that includes response text and time taken for response
* @throws OllamaBaseException if the response indicates an error status
* @throws IOException if an I/O error occurs during the HTTP request
* @throws InterruptedException if the operation is interrupted
*/
public OllamaResult generateWithImageFiles(String model, String prompt, List<File> imageFiles, Options options,
OllamaStreamHandler streamHandler) throws OllamaBaseException, IOException, InterruptedException {
List<String> images = new ArrayList<>();
for (File imageFile : imageFiles) {
images.add(encodeFileToBase64(imageFile));
}
OllamaGenerateRequest ollamaRequestModel = new OllamaGenerateRequest(model, prompt, images);
ollamaRequestModel.setOptions(options.getOptionsMap());
return generateSyncForOllamaRequestModel(ollamaRequestModel, null, streamHandler);
}
/**
* Convenience method to call Ollama API without streaming responses.
* <p> * <p>
* Uses * This method allows you to provide images (as {@link File}, {@code byte[]}, or image URL {@link String})
* {@link #generateWithImageFiles(String, String, List, Options, OllamaStreamHandler)} * along with a prompt to the specified model. The images are automatically encoded as base64 before being sent.
* Additional model options can be specified via the {@link Options} parameter.
* </p>
* *
* @throws OllamaBaseException if the response indicates an error status
* @throws IOException if an I/O error occurs during the HTTP request
* @throws InterruptedException if the operation is interrupted
*/
public OllamaResult generateWithImageFiles(String model, String prompt, List<File> imageFiles, Options options)
throws OllamaBaseException, IOException, InterruptedException {
return generateWithImageFiles(model, prompt, imageFiles, options, null);
}
/**
* With one or more image URLs, ask a question to a model running on Ollama
* server. This is a
* sync/blocking call.
*
* @param model the ollama model to ask the question to
* @param prompt the prompt/question text
* @param imageURLs the list of image URLs to use for the question
* @param options the Options object - <a
* href=
* "https://github.com/jmorganca/ollama/blob/main/docs/modelfile.md#valid-parameters-and-values">More
* details on the options</a>
* @param streamHandler optional callback consumer that will be applied every
* time a streamed response is received. If not set, the
* stream parameter of the request is set to false.
* @return OllamaResult that includes response text and time taken for response
* @throws OllamaBaseException if the response indicates an error status
* @throws IOException if an I/O error occurs during the HTTP request
* @throws InterruptedException if the operation is interrupted
* @throws URISyntaxException if the URI for the request is malformed
*/
public OllamaResult generateWithImageURLs(String model, String prompt, List<String> imageURLs, Options options,
OllamaStreamHandler streamHandler)
throws OllamaBaseException, IOException, InterruptedException, URISyntaxException {
List<String> images = new ArrayList<>();
for (String imageURL : imageURLs) {
images.add(encodeByteArrayToBase64(Utils.loadImageBytesFromUrl(imageURL)));
}
OllamaGenerateRequest ollamaRequestModel = new OllamaGenerateRequest(model, prompt, images);
ollamaRequestModel.setOptions(options.getOptionsMap());
return generateSyncForOllamaRequestModel(ollamaRequestModel, null, streamHandler);
}
/**
* Convenience method to call Ollama API without streaming responses.
* <p> * <p>
* Uses * If a {@code streamHandler} is provided, the response will be streamed and the handler will be called
* {@link #generateWithImageURLs(String, String, List, Options, OllamaStreamHandler)} * for each streamed response chunk. If {@code streamHandler} is {@code null}, streaming is disabled and
* the full response is returned synchronously.
* </p>
* *
* @throws OllamaBaseException if the response indicates an error status * @param model the name of the Ollama model to use for generating the response
* @throws IOException if an I/O error occurs during the HTTP request
* @throws InterruptedException if the operation is interrupted
* @throws URISyntaxException if the URI for the request is malformed
*/
public OllamaResult generateWithImageURLs(String model, String prompt, List<String> imageURLs, Options options)
throws OllamaBaseException, IOException, InterruptedException, URISyntaxException {
return generateWithImageURLs(model, prompt, imageURLs, options, null);
}
/**
* Synchronously generates a response using a list of image byte arrays.
* <p>
* This method encodes the provided byte arrays into Base64 and sends them to
* the Ollama server.
*
* @param model the Ollama model to use for generating the response
* @param prompt the prompt or question text to send to the model * @param prompt the prompt or question text to send to the model
* @param images the list of image data as byte arrays * @param images a list of images to use for the question; each element must be a {@link File}, {@code byte[]}, or a URL {@link String}
* @param options the Options object - <a href= * @param options the {@link Options} object containing model parameters;
* "https://github.com/jmorganca/ollama/blob/main/docs/modelfile.md#valid-parameters-and-values">More * see <a href="https://github.com/jmorganca/ollama/blob/main/docs/modelfile.md#valid-parameters-and-values">Ollama model options documentation</a>
* details on the options</a> * @param streamHandler an optional callback that is invoked for each streamed response chunk;
* @param streamHandler optional callback that will be invoked with each * if {@code null}, disables streaming and returns the full response synchronously
* streamed response; if null, streaming is disabled * @return an {@link OllamaResult} containing the response text and time taken for the response
* @return OllamaResult containing the response text and the time taken for the * @throws OllamaBaseException if the response indicates an error status or an invalid image type is provided
* response
* @throws OllamaBaseException if the response indicates an error status
* @throws IOException if an I/O error occurs during the HTTP request * @throws IOException if an I/O error occurs during the HTTP request
* @throws InterruptedException if the operation is interrupted * @throws InterruptedException if the operation is interrupted
* @throws URISyntaxException if an image URL is malformed
*/ */
public OllamaResult generateWithImages(String model, String prompt, List<byte[]> images, Options options, public OllamaResult generateWithImages(String model, String prompt, List<Object> images, Options options, Map<String, Object> format,
OllamaStreamHandler streamHandler) throws OllamaBaseException, IOException, InterruptedException { OllamaStreamHandler streamHandler) throws OllamaBaseException, IOException, InterruptedException, URISyntaxException {
List<String> encodedImages = new ArrayList<>(); List<String> encodedImages = new ArrayList<>();
for (byte[] image : images) { for (Object image : images) {
encodedImages.add(encodeByteArrayToBase64(image)); if (image instanceof File) {
LOG.debug("Using image file: {}", ((File) image).getAbsolutePath());
encodedImages.add(encodeFileToBase64((File) image));
} else if (image instanceof byte[]) {
LOG.debug("Using image bytes: {}", ((byte[]) image).length + " bytes");
encodedImages.add(encodeByteArrayToBase64((byte[]) image));
} else if (image instanceof String) {
LOG.debug("Using image URL: {}", image);
encodedImages.add(encodeByteArrayToBase64(Utils.loadImageBytesFromUrl((String) image)));
} else {
throw new OllamaBaseException("Unsupported image type. Please provide a File, byte[], or a URL String.");
}
} }
OllamaGenerateRequest ollamaRequestModel = new OllamaGenerateRequest(model, prompt, encodedImages); OllamaGenerateRequest ollamaRequestModel = new OllamaGenerateRequest(model, prompt, encodedImages);
if (format != null) {
ollamaRequestModel.setFormat(format);
}
ollamaRequestModel.setOptions(options.getOptionsMap()); ollamaRequestModel.setOptions(options.getOptionsMap());
return generateSyncForOllamaRequestModel(ollamaRequestModel, null, streamHandler); return generateSyncForOllamaRequestModel(ollamaRequestModel, null, streamHandler);
} }
/**
* Convenience method to call the Ollama API using image byte arrays without
* streaming responses.
* <p>
* Uses
* {@link #generateWithImages(String, String, List, Options, OllamaStreamHandler)}
*
* @throws OllamaBaseException if the response indicates an error status
* @throws IOException if an I/O error occurs during the HTTP request
* @throws InterruptedException if the operation is interrupted
*/
public OllamaResult generateWithImages(String model, String prompt, List<byte[]> images, Options options)
throws OllamaBaseException, IOException, InterruptedException {
return generateWithImages(model, prompt, images, options, null);
}
/** /**
* Ask a question to a model based on a given message stack (i.e. a chat * Ask a question to a model based on a given message stack (i.e. a chat
* history). Creates a synchronous call to the api * history). Creates a synchronous call to the api

View File

@ -95,7 +95,7 @@ public class OllamaChatRequestBuilder {
} }
public OllamaChatRequestBuilder withGetJsonResponse() { public OllamaChatRequestBuilder withGetJsonResponse() {
this.request.setReturnFormatJson(true); this.request.setFormat("json");
return this; return this;
} }

View File

@ -7,6 +7,7 @@ import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import java.util.List; import java.util.List;
import java.util.Map;
@Getter @Getter
@Setter @Setter
@ -14,7 +15,6 @@ public class OllamaGenerateRequest extends OllamaCommonRequest implements Ollama
private String prompt; private String prompt;
private List<String> images; private List<String> images;
private String system; private String system;
private String context; private String context;
private boolean raw; private boolean raw;
@ -39,8 +39,6 @@ public class OllamaGenerateRequest extends OllamaCommonRequest implements Ollama
if (!(o instanceof OllamaGenerateRequest)) { if (!(o instanceof OllamaGenerateRequest)) {
return false; return false;
} }
return this.toString().equals(o.toString()); return this.toString().equals(o.toString());
} }
} }

View File

@ -28,7 +28,7 @@ public class OllamaGenerateRequestBuilder {
} }
public OllamaGenerateRequestBuilder withGetJsonResponse(){ public OllamaGenerateRequestBuilder withGetJsonResponse(){
this.request.setReturnFormatJson(true); this.request.setFormat("json");
return this; return this;
} }

View File

@ -15,9 +15,10 @@ import java.util.Map;
public abstract class OllamaCommonRequest { public abstract class OllamaCommonRequest {
protected String model; protected String model;
@JsonSerialize(using = BooleanToJsonFormatFlagSerializer.class) // @JsonSerialize(using = BooleanToJsonFormatFlagSerializer.class)
@JsonProperty(value = "format") // this can either be set to format=json or format={"key1": "val1", "key2": "val2"}
protected Boolean returnFormatJson; @JsonProperty(value = "format", required = false, defaultValue = "json")
protected Object format;
protected Map<String, Object> options; protected Map<String, Object> options;
protected String template; protected String template;
protected boolean stream; protected boolean stream;

View File

@ -17,5 +17,4 @@ public class BooleanToJsonFormatFlagSerializer extends JsonSerializer<Boolean>{
public boolean isEmpty(SerializerProvider provider, Boolean value) { public boolean isEmpty(SerializerProvider provider, Boolean value) {
return !value; return !value;
} }
} }

View File

@ -602,9 +602,9 @@ class OllamaAPIIntegrationTest {
throws OllamaBaseException, IOException, URISyntaxException, InterruptedException { throws OllamaBaseException, IOException, URISyntaxException, InterruptedException {
api.pullModel(VISION_MODEL); api.pullModel(VISION_MODEL);
OllamaResult result = api.generateWithImageURLs(VISION_MODEL, "What is in this image?", OllamaResult result = api.generateWithImages(VISION_MODEL, "What is in this image?",
List.of("https://i.pinimg.com/736x/f9/4e/cb/f94ecba040696a3a20b484d2e15159ec.jpg"), List.of("https://i.pinimg.com/736x/f9/4e/cb/f94ecba040696a3a20b484d2e15159ec.jpg"),
new OptionsBuilder().build()); new OptionsBuilder().build(), null, null);
assertNotNull(result); assertNotNull(result);
assertNotNull(result.getResponse()); assertNotNull(result.getResponse());
assertFalse(result.getResponse().isEmpty()); assertFalse(result.getResponse().isEmpty());
@ -617,8 +617,8 @@ class OllamaAPIIntegrationTest {
api.pullModel(VISION_MODEL); api.pullModel(VISION_MODEL);
File imageFile = getImageFileFromClasspath("roses.jpg"); File imageFile = getImageFileFromClasspath("roses.jpg");
try { try {
OllamaResult result = api.generateWithImageFiles(VISION_MODEL, "What is in this image?", OllamaResult result = api.generateWithImages(VISION_MODEL, "What is in this image?",
List.of(imageFile), new OptionsBuilder().build()); List.of(imageFile), new OptionsBuilder().build(), null, null);
assertNotNull(result); assertNotNull(result);
assertNotNull(result.getResponse()); assertNotNull(result.getResponse());
assertFalse(result.getResponse().isEmpty()); assertFalse(result.getResponse().isEmpty());
@ -637,11 +637,17 @@ class OllamaAPIIntegrationTest {
StringBuffer sb = new StringBuffer(); StringBuffer sb = new StringBuffer();
OllamaResult result = api.generateWithImageFiles(VISION_MODEL, "What is in this image?", OllamaResult result = api.generateWithImages(
List.of(imageFile), new OptionsBuilder().build(), (s) -> { VISION_MODEL,
"What is in this image?",
List.of(imageFile),
new OptionsBuilder().build(),
null,
(s) -> {
LOG.info(s); LOG.info(s);
sb.append(s); sb.append(s);
}); }
);
assertNotNull(result); assertNotNull(result);
assertNotNull(result.getResponse()); assertNotNull(result.getResponse());
assertFalse(result.getResponse().isEmpty()); assertFalse(result.getResponse().isEmpty());

View File

@ -154,15 +154,15 @@ class TestMockedAPIs {
String model = OllamaModelType.LLAMA2; String model = OllamaModelType.LLAMA2;
String prompt = "some prompt text"; String prompt = "some prompt text";
try { try {
when(ollamaAPI.generateWithImageFiles( when(ollamaAPI.generateWithImages(
model, prompt, Collections.emptyList(), new OptionsBuilder().build())) model, prompt, Collections.emptyList(), new OptionsBuilder().build(), null, null))
.thenReturn(new OllamaResult("", "", 0, 200)); .thenReturn(new OllamaResult("", "", 0, 200));
ollamaAPI.generateWithImageFiles( ollamaAPI.generateWithImages(
model, prompt, Collections.emptyList(), new OptionsBuilder().build()); model, prompt, Collections.emptyList(), new OptionsBuilder().build(), null, null);
verify(ollamaAPI, times(1)) verify(ollamaAPI, times(1))
.generateWithImageFiles( .generateWithImages(
model, prompt, Collections.emptyList(), new OptionsBuilder().build()); model, prompt, Collections.emptyList(), new OptionsBuilder().build(), null, null);
} catch (IOException | OllamaBaseException | InterruptedException e) { } catch (Exception e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
@ -173,14 +173,14 @@ class TestMockedAPIs {
String model = OllamaModelType.LLAMA2; String model = OllamaModelType.LLAMA2;
String prompt = "some prompt text"; String prompt = "some prompt text";
try { try {
when(ollamaAPI.generateWithImageURLs( when(ollamaAPI.generateWithImages(
model, prompt, Collections.emptyList(), new OptionsBuilder().build())) model, prompt, Collections.emptyList(), new OptionsBuilder().build(), null, null))
.thenReturn(new OllamaResult("", "", 0, 200)); .thenReturn(new OllamaResult("", "", 0, 200));
ollamaAPI.generateWithImageURLs( ollamaAPI.generateWithImages(
model, prompt, Collections.emptyList(), new OptionsBuilder().build()); model, prompt, Collections.emptyList(), new OptionsBuilder().build(), null, null);
verify(ollamaAPI, times(1)) verify(ollamaAPI, times(1))
.generateWithImageURLs( .generateWithImages(
model, prompt, Collections.emptyList(), new OptionsBuilder().build()); model, prompt, Collections.emptyList(), new OptionsBuilder().build(), null, null);
} catch (IOException | OllamaBaseException | InterruptedException | URISyntaxException e) { } catch (IOException | OllamaBaseException | InterruptedException | URISyntaxException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
@ -191,10 +191,10 @@ class TestMockedAPIs {
OllamaAPI ollamaAPI = Mockito.mock(OllamaAPI.class); OllamaAPI ollamaAPI = Mockito.mock(OllamaAPI.class);
String model = OllamaModelType.LLAMA2; String model = OllamaModelType.LLAMA2;
String prompt = "some prompt text"; String prompt = "some prompt text";
when(ollamaAPI.generateAsync(model, prompt, false, false)) when(ollamaAPI.generate(model, prompt, false, false))
.thenReturn(new OllamaAsyncResultStreamer(null, null, 3)); .thenReturn(new OllamaAsyncResultStreamer(null, null, 3));
ollamaAPI.generateAsync(model, prompt, false, false); ollamaAPI.generate(model, prompt, false, false);
verify(ollamaAPI, times(1)).generateAsync(model, prompt, false, false); verify(ollamaAPI, times(1)).generate(model, prompt, false, false);
} }
@Test @Test

View File

@ -44,11 +44,11 @@ public class TestGenerateRequestSerialization extends AbstractSerializationTest<
builder.withPrompt("Some prompt").withGetJsonResponse().build(); builder.withPrompt("Some prompt").withGetJsonResponse().build();
String jsonRequest = serialize(req); String jsonRequest = serialize(req);
System.out.printf(jsonRequest);
// no jackson deserialization as format property is not boolean ==> omit as deserialization // no jackson deserialization as format property is not boolean ==> omit as deserialization
// of request is never used in real code anyways // of request is never used in real code anyways
JSONObject jsonObject = new JSONObject(jsonRequest); JSONObject jsonObject = new JSONObject(jsonRequest);
String requestFormatProperty = jsonObject.getString("format"); String requestFormatProperty = jsonObject.getString("format");
assertEquals("json", requestFormatProperty); assertEquals("json", requestFormatProperty);
} }
} }