Support for structured output

Added support for structured output
This commit is contained in:
amithkoujalgi 2025-03-24 00:25:20 +05:30
parent e62a7511db
commit b9b18271a1
No known key found for this signature in database
GPG Key ID: E29A37746AF94B70
7 changed files with 1304 additions and 715 deletions

View File

@ -7,13 +7,19 @@ dev:
pre-commit install --install-hooks pre-commit install --install-hooks
build: build:
mvn -B clean install -Dgpg.skip=true
full-build:
mvn -B clean install mvn -B clean install
unit-tests: unit-tests:
mvn clean test -Punit-tests mvn clean test -Punit-tests
integration-tests: integration-tests:
mvn clean verify -Pintegration-tests export USE_EXTERNAL_OLLAMA_HOST=false && mvn clean verify -Pintegration-tests
integration-tests-local:
export USE_EXTERNAL_OLLAMA_HOST=true && export OLLAMA_HOST=http://localhost:11434 && mvn clean verify -Pintegration-tests -Dgpg.skip=true
doxygen: doxygen:
doxygen Doxyfile doxygen Doxyfile

View File

@ -209,6 +209,9 @@ pip install pre-commit
#### Setup dev environment #### Setup dev environment
> **Note**
> If you're on Windows, install [Chocolatey Package Manager for Windows](https://chocolatey.org/install) and then install `make` by running `choco install make`. Just a little tip - run the command with administrator privileges if installation faiils.
```shell ```shell
make dev make dev
``` ```

View File

@ -13,7 +13,7 @@ with [extra parameters](https://github.com/jmorganca/ollama/blob/main/docs/model
Refer Refer
to [this](/apis-extras/options-builder). to [this](/apis-extras/options-builder).
## Try asking a question about the model. ## Try asking a question about the model
```java ```java
import io.github.ollama4j.OllamaAPI; import io.github.ollama4j.OllamaAPI;
@ -87,7 +87,7 @@ You will get a response similar to:
> The capital of France is Paris. > The capital of France is Paris.
> Full response: The capital of France is Paris. > Full response: The capital of France is Paris.
## Try asking a question from general topics. ## Try asking a question from general topics
```java ```java
import io.github.ollama4j.OllamaAPI; import io.github.ollama4j.OllamaAPI;
@ -135,7 +135,7 @@ You'd then get a response from the model:
> semi-finals. The tournament was > semi-finals. The tournament was
> won by the England cricket team, who defeated New Zealand in the final. > won by the England cricket team, who defeated New Zealand in the final.
## Try asking for a Database query for your data schema. ## Try asking for a Database query for your data schema
```java ```java
import io.github.ollama4j.OllamaAPI; import io.github.ollama4j.OllamaAPI;
@ -161,6 +161,7 @@ public class Main {
``` ```
_Note: Here I've used _Note: Here I've used
a [sample prompt](https://github.com/ollama4j/ollama4j/blob/main/src/main/resources/sample-db-prompt-template.txt) a [sample prompt](https://github.com/ollama4j/ollama4j/blob/main/src/main/resources/sample-db-prompt-template.txt)
containing a database schema from within this library for demonstration purposes._ containing a database schema from within this library for demonstration purposes._
@ -173,3 +174,124 @@ FROM sales
JOIN customers ON sales.customer_id = customers.customer_id JOIN customers ON sales.customer_id = customers.customer_id
GROUP BY customers.name; GROUP BY customers.name;
``` ```
## Generate structured output
### With response as a `Map`
```java
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import io.github.ollama4j.OllamaAPI;
import io.github.ollama4j.utils.Utilities;
import io.github.ollama4j.models.chat.OllamaChatMessageRole;
import io.github.ollama4j.models.chat.OllamaChatRequest;
import io.github.ollama4j.models.chat.OllamaChatRequestBuilder;
import io.github.ollama4j.models.chat.OllamaChatResult;
import io.github.ollama4j.models.response.OllamaResult;
import io.github.ollama4j.types.OllamaModelType;
public class StructuredOutput {
public static void main(String[] args) throws Exception {
String host = "http://localhost:11434/";
OllamaAPI api = new OllamaAPI(host);
String chatModel = "qwen2.5:0.5b";
api.pullModel(chatModel);
String prompt = "Ollama is 22 years old and is busy saving the world. Respond using JSON";
Map<String, Object> format = new HashMap<>();
format.put("type", "object");
format.put("properties", new HashMap<String, Object>() {
{
put("age", new HashMap<String, Object>() {
{
put("type", "integer");
}
});
put("available", new HashMap<String, Object>() {
{
put("type", "boolean");
}
});
}
});
format.put("required", Arrays.asList("age", "available"));
OllamaResult result = api.generate(chatModel, prompt, format);
System.out.println(result);
}
}
```
### With response mapped to specified class type
```java
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import io.github.ollama4j.OllamaAPI;
import io.github.ollama4j.utils.Utilities;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import io.github.ollama4j.models.chat.OllamaChatMessageRole;
import io.github.ollama4j.models.chat.OllamaChatRequest;
import io.github.ollama4j.models.chat.OllamaChatRequestBuilder;
import io.github.ollama4j.models.chat.OllamaChatResult;
import io.github.ollama4j.models.response.OllamaResult;
import io.github.ollama4j.types.OllamaModelType;
public class StructuredOutput {
public static void main(String[] args) throws Exception {
String host = Utilities.getFromConfig("host");
OllamaAPI api = new OllamaAPI(host);
int age = 28;
boolean available = false;
String prompt = "Batman is " + age + " years old and is " + (available ? "available" : "not available")
+ " because he is busy saving Gotham City. Respond using JSON";
Map<String, Object> format = new HashMap<>();
format.put("type", "object");
format.put("properties", new HashMap<String, Object>() {
{
put("age", new HashMap<String, Object>() {
{
put("type", "integer");
}
});
put("available", new HashMap<String, Object>() {
{
put("type", "boolean");
}
});
}
});
format.put("required", Arrays.asList("age", "available"));
OllamaResult result = api.generate(CHAT_MODEL_QWEN_SMALL, prompt, format);
Person person = result.as(Person.class);
System.out.println(person.getAge());
System.out.println(person.getAvailable());
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class Person {
private int age;
private boolean available;
}
```

File diff suppressed because it is too large Load Diff

View File

@ -1,19 +1,26 @@
package io.github.ollama4j.models.response; package io.github.ollama4j.models.response;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import lombok.Data;
import lombok.Getter;
import static io.github.ollama4j.utils.Utils.getObjectMapper; import static io.github.ollama4j.utils.Utils.getObjectMapper;
import com.fasterxml.jackson.core.JsonProcessingException; import java.util.HashMap;
import lombok.Data; import java.util.Map;
import lombok.Getter;
/** The type Ollama result. */ /** The type Ollama result. */
@Getter @Getter
@SuppressWarnings("unused") @SuppressWarnings("unused")
@Data @Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class OllamaResult { public class OllamaResult {
/** /**
* -- GETTER -- * -- GETTER --
* Get the completion/response text * Get the completion/response text
* *
* @return String completion/response text * @return String completion/response text
*/ */
@ -21,7 +28,7 @@ public class OllamaResult {
/** /**
* -- GETTER -- * -- GETTER --
* Get the response status code. * Get the response status code.
* *
* @return int - response status code * @return int - response status code
*/ */
@ -29,7 +36,7 @@ public class OllamaResult {
/** /**
* -- GETTER -- * -- GETTER --
* Get the response time in milliseconds. * Get the response time in milliseconds.
* *
* @return long - response time in milliseconds * @return long - response time in milliseconds
*/ */
@ -44,9 +51,68 @@ public class OllamaResult {
@Override @Override
public String toString() { public String toString() {
try { try {
return getObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(this); Map<String, Object> responseMap = new HashMap<>();
responseMap.put("response", this.response);
responseMap.put("httpStatusCode", this.httpStatusCode);
responseMap.put("responseTime", this.responseTime);
return getObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(responseMap);
} catch (JsonProcessingException e) { } catch (JsonProcessingException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
/**
* Get the structured response if the response is a JSON object.
*
* @return Map - structured response
* @throws IllegalArgumentException if the response is not a valid JSON object
*/
public Map<String, Object> getStructuredResponse() {
String responseStr = this.getResponse();
if (responseStr == null || responseStr.trim().isEmpty()) {
throw new IllegalArgumentException("Response is empty or null");
}
try {
// Check if the response is a valid JSON
if ((!responseStr.trim().startsWith("{") && !responseStr.trim().startsWith("[")) ||
(!responseStr.trim().endsWith("}") && !responseStr.trim().endsWith("]"))) {
throw new IllegalArgumentException("Response is not a valid JSON object");
}
Map<String, Object> response = getObjectMapper().readValue(responseStr,
new TypeReference<Map<String, Object>>() {
});
return response;
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("Failed to parse response as JSON: " + e.getMessage(), e);
}
}
/**
* Get the structured response mapped to a specific class type.
*
* @param <T> The type of class to map the response to
* @param clazz The class to map the response to
* @return An instance of the specified class with the response data
* @throws IllegalArgumentException if the response is not a valid JSON or is empty
* @throws RuntimeException if there is an error mapping the response
*/
public <T> T as(Class<T> clazz) {
String responseStr = this.getResponse();
if (responseStr == null || responseStr.trim().isEmpty()) {
throw new IllegalArgumentException("Response is empty or null");
}
try {
// Check if the response is a valid JSON
if ((!responseStr.trim().startsWith("{") && !responseStr.trim().startsWith("[")) ||
(!responseStr.trim().endsWith("}") && !responseStr.trim().endsWith("]"))) {
throw new IllegalArgumentException("Response is not a valid JSON object");
}
return getObjectMapper().readValue(responseStr, clazz);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("Failed to parse response as JSON: " + e.getMessage(), e);
}
}
} }

View File

@ -0,0 +1,77 @@
package io.github.ollama4j.models.response;
import static io.github.ollama4j.utils.Utils.getObjectMapper;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@SuppressWarnings("unused")
@Data
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class OllamaStructuredResult {
private String response;
private int httpStatusCode;
private long responseTime = 0;
private String model;
public OllamaStructuredResult(String response, long responseTime, int httpStatusCode) {
this.response = response;
this.responseTime = responseTime;
this.httpStatusCode = httpStatusCode;
}
@Override
public String toString() {
try {
return getObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(this);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
/**
* Get the structured response if the response is a JSON object.
*
* @return Map - structured response
*/
public Map<String, Object> getStructuredResponse() {
try {
Map<String, Object> response = getObjectMapper().readValue(this.getResponse(),
new TypeReference<Map<String, Object>>() {
});
return response;
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
/**
* Get the structured response mapped to a specific class type.
*
* @param <T> The type of class to map the response to
* @param clazz The class to map the response to
* @return An instance of the specified class with the response data
* @throws RuntimeException if there is an error mapping the response
*/
public <T> T getStructuredResponse(Class<T> clazz) {
try {
return getObjectMapper().readValue(this.getResponse(), clazz);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
}