Compare commits

..

75 Commits

Author SHA1 Message Date
Amith Koujalgi
48d0a494ee Merge pull request #119 from ollama4j/113-tests-fix
All checks were successful
Mark stale issues / stale (push) Successful in 27s
[Fix]: NPE when tool not found
2025-04-06 23:34:53 +05:30
amithkoujalgi
b2e1330ec0 Updated 'build-docs' to 'docs' and 'start-docs' to 'docs-dev' for clarity.
Updated a workflow to run tests and build docs on pull requests.
2025-04-06 23:22:29 +05:30
amithkoujalgi
a2d95a052a - Updated Makefile to add a new remote integration test command.
- Updated docs – Enhanced DBQueryFunction to validate input arguments and throw a RuntimeException if required arguments are missing.
- Updated docs – Refactored tool specifications for fuel price, weather, and employee details to use a unified prompt function structure.  (Addresses #116).
- Improved javadoc for `chatStreaming()` API. (Addresses #115).
- Introduced `ToolInvocationException` to handle errors during tool invocation in OllamaAPI. (Addresses #117).
- Updated integration tests to include `ToolInvocationException` in method signatures for better error handling.
2025-04-06 23:05:26 +05:30
Amith Koujalgi
d86217dd0b Merge pull request #114 from ollama4j/113-tests-fix
All checks were successful
Mark stale issues / stale (push) Successful in 39s
Tests fix
2025-03-25 22:30:56 +05:30
amithkoujalgi
70b4a7961a Used smaller-sized images for the test cases 2025-03-25 21:56:45 +05:30
amithkoujalgi
0248f21654 Used smaller-sized images for the test cases 2025-03-25 21:34:13 +05:30
Amith Koujalgi
252ea50717 Update OllamaAPIIntegrationTest.java 2025-03-25 20:40:58 +05:30
Amith Koujalgi
1155a9be9f Merge pull request #113 from bavardage/support-handing-images-as-byte-arrays
add api methods to support passing images as byte[]
2025-03-25 20:36:12 +05:30
Benjamin Duffield
bc87d0c7ec add api methods to support passing images as byte[] 2025-03-25 14:00:18 +00:00
Amith Koujalgi
a895e3a0ea Merge pull request #112 from ollama4j/structured-output
Add structured response model
2025-03-25 18:59:56 +05:30
amithkoujalgi
68acedfd0a Merge branch 'structured-output' of https://github.com/ollama4j/ollama4j into structured-output 2025-03-25 00:22:22 +05:30
amithkoujalgi
be08f11027 Update OllamaAPIIntegrationTest.java 2025-03-25 00:20:25 +05:30
amithkoujalgi
b9b18271a1 Support for structured output
Added support for structured output
2025-03-24 23:45:01 +05:30
amithkoujalgi
12aa38cab0 updated doc 2025-03-24 23:43:20 +05:30
amithkoujalgi
b05f1d9b12 Refactor imports in OllamaAPIIntegrationTest for improved clarity and organization 2025-03-24 23:33:35 +05:30
Amith Koujalgi
bc2a931586 Enhance OllamaAPI and OllamaResult for improved model pulling and structured responses
- Added a retry mechanism in OllamaAPI for model pulling, allowing configurable retries.
- Introduced new methods in OllamaResult for structured response handling, including parsing JSON responses into a Map or specific class types.
- Updated integration tests to validate the new functionality and ensure robust testing of model interactions.
- Improved code formatting and consistency across the OllamaAPI and integration test classes.
2025-03-24 21:40:20 +05:30
Amith Koujalgi
1bda78e35b revert OllamaResult.java
Signed-off-by: Amith Koujalgi <koujalgi.amith@gmail.com>
2025-03-24 18:24:09 +05:30
Amith Koujalgi
e7e71f6421 Enhance integration tests and Makefile for external Ollama host support
- Updated the Makefile to include a new target for local integration tests with external Ollama host configuration.
- Modified the OllamaAPIIntegrationTest to dynamically set the Ollama host based on environment variables, allowing for both external and Testcontainers usage.
- Refactored model pulling logic in tests to use constants for model names, improving readability and maintainability.
2025-03-24 17:59:24 +05:30
Amith Koujalgi
57f874921c Add Windows installation note for Chocolatey in README
- Added a note in the README.md to guide Windows users on installing Chocolatey Package Manager and using it to install `make`.
- Suggested running the installation command with administrator privileges for better success.
2025-03-24 15:48:01 +05:30
Amith Koujalgi
2d7902167b Enhance OllamaAPI and documentation for structured responses
- Updated OllamaAPI to return an instance of OllamaResult instead of OllamaStructuredResult for structured responses.
- Removed the obsolete OllamaStructuredResult class.
- Added new methods in OllamaResult for retrieving structured responses as a Map or mapped to a specific class type.
- Updated integration tests to validate the new structured response functionality.
- Improved Makefile with a new full-build target for building the project.
2025-03-24 15:30:00 +05:30
amithkoujalgi
407b7eb280 Refactor OllamaAPI documentation and add structured response model
- Improved formatting and readability of comments in OllamaAPI.java.
- Introduced OllamaStructuredResult class to handle structured responses from the Ollama API.
- Updated integration tests to include a new test for structured output from the API.
- Cleaned up imports and ensured consistent code style across the OllamaAPIIntegrationTest class.
2025-03-24 00:25:20 +05:30
amithkoujalgi
e62a7511db Merge remote-tracking branch 'origin/main'
All checks were successful
Mark stale issues / stale (push) Successful in 38s
2025-03-23 22:21:42 +05:30
Amith Koujalgi
c904a69b09 Merge pull request #110 from ollama4j/integration-tests-updates
All checks were successful
Mark stale issues / stale (push) Successful in 40s
Updated docs
2025-03-19 08:50:22 +05:30
Amith Koujalgi
11bf20c405 Updated docs 2025-03-19 08:49:58 +05:30
Amith Koujalgi
c3273ea8ca Merge pull request #109 from ollama4j/integration-tests-updates
Updated docs
2025-03-19 08:32:00 +05:30
Amith Koujalgi
f6a29842b5 Updated docs 2025-03-19 08:31:10 +05:30
Amith Koujalgi
3781ea7a51 Merge pull request #108 from ollama4j/integration-tests-updates
Add blog post about tooling with Couchbase
2025-03-19 08:20:39 +05:30
Amith Koujalgi
6f1da25f7e Updated GH action 2025-03-19 08:20:09 +05:30
Amith Koujalgi
e74ef7115c Add blog post about tooling with couchbase 2025-03-19 08:08:39 +05:30
Amith Koujalgi
c9db51a71e Merge pull request #107 from ollama4j/integration-tests-updates
All checks were successful
Mark stale issues / stale (push) Successful in 21s
test
2025-03-18 23:29:21 +05:30
Amith Koujalgi
681a692ca9 Updated integration tests 2025-03-18 23:18:42 +05:30
Amith Koujalgi
9a6065fdb3 Updated integration tests 2025-03-18 23:07:19 +05:30
Amith Koujalgi
e245d9633f Updated integration tests 2025-03-18 22:54:56 +05:30
Amith Koujalgi
590364dd53 test 2025-03-18 22:33:29 +05:30
Amith Koujalgi
bb4e7477bd Merge pull request #106 from ollama4j/integration-tests-updates
test
2025-03-18 22:32:38 +05:30
Amith Koujalgi
c33c1c8315 test 2025-03-18 22:23:06 +05:30
Amith Koujalgi
05eecdccaa Merge pull request #105 from ollama4j/integration-tests-refactor
Integration tests refactor
2025-03-18 22:17:33 +05:30
Amith Koujalgi
26bb2f9bab Updated GH workflow 2025-03-18 22:15:58 +05:30
Amith Koujalgi
bbafc95577 Updated GH workflow 2025-03-18 22:13:39 +05:30
Amith Koujalgi
bee09aa626 Updated integration tests 2025-03-18 22:03:04 +05:30
Amith Koujalgi
8aa6e3b066 Updated integration tests 2025-03-18 21:41:20 +05:30
Amith Koujalgi
d40912c638 Merge remote-tracking branch 'origin/main' into integration-tests-refactor 2025-03-18 20:54:04 +05:30
Amith Koujalgi
ba0444194f Merge pull request #98 from csware/bearertoken
Support bearer token
2025-03-18 20:30:08 +05:30
Amith Koujalgi
ac3f505aa6 Switch image model to "moondream" in integration test 2025-03-11 13:12:55 +05:30
Amith Koujalgi
7e5ca53fda Merge remote-tracking branch 'origin/integration-tests-refactor' into integration-tests-refactor
# Conflicts:
#	Makefile
#	README.md
2025-03-11 12:28:39 +05:30
Amith Koujalgi
2b0238b9e8 Ensure Docker availability in dev setup and integration tests
Updated `README.md` to include Docker as a prerequisite for running integration tests using Testcontainers. Modified the `Makefile` to check for Docker installation during the dev environment setup.
2025-03-11 12:26:35 +05:30
amithkoujalgi
469a0fe491 Refactor
- Remove TestRealAPIs and enhance OllamaAPIIntegrationTest
- Add dev setup instruction
2025-03-11 12:26:08 +05:30
Amith Koujalgi
983a3617f0 Add dev setup instructions and update pre-commit config 2025-03-11 12:15:19 +05:30
Amith Koujalgi
b638b981c9 Remove unnecessary blank lines from pom.xml
Cleaned up redundant blank lines at the end of the pom.xml file to ensure consistent formatting. This helps improve code readability and adheres to standard practices.
2025-03-11 12:05:11 +05:30
amithkoujalgi
fe5078891f Remove TestRealAPIs and enhance OllamaAPIIntegrationTest 2025-03-11 11:41:51 +05:30
Amith Koujalgi
44b4de9ed9 Merge pull request #102 from ollama4j/update-pre-commit-hook
All checks were successful
Mark stale issues / stale (push) Successful in 31s
update-pre-commit-hook
2025-03-11 10:38:05 +05:30
amithkoujalgi
854c0b4acf test GH action 2025-03-11 10:35:06 +05:30
Amith Koujalgi
18c5d06a6c Merge pull request #101 from ollama4j/update-pre-commit-hook
update-pre-commit-hook
2025-03-11 10:34:10 +05:30
amithkoujalgi
22b403d0b0 Remove unnecessary write permission for packages in workflow 2025-03-11 10:33:14 +05:30
amithkoujalgi
ee0493eb57 Rename and adjust workflows for PR builds and testing.
Renamed the PR-related workflow for clarity and replaced `build-on-pr-create.yml` with `build-and-test-on-pr-open.yml` for better naming consistency. Also commented out the push trigger in `run-tests.yml` to refine its activation criteria.
2025-03-11 10:19:34 +05:30
Amith Koujalgi
f966b4b74e Merge pull request #100 from ollama4j/update-pre-commit-hook
update-pre-commit-hook
2025-03-11 10:18:06 +05:30
amithkoujalgi
1dadbacd2c Enable no-commit-to-branch pre-commit hook. 2025-03-11 10:12:03 +05:30
amithkoujalgi
714c16c216 Merge remote-tracking branch 'origin/main' 2025-03-11 10:10:46 +05:30
amithkoujalgi
cf2c510b23 Add integration test step to CI workflow
Previously, only unit tests were run during the PR workflow. This update introduces a separate step to run integration tests, ensuring broader test coverage. It enhances build verification by validating both unit and integration aspects.
2025-03-11 10:10:14 +05:30
amithkoujalgi
a0bcc47b2e Add pre-commit configuration file
test
2025-03-11 10:08:57 +05:30
amithkoujalgi
57ecbc2572 Add pre-commit configuration file
test
2025-03-11 10:00:11 +05:30
amithkoujalgi
99beb3e6d0 Add pre-commit configuration file
test
2025-03-11 09:31:12 +05:30
amithkoujalgi
7756eed9a0 Add pre-commit configuration file
Introduce a pre-commit-config.yaml to automate code quality checks and enforce best practices. Includes hooks for file validation, formatting, and commit message standardization, as well as Java-specific quality tools. This ensures consistent coding standards and reduces manual errors.
2025-03-11 09:29:57 +05:30
amithkoujalgi
b795117f0a Add integration test step to CI workflow
Some checks failed
Run Unit and Integration Tests / run-tests (push) Failing after 2m2s
Mark stale issues / stale (push) Successful in 15s
Previously, only unit tests were run during the PR workflow. This update introduces a separate step to run integration tests, ensuring broader test coverage. It enhances build verification by validating both unit and integration aspects.
2025-03-11 00:20:01 +05:30
amithkoujalgi
0d091d1826 Update integration test 2025-03-11 00:06:30 +05:30
amithkoujalgi
9fd77a6743 Add branch trigger for tests and update README with badge
Added a trigger to run tests on pushes to the main branch in the GitHub Actions workflow. Also updated the README to include a badge linking to the test workflow for better visibility.
2025-03-11 00:02:13 +05:30
amithkoujalgi
57a962148b Update workflow name and job for clarity in testing
Renamed the workflow to specify both unit and integration tests. Adjusted the job name to better reflect its purpose and ensured clear descriptions for inputs. These changes enhance readability and intent in the CI configuration.
2025-03-10 23:58:03 +05:30
amithkoujalgi
3c64f2099f Add GPG plugin with test-skipping configuration
Integrated the Maven GPG plugin to sign artifacts during the "verify" phase, with the ability to skip it during tests using a new `skipGpgPluginDuringTests` property. Enhanced the build profiles to manage GPG signing selectively, ensuring smoother test and build workflows.
2025-03-10 23:53:02 +05:30
amithkoujalgi
a9c7f4e5e0 Rename TestAPIsTest to OllamaAPIIntegrationTest 2025-03-10 23:44:18 +05:30
amithkoujalgi
e7f58d4e0d Add integration tests and enhance test configurations
Introduced integration tests for various API functionalities, ensuring comprehensive coverage. Updated test dependencies in `pom.xml` and added handling for unknown JSON properties in the `Model` class. Also included configuration to support running unit and integration tests in the CI workflow.
2025-03-10 23:40:44 +05:30
Sven Strickroth
138497b30f Introduce BearerAuth class
Signed-off-by: Sven Strickroth <email@cs-ware.de>
2025-03-10 14:55:38 +01:00
Sven Strickroth
3a792090e2 Support bearer token
May be use as follows:
```
ollamaAPI.setBasicAuth(new BasicAuth() {
	@Override
	public String getBasicAuthHeaderValue() { return "Bearer [sometext]"; }
});
```

Signed-off-by: Sven Strickroth <email@cs-ware.de>
2025-03-10 14:39:54 +01:00
amithkoujalgi
7ef859bba5 clean up
All checks were successful
Mark stale issues / stale (push) Successful in 31s
2025-03-09 20:29:34 +05:30
amithkoujalgi
3c30593e1e clean up
All checks were successful
Mark stale issues / stale (push) Successful in 31s
2025-03-08 17:44:00 +05:30
amithkoujalgi
98b794ca2b Add a test GitHub Actions workflow to publish to a GitHub repository, Maven repository, and GitHub Pages 2025-03-08 17:24:29 +05:30
25 changed files with 2627 additions and 1000 deletions

View File

@@ -1,34 +0,0 @@
# This workflow will build a package using Maven and then publish it to GitHub packages when a release is created
# For more information see: https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#apache-maven-with-a-settings-path
name: Build on PR Create
on:
pull_request:
types: [ opened, reopened ]
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v3
- name: Set up JDK 11
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'adopt-hotspot'
server-id: github # Value of the distributionManagement/repository/id field of the pom.xml
settings-path: ${{ github.workspace }} # location for the settings.xml file
- name: Build with Maven
run: mvn --file pom.xml -U clean package
- name: Run Tests
run: mvn --file pom.xml -U clean test -Punit-tests

View File

@@ -0,0 +1,46 @@
name: Run Tests
on:
pull_request:
# types: [opened, reopened, synchronize, edited]
branches: [ "main" ]
paths:
- 'src/**' # Run if changes occur in the 'src' folder
- 'pom.xml' # Run if changes occur in the 'pom.xml' file
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
run-tests:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v3
- name: Set up JDK 11
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'adopt-hotspot'
server-id: github # Value of the distributionManagement/repository/id field of the pom.xml
settings-path: ${{ github.workspace }} # location for the settings.xml file
- name: Build with Maven
run: mvn --file pom.xml -U clean package
- name: Run unit tests
run: mvn --file pom.xml -U clean test -Punit-tests
- name: Run integration tests
run: mvn --file pom.xml -U clean verify -Pintegration-tests
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: '20.x'
- run: cd docs && npm ci
- run: cd docs && npm run build

View File

@@ -1,15 +1,18 @@
name: Run Tests
name: Run Unit and Integration Tests
on:
# push:
# branches:
# - main
workflow_dispatch:
inputs:
branch:
description: 'Branch name to run tests on'
description: 'Branch name to run the tests on'
required: true
default: 'main'
jobs:
build:
run-tests:
runs-on: ubuntu-latest
steps:
@@ -27,3 +30,6 @@ jobs:
- name: Run unit tests
run: mvn clean test -Punit-tests
- name: Run integration tests
run: mvn clean verify -Pintegration-tests

38
.pre-commit-config.yaml Normal file
View File

@@ -0,0 +1,38 @@
repos:
# pre-commit hooks
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: "v5.0.0"
hooks:
- id: no-commit-to-branch
args: ['--branch', 'main']
- id: check-merge-conflict
- id: check-added-large-files
- id: check-yaml
- id: check-xml
- id: check-json
- id: pretty-format-json
args: ['--no-sort-keys', '--autofix', '--indent=4']
- id: end-of-file-fixer
exclude: \.json$
files: \.java$|\.xml$
- id: trailing-whitespace
- id: mixed-line-ending
# for commit message formatting
- repo: https://github.com/commitizen-tools/commitizen
rev: v4.4.1
hooks:
- id: commitizen
stages: [commit-msg]
# # for java code quality
# - repo: https://github.com/gherynos/pre-commit-java
# rev: v0.6.10
# hooks:
# - id: pmd
# exclude: /test/
# - id: cpd
# exclude: /test/
# - id: checkstyle
# exclude: /test/

View File

@@ -1,11 +1,25 @@
dev:
@echo "Setting up dev environment..."
@command -v pre-commit >/dev/null 2>&1 || { echo "Error: pre-commit is not installed. Please install it first."; exit 1; }
@command -v docker >/dev/null 2>&1 || { echo "Error: docker is not installed. Please install it first."; exit 1; }
pre-commit install
pre-commit autoupdate
pre-commit install --install-hooks
build:
mvn -B clean install -Dgpg.skip=true
full-build:
mvn -B clean install
unit-tests:
mvn clean test -Punit-tests
integration-tests:
mvn clean verify -Pintegration-tests
export USE_EXTERNAL_OLLAMA_HOST=false && mvn clean verify -Pintegration-tests
integration-tests-remote:
export USE_EXTERNAL_OLLAMA_HOST=true && export OLLAMA_HOST=http://192.168.29.223:11434 && mvn clean verify -Pintegration-tests -Dgpg.skip=true
doxygen:
doxygen Doxyfile
@@ -15,10 +29,10 @@ list-releases:
--compressed \
--silent | jq -r '.components[].version'
build-docs:
docs:
npm i --prefix docs && npm run build --prefix docs
start-docs:
docs-dev:
npm i --prefix docs && npm run start --prefix docs
start-cpu:

101
README.md
View File

@@ -31,8 +31,11 @@ Find more details on the [website](https://ollama4j.github.io/ollama4j/).
![GitHub last commit](https://img.shields.io/github/last-commit/ollama4j/ollama4j?color=green)
[![codecov](https://codecov.io/gh/ollama4j/ollama4j/graph/badge.svg?token=U0TE7BGP8L)](https://codecov.io/gh/ollama4j/ollama4j)
[![Run Unit and Integration Tests](https://github.com/ollama4j/ollama4j/actions/workflows/run-tests.yml/badge.svg)](https://github.com/ollama4j/ollama4j/actions/workflows/run-tests.yml)
![Build Status](https://github.com/ollama4j/ollama4j/actions/workflows/maven-publish.yml/badge.svg)
</div>
[//]: # (![Hits]&#40;https://hits.seeyoufarm.com/api/count/incr/badge.svg?url=https%3A%2F%2Fgithub.com%2Follama4j%2Follama4j&count_bg=%2379C83D&title_bg=%23555555&icon=&icon_color=%23E7E7E7&title=hits&edge_flat=false&#41;)
@@ -74,61 +77,6 @@ Find more details on the [website](https://ollama4j.github.io/ollama4j/).
<img src="https://img.shields.io/badge/v0.3.0-green.svg?style=for-the-badge&labelColor=gray&label=Ollama&color=blue" alt=""/>
</a>
<table>
<tr>
<td>
<a href="https://ollama.ai/" target="_blank">Local Installation</a>
</td>
<td>
<a href="https://hub.docker.com/r/ollama/ollama" target="_blank">Docker Installation</a>
</td>
</tr>
<tr>
<td>
<a href="https://ollama.com/download/Ollama-darwin.zip" target="_blank">Download for macOS</a>
<a href="https://ollama.com/download/OllamaSetup.exe" target="_blank">Download for Windows</a>
Install on Linux
```shell
curl -fsSL https://ollama.com/install.sh | sh
```
</td>
<td>
CPU only
```shell
docker run -d -p 11434:11434 \
-v ollama:/root/.ollama \
--name ollama \
ollama/ollama
```
NVIDIA GPU
```shell
docker run -d -p 11434:11434 \
--gpus=all \
-v ollama:/root/.ollama \
--name ollama \
ollama/ollama
```
</td>
</tr>
</table>
## Installation
> [!NOTE]
@@ -243,33 +191,60 @@ dependencies {
> [!TIP]
> Find the full API specifications on the [website](https://ollama4j.github.io/ollama4j/).
#### Development
### Development
Build:
Make sure you have `pre-commit` installed.
With `brew`:
```shell
brew install pre-commit
```
With `pip`:
```shell
pip install pre-commit
```
#### 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
make dev
```
#### Build
```shell
make build
```
Run unit tests:
#### Run unit tests
```shell
make unit-tests
```
Run integration tests:
#### Run integration tests
Make sure you have Docker running as this uses [testcontainers](https://testcontainers.com/) to run the integration
tests on Ollama Docker container.
```shell
make integration-tests
```
#### Releases
### Releases
Newer artifacts are published via GitHub Actions CI workflow when a new release is created from `main` branch.
## Examples
The `ollama4j-examples` repository contains examples for using the Ollama4j library. You can explore it [here](https://github.com/ollama4j/ollama4j-examples).
The `ollama4j-examples` repository contains examples for using the Ollama4j library. You can explore
it [here](https://github.com/ollama4j/ollama4j-examples).
## ⭐ Give us a Star!
@@ -288,7 +263,7 @@ If you like or are using this project to build your own, please give us a star.
| 7 | Katie Backend | An open-source AI-based question-answering platform for accessing private domain knowledge | [GitHub](https://github.com/wyona/katie-backend) |
| 8 | TeleLlama3 Bot | A question-answering Telegram bot | [Repo](https://git.hiast.edu.sy/mohamadbashar.disoki/telellama3-bot) |
| 9 | moqui-wechat | A moqui-wechat component | [GitHub](https://github.com/heguangyong/moqui-wechat) |
| 10 | B4X | A set of simple and powerful RAD tool for Desktop and Server development | [Website](https://www.b4x.com/android/forum/threads/ollama4j-library-pnd_ollama4j-your-local-offline-llm-like-chatgpt.165003/) |
| 10 | B4X | A set of simple and powerful RAD tool for Desktop and Server development | [Website](https://www.b4x.com/android/forum/threads/ollama4j-library-pnd_ollama4j-your-local-offline-llm-like-chatgpt.165003/) |
| 11 | Research Article | Article: `Large language model based mutations in genetic improvement` - published on National Library of Medicine (National Center for Biotechnology Information) | [Website](https://pmc.ncbi.nlm.nih.gov/articles/PMC11750896/) |
## Traction
@@ -356,7 +331,7 @@ project.
</a>
</p>
### Appreciate my work?
### Appreciate the work?
<p align="center">
<a href="https://www.buymeacoffee.com/amithkoujalgi" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>

View File

@@ -0,0 +1,709 @@
---
slug: talk-to-your-data-on-couchbase-via-ollama4j
title: "Talk to Your Data Using Natural Language: A Guide to Interacting with Couchbase via Ollama4j"
authors: [ amith ]
tags: [ Java, AI, LLM, GenAI, GenerativeAI, Generative AI Tools, Ollama, Ollama4J, OpenSource, Developers,
]
---
Sometime back, I created a small wrapper called Ollama4j to interact with the Ollama server over the REST API in Java as
a side project and made the [repository](https://github.com/ollama4j/ollama4j) public on GitHub. Over time, the project
gained traction, with many fellow Java
developers contributing, and it now boasts over _300 stars_! 😍
Weve consistently introduced new features, and when we added the tool-calling capability, the library became incredibly
powerful, opening up so many possibilities. With this addition, we could automate numerous tasks using natural language!
I wanted to share how to make the most of this functionality.
In this article, well explore how to use Ollama4j, a Java SDK for interacting with Ollama-hosted models, to leverage
tool-calling models like Mistral for querying a Couchbase database. The goal is to create a system where you can query
your database using natural, conversational language — just like interacting with a virtual assistant. Well walk you
through the code, explain the key components, and show you how to set up your environment to ensure everything runs
smoothly.
### Overview of the Technologies Involved
Before diving into the implementation, lets understand the core technologies were using:
- **Ollama4j**: A Java SDK that interacts with hosted AI models through a convenient API. Ollama allows you to interact
with
pre-trained models (like Mistral) and access additional tools that can be applied to real-world tasks.
- **Mistral**: A powerful, language-based model that can be used for a variety of tasks, including answering questions,
text
generation, and data retrieval from external sources. While Ive used Mistral in this instance, you can easily replace
it with [any other model](https://ollama.com/search?c=tools) that supports tool-calling capabilities.
- **Couchbase**: A NoSQL database that provides a flexible and scalable data model. In this example, well query a
Couchbase
database to retrieve airline information.
The magic happens when we combine these technologies to allow the model to query the database in a more intuitive and
human-like way, acting as an interface between the users natural language and Couchbases structured data.
> Oh, by the way, you can either set up
> a [Couchbase server](https://www.couchbase.com/downloads/?family=couchbase-server) on your own or, if you prefer a
> more
> effortless approach like
> I
> do, give [Couchbase Capella](https://www.couchbase.com/products/capella/) a spin. Its a fully managed
> Database-as-a-Service (DBaaS) with a free tier 🎉 thats so
> easy
> to set up, youll be querying your data in no time. Its perfect for developers who want to dive in without any
> hassle —
> its like having your own cloud database, minus the headache!
In the following section, we will walk you through the simple steps to create your free Couchbase Capella database
cluster. If youd prefer to set up your own Couchbase server elsewhere, feel free to skip this section and go directly
to the [Code Environment Setup](#setting-up-the-environment-for-code) section.
Sign up for a free database cluster on Couchbase Capella
Head over to https://cloud.couchbase.com/sign-in and sign up for an account.
<img src={'https://miro.medium.com/v2/resize:fit:1400/format:webp/1*vsJC0ugfoh9vpYNapt4-5A.png'} />
Once youre in, you will be able to create a new database cluster. Click on the _**Operational**_ tab and click on the
**_Create Cluster_** button.
<img src={'https://miro.medium.com/v2/resize:fit:1400/format:webp/1*ZNicgmYNkclgaBIxwRN7Ug.png'} />
Select the default project named **_My First Project_** and click on the **_Continue_** button.
<img src={'https://miro.medium.com/v2/resize:fit:1400/format:webp/1*vfc2cF7IgkjLtNXvls8giQ.png'} />
Youll now see the available cluster options. Go ahead and select the **_Free_** option! 😍
Next, choose your preferred cloud provider (you can select any provider or stick with the default AWS provider).
Pick a region (or leave it set to the default).
Finally, click on the Create Cluster button to proceed.
<img src={'https://miro.medium.com/v2/resize:fit:1400/format:webp/1*rdWpeSrUaBKC6Y5q8Kd6EA.png'} />
Give it a couple of minutes, and let the magic happen as your cluster gets deployed.
<img src={'https://miro.medium.com/v2/resize:fit:1400/format:webp/1*no3uHx8cIzVBn7qccYEZ3A.png'} />
Once your cluster is deployed, youll see the status of your cluster as **_Healthy_**.
<img src={'https://miro.medium.com/v2/resize:fit:1400/format:webp/1*Jyu9uiSDSE0o-EQRb53CJA.png'} />
Click on the listed cluster to open its details. Here, you can view the version of the deployed Couchbase server, the
enabled services, as well as the cloud provider and region.
<img src={'https://miro.medium.com/v2/resize:fit:1400/format:webp/1*Sv-7wQuAoD0l0bjbI5I7Aw.png'} />
Click on **_Explore Data_** button. Notice that a default bucket called **_travel-sample_** with some sample data has
been created
for you.
<img src={'https://miro.medium.com/v2/resize:fit:1400/format:webp/1*z85GsgMBvdR2mrvKUrIjJg.png'} />
Browse through the collection to explore the pre-created buckets, scopes and collections available to you.
<img src={'https://miro.medium.com/v2/resize:fit:1400/format:webp/1*Qr84bs1dvn6m9ZjkNxXvUg.png'} />
Open up a sample document from the **_travel-sample_** (bucket) > **_inventory_** (scope) > **_airline_** (collection)
to see the contents
of the document.
The document shown in the image below is about an airline named **_Astraeus_**, whose call sign (a unique name or code
used to
identify an airline or aircraft in communication) is **_FLYSTAR_**.
<img src={'https://miro.medium.com/v2/resize:fit:1400/format:webp/1*AmvixYfdNNKC6nXNNXbe4Q.png'} />
Navigate to the **_Connect_** tab, and you will see a **_Public Connection String_** that allows you to access the
Capella cluster
endpoint from your client application, which looks like the following URL:
```
couchbases://cb.uniqueclusteridentifer.cloud.couchbase.com
```
<img src={'https://miro.medium.com/v2/resize:fit:1400/format:webp/1*jwnVdj5ZOQMHoggj9JZeJQ.png'} />
To access this cluster endpoint, you need to allow the IP addresses that are permitted to connect. Click on the
**_Settings_**
tab, which will take you to the **_Cluster Settings_** view. Then, click on **_Allowed IP Addresses_** in the left pane
under
**_Networking_**, where you can add allowed IP addresses. Then, click on the **_Add Allowed IP_** button.
<img src={'https://miro.medium.com/v2/resize:fit:1400/format:webp/1*tS83AJaNzlBa4Q3aadxohw.png'} />
You can either click on the **_Add Current IP Address_** button to limit access to your cluster to your IP address
alone, or
if youd like to allow access from anywhere, click on the **_Allow Access from Anywhere_** button.
<img src={'https://miro.medium.com/v2/resize:fit:1400/format:webp/1*XBgqQoXQQJyYg51Ztugw6w.png'} />
Confirm that you want to allow the IP addresses.
<img src={'https://miro.medium.com/v2/resize:fit:1400/format:webp/1*WjfYQQaiT2WqwNnWvUCyww.png'} />
The IP addresses have now been added to the allow list, and the networking is set up.
<img src={'https://miro.medium.com/v2/resize:fit:1400/format:webp/1*5BHIp2rqUf7E_GNX8TENoA.png'} />
Now that youve allowed IP addresses, its time to create credentials for accessing the cluster using a username and
password. Click on the **_Cluster Access_** tab in the left pane, then click on the **_Create Cluster Access_** button.
<img src={'https://miro.medium.com/v2/resize:fit:1400/format:webp/1*Q5l_EE3gGtxiANdkKilVTQ.png'} />
Enter a username of your choice in the **_Cluster Access Name_** text field, and then enter a password of your choice in
the
**_Password_** text field.
Next, select the bucket, scope, and the read/write permissions you want these credentials to have access to. In this
example, Ive granted access to all buckets and scopes with both read and write permissions.
<img src={'https://miro.medium.com/v2/resize:fit:1400/format:webp/1*j2DRB1oDWE78SKpcsIb2SA.png'} />
Alright, your cluster access is now set up.
<img src={'https://miro.medium.com/v2/resize:fit:1400/format:webp/1*8TY-5DPDfQlwz0-2IYR8Sg.png'} />
One last step: you just need to select the **_Cluster Access Credentials_** that you want to allow to connect to your
Capella
cluster. Head over to the **_Connect_** tab, then click on the **_SDKs_** tab in the left pane. Under Choose the
**_Cluster Access Credentials you want to use to connect to your Capella cluster_**, select the cluster credentials you
just created.
<img src={'https://miro.medium.com/v2/resize:fit:1400/format:webp/1*sIlH51v2HllTzBDV8K-9Aw.png'} />
Awesome! Your cluster access is all set up, and youre ready to connect to your Capella cluster using a Couchbase
client. Thats it — youre all set and good to go!
### Setting Up the Environment For Code
Before you begin, ensure you have the following components setup.
**Java**: Make sure you have Java 11+ installed on your system. Set it up
from [here](https://www.oracle.com/in/java/technologies/downloads/). Verify it by running the following
command in your terminal.
```shell
java --version
```
**Maven**: Make sure you have the Maven build system set up. Set it up from [here](https://maven.apache.org/download.cgi).
Verify it by running the following command
in your terminal.
```
mvn --version
```
**Ollama Server**: Make sure you have installed the latest version of [Ollama server](https://ollama.com/) and it is up
and running. Verify it by
running the following command in your terminal.
```shell
ollama --version
```
**Model**: Youll need [tool-calling model](https://ollama.com/search?c=tools) (such as Mistral) downloaded and ready to
serve from your Ollama server.
To download/pull the model into your Ollama server, run the following command in your terminal.
```shell
ollama pull mistral
```
You can list the models available on your model server by running the following command in your terminal.
```shell
ollama list
```
Once you have these, you can start setting up the application.
Setup `pom.xml` for your Maven project.
```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>io.github.ollama4j.couchbase</groupId>
<artifactId>ollama4j-couchbase</artifactId>
<version>0.0.1</version>
<name>Ollama4j Couchbase</name>
<description>Talk to your data in Couchbase over Ollama4j</description>
<packaging>jar</packaging>
<properties>
<maven.compiler.release>11</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<lombok.version>1.18.30</lombok.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.3.1</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.11.2</version>
<configuration>
<!-- to disable the "missing" warnings. Remove the doclint to enable warnings-->
<doclint>all,-missing</doclint>
</configuration>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
<pluginManagement>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.0</version>
</plugin>
</plugins>
</pluginManagement>
</build>
<dependencies>
<dependency>
<groupId>io.github.ollama4j</groupId>
<artifactId>ollama4j</artifactId>
<version>ollama4j-revision</version>
</dependency>
<!-- SLF4J API -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.0</version>
</dependency>
<!-- Logback Classic (SLF4J binding) -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.12</version>
</dependency>
<dependency>
<groupId>com.couchbase.client</groupId>
<artifactId>java-client</artifactId>
<version>3.7.8</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
```
### Code Walkthrough
Heres the main part of the implementation in the Java code.
```java
package io.github.ollama4j.examples;
import com.couchbase.client.java.Bucket;
import com.couchbase.client.java.Cluster;
import com.couchbase.client.java.ClusterOptions;
import com.couchbase.client.java.Scope;
import com.couchbase.client.java.json.JsonObject;
import com.couchbase.client.java.query.QueryResult;
import io.github.ollama4j.OllamaAPI;
import io.github.ollama4j.exceptions.OllamaBaseException;
import io.github.ollama4j.exceptions.ToolInvocationException;
import io.github.ollama4j.tools.OllamaToolsResult;
import io.github.ollama4j.tools.ToolFunction;
import io.github.ollama4j.tools.Tools;
import io.github.ollama4j.utils.OptionsBuilder;
import io.github.ollama4j.utils.Utilities;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.IOException;
import java.time.Duration;
import java.util.Arrays;
import java.util.Map;
public class CouchbaseToolCallingExample {
public static void main(String[] args) throws IOException, ToolInvocationException, OllamaBaseException, InterruptedException {
String connectionString = Utilities.getFromEnvVar("CB_CLUSTER_URL");
String username = Utilities.getFromEnvVar("CB_CLUSTER_USERNAME");
String password = Utilities.getFromEnvVar("CB_CLUSTER_PASSWORD");
String bucketName = "travel-sample";
Cluster cluster = Cluster.connect(
connectionString,
ClusterOptions.clusterOptions(username, password).environment(env -> {
env.applyProfile("wan-development");
})
);
String host = Utilities.getFromConfig("host");
String modelName = Utilities.getFromConfig("tools_model_mistral");
OllamaAPI ollamaAPI = new OllamaAPI(host);
ollamaAPI.setVerbose(false);
ollamaAPI.setRequestTimeoutSeconds(60);
Tools.ToolSpecification callSignFinderToolSpec = getCallSignFinderToolSpec(cluster, bucketName);
Tools.ToolSpecification callSignUpdaterToolSpec = getCallSignUpdaterToolSpec(cluster, bucketName);
ollamaAPI.registerTool(callSignFinderToolSpec);
ollamaAPI.registerTool(callSignUpdaterToolSpec);
String prompt1 = "What is the call-sign of Astraeus?";
for (OllamaToolsResult.ToolResult r : ollamaAPI.generateWithTools(modelName, new Tools.PromptBuilder()
.withToolSpecification(callSignFinderToolSpec)
.withPrompt(prompt1)
.build(), new OptionsBuilder().build()).getToolResults()) {
AirlineDetail airlineDetail = (AirlineDetail) r.getResult();
System.out.println(String.format("[Result of tool '%s']: Call-sign of %s is '%s'! ✈️", r.getFunctionName(), airlineDetail.getName(), airlineDetail.getCallsign()));
}
String prompt2 = "I want to code name Astraeus as STARBOUND";
for (OllamaToolsResult.ToolResult r : ollamaAPI.generateWithTools(modelName, new Tools.PromptBuilder()
.withToolSpecification(callSignUpdaterToolSpec)
.withPrompt(prompt2)
.build(), new OptionsBuilder().build()).getToolResults()) {
Boolean updated = (Boolean) r.getResult();
System.out.println(String.format("[Result of tool '%s']: Call-sign is %s! ✈️", r.getFunctionName(), updated ? "updated" : "not updated"));
}
String prompt3 = "What is the call-sign of Astraeus?";
for (OllamaToolsResult.ToolResult r : ollamaAPI.generateWithTools(modelName, new Tools.PromptBuilder()
.withToolSpecification(callSignFinderToolSpec)
.withPrompt(prompt3)
.build(), new OptionsBuilder().build()).getToolResults()) {
AirlineDetail airlineDetail = (AirlineDetail) r.getResult();
System.out.println(String.format("[Result of tool '%s']: Call-sign of %s is '%s'! ✈️", r.getFunctionName(), airlineDetail.getName(), airlineDetail.getCallsign()));
}
}
public static Tools.ToolSpecification getCallSignFinderToolSpec(Cluster cluster, String bucketName) {
return Tools.ToolSpecification.builder()
.functionName("airline-lookup")
.functionDescription("You are a tool who finds only the airline name and do not worry about any other parameters. You simply find the airline name and ignore the rest of the parameters. Do not validate airline names as I want to use fake/fictitious airline names as well.")
.toolFunction(new AirlineCallsignQueryToolFunction(bucketName, cluster))
.toolPrompt(
Tools.PromptFuncDefinition.builder()
.type("prompt")
.function(
Tools.PromptFuncDefinition.PromptFuncSpec.builder()
.name("get-airline-name")
.description("Get the airline name")
.parameters(
Tools.PromptFuncDefinition.Parameters.builder()
.type("object")
.properties(
Map.of(
"airlineName", Tools.PromptFuncDefinition.Property.builder()
.type("string")
.description("The name of the airline. e.g. Emirates")
.required(true)
.build()
)
)
.required(java.util.List.of("airline-name"))
.build()
)
.build()
)
.build()
)
.build();
}
public static Tools.ToolSpecification getCallSignUpdaterToolSpec(Cluster cluster, String bucketName) {
return Tools.ToolSpecification.builder()
.functionName("airline-update")
.functionDescription("You are a tool who finds the airline name and its callsign and do not worry about any validations. You simply find the airline name and its callsign. Do not validate airline names as I want to use fake/fictitious airline names as well.")
.toolFunction(new AirlineCallsignUpdateToolFunction(bucketName, cluster))
.toolPrompt(
Tools.PromptFuncDefinition.builder()
.type("prompt")
.function(
Tools.PromptFuncDefinition.PromptFuncSpec.builder()
.name("get-airline-name-and-callsign")
.description("Get the airline name and callsign")
.parameters(
Tools.PromptFuncDefinition.Parameters.builder()
.type("object")
.properties(
Map.of(
"airlineName", Tools.PromptFuncDefinition.Property.builder()
.type("string")
.description("The name of the airline. e.g. Emirates")
.required(true)
.build(),
"airlineCallsign", Tools.PromptFuncDefinition.Property.builder()
.type("string")
.description("The callsign of the airline. e.g. Maverick")
.enumValues(Arrays.asList("petrol", "diesel"))
.required(true)
.build()
)
)
.required(java.util.List.of("airlineName", "airlineCallsign"))
.build()
)
.build()
)
.build()
)
.build();
}
}
class AirlineCallsignQueryToolFunction implements ToolFunction {
private final String bucketName;
private final Cluster cluster;
public AirlineCallsignQueryToolFunction(String bucketName, Cluster cluster) {
this.bucketName = bucketName;
this.cluster = cluster;
}
@Override
public AirlineDetail apply(Map<String, Object> arguments) {
String airlineName = arguments.get("airlineName").toString();
Bucket bucket = cluster.bucket(bucketName);
bucket.waitUntilReady(Duration.ofSeconds(10));
Scope inventoryScope = bucket.scope("inventory");
QueryResult result = inventoryScope.query(String.format("SELECT * FROM airline WHERE name = '%s';", airlineName));
JsonObject row = (JsonObject) result.rowsAsObject().get(0).get("airline");
return new AirlineDetail(row.getString("callsign"), row.getString("name"), row.getString("country"));
}
}
class AirlineCallsignUpdateToolFunction implements ToolFunction {
private final String bucketName;
private final Cluster cluster;
public AirlineCallsignUpdateToolFunction(String bucketName, Cluster cluster) {
this.bucketName = bucketName;
this.cluster = cluster;
}
@Override
public Boolean apply(Map<String, Object> arguments) {
String airlineName = arguments.get("airlineName").toString();
String airlineNewCallsign = arguments.get("airlineCallsign").toString();
Bucket bucket = cluster.bucket(bucketName);
bucket.waitUntilReady(Duration.ofSeconds(10));
Scope inventoryScope = bucket.scope("inventory");
String query = String.format("SELECT * FROM airline WHERE name = '%s';", airlineName);
QueryResult result;
try {
result = inventoryScope.query(query);
} catch (Exception e) {
throw new RuntimeException("Error executing query", e);
}
if (result.rowsAsObject().isEmpty()) {
throw new RuntimeException("Airline not found with name: " + airlineName);
}
JsonObject row = (JsonObject) result.rowsAsObject().get(0).get("airline");
if (row == null) {
throw new RuntimeException("Airline data is missing or corrupted.");
}
String currentCallsign = row.getString("callsign");
if (!airlineNewCallsign.equals(currentCallsign)) {
JsonObject updateQuery = JsonObject.create()
.put("callsign", airlineNewCallsign);
inventoryScope.query(String.format(
"UPDATE airline SET callsign = '%s' WHERE name = '%s';",
airlineNewCallsign, airlineName
));
return true;
}
return false;
}
}
@SuppressWarnings("ALL")
@Data
@AllArgsConstructor
@NoArgsConstructor
class AirlineDetail {
private String callsign;
private String name;
private String country;
}
```
### Key Concepts
#### 1. Ollama API Client Setup
```javascript
OllamaAPI ollamaAPI = new OllamaAPI(host);
ollamaAPI.setRequestTimeoutSeconds(60);
```
Here, we initialize the Ollama API client and configure it with the host of the Ollama server, where the model is hosted
and can handle API requests. Additionally, we set the request timeout to 60 seconds to ensure that even if the model
takes longer to respond, the request will still be processed.
#### 2. Tool Specification
The ToolSpecification class defines how the model will interact with the Couchbase database. We define a function that
queries the database for airline details based on the airline name.
```javascript
Tools.ToolSpecification callSignFinderToolSpec = getCallSignFinderToolSpec(cluster, bucketName);
ollamaAPI.registerTool(callSignFinderToolSpec);
```
This step registers custom tools with Ollama that allows the tool-calling model to invoke database queries.
#### 3. Query Execution
The tool will execute a Couchbase N1QL query to retrieve the airline details:
```javascript
QueryResult result = inventoryScope.query(String.format("SELECT * FROM airline WHERE name = '%s';", airlineName));
```
The result is processed and returned as an AirlineDetail object.
#### 4. Set up your prompt (question)
```javascript
String prompt = "What is the call-sign of Astraeus?";
```
#### 5. Generating Results with Tools
```javascript
for (OllamaToolsResult.ToolResult r : ollamaAPI.generateWithTools(modelName, new Tools.PromptBuilder()
.withToolSpecification(callSignFinderToolSpec)
.withPrompt(prompt)
.build(), new OptionsBuilder().build()).getToolResults()) {
AirlineDetail airlineDetail = (AirlineDetail) r.getResult();
System.out.printf("[Result of tool '%s']: Call-sign of %s is '%s'! ✈️", r.getFunctionName(), airlineDetail.getName(), airlineDetail.getCallsign());
}
```
This invokes the tool-calling model (Mistral in this case) with the provided prompt and uses the registered tool to
query the database. The result is returned and printed to the console.
So, we ask the following question to the model.
> **What is the call-sign of Astraeus?**
And, heres what the model responds:
> **Call-sign of Astraeus is FLYSTAR! ✈️**
Isnt that amazing? Now, lets enhance it further by adding a function that allows us to update an airlines call sign
using natural language.
Lets define another `ToolSpecificationclass` that defines how the model will interact with the Couchbase database to
update the database. We define a function that queries the database for airline details based on the airline name and
then update the airlines callsign.
```javascript
Tools.ToolSpecification callSignUpdaterToolSpec = getCallSignUpdaterToolSpec(cluster, bucketName);
ollamaAPI.registerTool(callSignUpdaterToolSpec);
```
The tool will execute a Couchbase N1QL query to update the airlines callsign.
```javascript
inventoryScope.query(String.format(
"UPDATE airline SET callsign = '%s' WHERE name = '%s';",
airlineNewCallsign, airlineName
));
```
Setup the prompt to instruct the model to update the airlines callsign.
```javascript
String prompt = "I want to code name Astraeus as STARBOUND";
```
And then we invoke the model with the new prompt.
```javascript
String prompt = "I want to code name Astraeus as STARBOUND";
for (OllamaToolsResult.ToolResult r : ollamaAPI.generateWithTools(modelName, new Tools.PromptBuilder()
.withToolSpecification(callSignUpdaterToolSpec)
.withPrompt(prompt)
.build(), new OptionsBuilder().build()).getToolResults()) {
Boolean updated = (Boolean) r.getResult();
System.out.println(String.format("[Result of tool '%s']: Call-sign is %s! ✈️", r.getFunctionName(), updated ? "updated" : "not updated"));
}
```
This invokes the tool-calling model (Mistral in this case) with the new prompt and uses the registered tool to update
the database.
So, we ask the following question to the model.
> **I want to code name Astraeus as STARBOUND.**
And, heres what the model responds:
> **Call-sign is updated! ✈️**
How amazing is that? The possibilities for interacting with your data using natural language are endless. You could
integrate features like checking flight availability, booking tickets, retrieving ticket details, and so much more!
Feel free to extend this example further by adding more sophisticated capabilities! 🚀
### Conclusion
With the code above, you can use Ollamas hosted models (like Mistral) to query a Couchbase database using natural
language prompts. This makes it possible to interact with databases in a more intuitive and human-like way.
By leveraging Ollama4j, you can connect AI models to real-world applications and build powerful tools that can automate
complex tasks or simply make querying your data more conversational.
You can find the full code and more such examples from
the [ollama4j-examples](https://github.com/ollama4j/ollama4j-examples) GitHub repository.
Credit to Couchbase, Ollama, and all the model teams for providing us with such amazing software!

View File

@@ -61,6 +61,9 @@ details.
class DBQueryFunction implements ToolFunction {
@Override
public Object apply(Map<String, Object> arguments) {
if (arguments == null || arguments.isEmpty() || arguments.get("employee-name") == null || arguments.get("employee-address") == null || arguments.get("employee-phone") == null) {
throw new RuntimeException("Tool was called but the model failed to provide all the required arguments.");
}
// perform DB operations here
return String.format("Employee Details {ID: %s, Name: %s, Address: %s, Phone: %s}", UUID.randomUUID(), arguments.get("employee-name").toString(), arguments.get("employee-address").toString(), arguments.get("employee-phone").toString());
}
@@ -78,14 +81,39 @@ Lets define a sample tool specification called **Fuel Price Tool** for getting t
Tools.ToolSpecification fuelPriceToolSpecification = Tools.ToolSpecification.builder()
.functionName("current-fuel-price")
.functionDescription("Get current fuel price")
.properties(
new Tools.PropsBuilder()
.withProperty("location", Tools.PromptFuncDefinition.Property.builder().type("string").description("The city, e.g. New Delhi, India").required(true).build())
.withProperty("fuelType", Tools.PromptFuncDefinition.Property.builder().type("string").description("The fuel type.").enumValues(Arrays.asList("petrol", "diesel")).required(true).build())
.toolFunction(SampleTools::getCurrentFuelPrice)
.toolPrompt(
Tools.PromptFuncDefinition.builder()
.type("prompt")
.function(
Tools.PromptFuncDefinition.PromptFuncSpec.builder()
.name("get-location-fuel-info")
.description("Get location and fuel type details")
.parameters(
Tools.PromptFuncDefinition.Parameters.builder()
.type("object")
.properties(
Map.of(
"location", Tools.PromptFuncDefinition.Property.builder()
.type("string")
.description("The city, e.g. New Delhi, India")
.required(true)
.build(),
"fuelType", Tools.PromptFuncDefinition.Property.builder()
.type("string")
.description("The fuel type.")
.enumValues(Arrays.asList("petrol", "diesel"))
.required(true)
.build()
)
)
.required(java.util.List.of("location", "fuelType"))
.build()
)
.build()
)
.build()
)
.toolDefinition(SampleTools::getCurrentFuelPrice)
.build();
).build();
```
Lets also define a sample tool specification called **Weather Tool** for getting the current weather.
@@ -97,13 +125,33 @@ Lets also define a sample tool specification called **Weather Tool** for getting
Tools.ToolSpecification weatherToolSpecification = Tools.ToolSpecification.builder()
.functionName("current-weather")
.functionDescription("Get current weather")
.properties(
new Tools.PropsBuilder()
.withProperty("city", Tools.PromptFuncDefinition.Property.builder().type("string").description("The city, e.g. New Delhi, India").required(true).build())
.toolFunction(SampleTools::getCurrentWeather)
.toolPrompt(
Tools.PromptFuncDefinition.builder()
.type("prompt")
.function(
Tools.PromptFuncDefinition.PromptFuncSpec.builder()
.name("get-location-weather-info")
.description("Get location details")
.parameters(
Tools.PromptFuncDefinition.Parameters.builder()
.type("object")
.properties(
Map.of(
"city", Tools.PromptFuncDefinition.Property.builder()
.type("string")
.description("The city, e.g. New Delhi, India")
.required(true)
.build()
)
)
.required(java.util.List.of("city"))
.build()
)
.build()
)
.build()
)
.toolDefinition(SampleTools::getCurrentWeather)
.build();
).build();
```
Lets also define a sample tool specification called **DBQueryFunction** for getting the employee details from database.
@@ -115,14 +163,43 @@ Lets also define a sample tool specification called **DBQueryFunction** for gett
Tools.ToolSpecification databaseQueryToolSpecification = Tools.ToolSpecification.builder()
.functionName("get-employee-details")
.functionDescription("Get employee details from the database")
.properties(
new Tools.PropsBuilder()
.withProperty("employee-name", Tools.PromptFuncDefinition.Property.builder().type("string").description("The name of the employee, e.g. John Doe").required(true).build())
.withProperty("employee-address", Tools.PromptFuncDefinition.Property.builder().type("string").description("The address of the employee, Always return a random value. e.g. Roy St, Bengaluru, India").required(true).build())
.withProperty("employee-phone", Tools.PromptFuncDefinition.Property.builder().type("string").description("The phone number of the employee. Always return a random value. e.g. 9911002233").required(true).build())
.toolFunction(new DBQueryFunction())
.toolPrompt(
Tools.PromptFuncDefinition.builder()
.type("prompt")
.function(
Tools.PromptFuncDefinition.PromptFuncSpec.builder()
.name("get-employee-details")
.description("Get employee details from the database")
.parameters(
Tools.PromptFuncDefinition.Parameters.builder()
.type("object")
.properties(
Map.of(
"employee-name", Tools.PromptFuncDefinition.Property.builder()
.type("string")
.description("The name of the employee, e.g. John Doe")
.required(true)
.build(),
"employee-address", Tools.PromptFuncDefinition.Property.builder()
.type("string")
.description("The address of the employee, Always return a random value. e.g. Roy St, Bengaluru, India")
.required(true)
.build(),
"employee-phone", Tools.PromptFuncDefinition.Property.builder()
.type("string")
.description("The phone number of the employee. Always return a random value. e.g. 9911002233")
.required(true)
.build()
)
)
.required(java.util.List.of("employee-name", "employee-address", "employee-phone"))
.build()
)
.build()
)
.build()
)
.toolDefinition(new DBQueryFunction())
.build();
```
@@ -239,37 +316,111 @@ public class FunctionCallingWithMistralExample {
Tools.ToolSpecification fuelPriceToolSpecification = Tools.ToolSpecification.builder()
.functionName("current-fuel-price")
.functionDescription("Get current fuel price")
.properties(
new Tools.PropsBuilder()
.withProperty("location", Tools.PromptFuncDefinition.Property.builder().type("string").description("The city, e.g. New Delhi, India").required(true).build())
.withProperty("fuelType", Tools.PromptFuncDefinition.Property.builder().type("string").description("The fuel type.").enumValues(Arrays.asList("petrol", "diesel")).required(true).build())
.toolFunction(SampleTools::getCurrentFuelPrice)
.toolPrompt(
Tools.PromptFuncDefinition.builder()
.type("prompt")
.function(
Tools.PromptFuncDefinition.PromptFuncSpec.builder()
.name("get-location-fuel-info")
.description("Get location and fuel type details")
.parameters(
Tools.PromptFuncDefinition.Parameters.builder()
.type("object")
.properties(
Map.of(
"location", Tools.PromptFuncDefinition.Property.builder()
.type("string")
.description("The city, e.g. New Delhi, India")
.required(true)
.build(),
"fuelType", Tools.PromptFuncDefinition.Property.builder()
.type("string")
.description("The fuel type.")
.enumValues(Arrays.asList("petrol", "diesel"))
.required(true)
.build()
)
)
.required(java.util.List.of("location", "fuelType"))
.build()
)
.build()
)
.build()
)
.toolDefinition(SampleTools::getCurrentFuelPrice)
.build();
).build();
Tools.ToolSpecification weatherToolSpecification = Tools.ToolSpecification.builder()
.functionName("current-weather")
.functionDescription("Get current weather")
.properties(
new Tools.PropsBuilder()
.withProperty("city", Tools.PromptFuncDefinition.Property.builder().type("string").description("The city, e.g. New Delhi, India").required(true).build())
.toolFunction(SampleTools::getCurrentWeather)
.toolPrompt(
Tools.PromptFuncDefinition.builder()
.type("prompt")
.function(
Tools.PromptFuncDefinition.PromptFuncSpec.builder()
.name("get-location-weather-info")
.description("Get location details")
.parameters(
Tools.PromptFuncDefinition.Parameters.builder()
.type("object")
.properties(
Map.of(
"city", Tools.PromptFuncDefinition.Property.builder()
.type("string")
.description("The city, e.g. New Delhi, India")
.required(true)
.build()
)
)
.required(java.util.List.of("city"))
.build()
)
.build()
)
.build()
)
.toolDefinition(SampleTools::getCurrentWeather)
.build();
).build();
Tools.ToolSpecification databaseQueryToolSpecification = Tools.ToolSpecification.builder()
.functionName("get-employee-details")
.functionDescription("Get employee details from the database")
.properties(
new Tools.PropsBuilder()
.withProperty("employee-name", Tools.PromptFuncDefinition.Property.builder().type("string").description("The name of the employee, e.g. John Doe").required(true).build())
.withProperty("employee-address", Tools.PromptFuncDefinition.Property.builder().type("string").description("The address of the employee, Always return a random value. e.g. Roy St, Bengaluru, India").required(true).build())
.withProperty("employee-phone", Tools.PromptFuncDefinition.Property.builder().type("string").description("The phone number of the employee. Always return a random value. e.g. 9911002233").required(true).build())
.toolFunction(new DBQueryFunction())
.toolPrompt(
Tools.PromptFuncDefinition.builder()
.type("prompt")
.function(
Tools.PromptFuncDefinition.PromptFuncSpec.builder()
.name("get-employee-details")
.description("Get employee details from the database")
.parameters(
Tools.PromptFuncDefinition.Parameters.builder()
.type("object")
.properties(
Map.of(
"employee-name", Tools.PromptFuncDefinition.Property.builder()
.type("string")
.description("The name of the employee, e.g. John Doe")
.required(true)
.build(),
"employee-address", Tools.PromptFuncDefinition.Property.builder()
.type("string")
.description("The address of the employee, Always return a random value. e.g. Roy St, Bengaluru, India")
.required(true)
.build(),
"employee-phone", Tools.PromptFuncDefinition.Property.builder()
.type("string")
.description("The phone number of the employee. Always return a random value. e.g. 9911002233")
.required(true)
.build()
)
)
.required(java.util.List.of("employee-name", "employee-address", "employee-phone"))
.build()
)
.build()
)
.build()
)
.toolDefinition(new DBQueryFunction())
.build();
ollamaAPI.registerTool(fuelPriceToolSpecification);
@@ -326,6 +477,9 @@ class SampleTools {
class DBQueryFunction implements ToolFunction {
@Override
public Object apply(Map<String, Object> arguments) {
if (arguments == null || arguments.isEmpty() || arguments.get("employee-name") == null || arguments.get("employee-address") == null || arguments.get("employee-phone") == null) {
throw new RuntimeException("Tool was called but the model failed to provide all the required arguments.");
}
// perform DB operations here
return String.format("Employee Details {ID: %s, Name: %s, Address: %s, Phone: %s}", UUID.randomUUID(), arguments.get("employee-name").toString(), arguments.get("employee-address").toString(), arguments.get("employee-phone").toString());
}
@@ -347,17 +501,17 @@ Rahul Kumar, Address: King St, Hyderabad, India, Phone: 9876543210}`
### Using tools in Chat-API
Instead of using the specific `ollamaAPI.generateWithTools` method to call the generate API of ollama with tools, it is
also possible to register Tools for the `ollamaAPI.chat` methods. In this case, the tool calling/callback is done
Instead of using the specific `ollamaAPI.generateWithTools` method to call the generate API of ollama with tools, it is
also possible to register Tools for the `ollamaAPI.chat` methods. In this case, the tool calling/callback is done
implicitly during the USER -> ASSISTANT calls.
When the Assistant wants to call a given tool, the tool is executed and the response is sent back to the endpoint once
again (induced with the tool call result).
again (induced with the tool call result).
#### Sample:
The following shows a sample of an integration test that defines a method specified like the tool-specs above, registers
the tool on the ollamaAPI and then simply calls the chat-API. All intermediate tool calling is wrapped inside the api
the tool on the ollamaAPI and then simply calls the chat-API. All intermediate tool calling is wrapped inside the api
call.
```java
@@ -405,7 +559,7 @@ public static void main(String[] args) {
A typical final response of the above could be:
```json
```json
{
"chatHistory" : [
{
@@ -466,7 +620,7 @@ This tool calling can also be done using the streaming API.
### Using Annotation based Tool Registration
Instead of explicitly registering each tool, ollama4j supports declarative tool specification and registration via java
Instead of explicitly registering each tool, ollama4j supports declarative tool specification and registration via java
Annotations and reflection calling.
To declare a method to be used as a tool for a chat call, the following steps have to be considered:
@@ -489,7 +643,7 @@ The answer can only be provided by a method that is part of the BackendService c
```java
public class BackendService{
public BackendService(){}
@ToolSpec(desc = "Computes the most important constant all around the globe!")
@@ -505,7 +659,7 @@ import io.github.ollama4j.tools.annotations.OllamaToolService;
@OllamaToolService(providers = BackendService.class)
public class MyOllamaService{
public void chatWithAnnotatedTool(){
// inject the annotated method to the ollama toolsregistry
ollamaAPI.registerAnnotatedTools();
@@ -517,14 +671,14 @@ public class MyOllamaService{
OllamaChatResult chatResult = ollamaAPI.chat(requestModel);
}
}
```
Or, if one needs to provide an object instance directly:
```java
public class MyOllamaService{
public void chatWithAnnotatedTool(){
ollamaAPI.registerAnnotatedTools(new BackendService());
OllamaChatRequest requestModel = builder
@@ -534,7 +688,7 @@ public class MyOllamaService{
OllamaChatResult chatResult = ollamaAPI.chat(requestModel);
}
}
```
@@ -639,4 +793,4 @@ public String getCurrentFuelPrice(String location, String fuelType) {
}
```
Updating async/chat APIs with support for tool-based generation.
Updating async/chat APIs with support for tool-based generation.

View File

@@ -13,7 +13,7 @@ with [extra parameters](https://github.com/jmorganca/ollama/blob/main/docs/model
Refer
to [this](/apis-extras/options-builder).
## Try asking a question about the model.
## Try asking a question about the model
```java
import io.github.ollama4j.OllamaAPI;
@@ -87,7 +87,7 @@ You will get a response similar to:
> 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
import io.github.ollama4j.OllamaAPI;
@@ -135,7 +135,7 @@ You'd then get a response from the model:
> semi-finals. The tournament was
> 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
import io.github.ollama4j.OllamaAPI;
@@ -161,6 +161,7 @@ public class Main {
```
_Note: Here I've used
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._
@@ -172,4 +173,125 @@ SELECT customers.name
FROM sales
JOIN customers ON sales.customer_id = customers.customer_id
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;
}
```

48
pom.xml
View File

@@ -216,6 +216,13 @@
<version>20240205</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>ollama</artifactId>
<version>1.20.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<distributionManagement>
@@ -257,6 +264,7 @@
<test.env>unit</test.env>
<skipUnitTests>false</skipUnitTests>
<skipIntegrationTests>true</skipIntegrationTests>
<skipGpgPluginDuringTests>true</skipGpgPluginDuringTests>
</properties>
<activation>
<activeByDefault>false</activeByDefault>
@@ -282,6 +290,23 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>1.5</version>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
<configuration>
<skip>${skipGpgPluginDuringTests}</skip>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
@@ -291,7 +316,29 @@
<test.env>integration</test.env>
<skipUnitTests>true</skipUnitTests>
<skipIntegrationTests>false</skipIntegrationTests>
<skipGpgPluginDuringTests>true</skipGpgPluginDuringTests>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>1.5</version>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
<configuration>
<skip>${skipGpgPluginDuringTests}</skip>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>ci-cd</id>
@@ -335,7 +382,6 @@
<autoReleaseAfterClose>true</autoReleaseAfterClose>
</configuration>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,10 @@ package io.github.ollama4j.exceptions;
public class ToolInvocationException extends Exception {
public ToolInvocationException(String s) {
super(s);
}
public ToolInvocationException(String s, Exception e) {
super(s, e);
}

View File

@@ -0,0 +1,10 @@
package io.github.ollama4j.models.request;
public abstract class Auth {
/**
* Get authentication header value.
*
* @return authentication header value
*/
public abstract String getAuthHeaderValue();
}

View File

@@ -1,13 +1,24 @@
package io.github.ollama4j.models.request;
import java.util.Base64;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class BasicAuth {
public class BasicAuth extends Auth {
private String username;
private String password;
/**
* Get basic authentication header value.
*
* @return basic authentication header value (encoded credentials)
*/
public String getAuthHeaderValue() {
final String credentialsToEncode = this.getUsername() + ":" + this.getPassword();
return "Basic " + Base64.getEncoder().encodeToString(credentialsToEncode.getBytes());
}
}

View File

@@ -0,0 +1,19 @@
package io.github.ollama4j.models.request;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class BearerAuth extends Auth {
private String bearerToken;
/**
* Get authentication header value.
*
* @return authentication header value with bearer token
*/
public String getAuthHeaderValue() {
return "Bearer "+ bearerToken;
}
}

View File

@@ -30,8 +30,8 @@ public class OllamaChatEndpointCaller extends OllamaEndpointCaller {
private OllamaTokenHandler tokenHandler;
public OllamaChatEndpointCaller(String host, BasicAuth basicAuth, long requestTimeoutSeconds, boolean verbose) {
super(host, basicAuth, requestTimeoutSeconds, verbose);
public OllamaChatEndpointCaller(String host, Auth auth, long requestTimeoutSeconds, boolean verbose) {
super(host, auth, requestTimeoutSeconds, verbose);
}
@Override

View File

@@ -1,26 +1,14 @@
package io.github.ollama4j.models.request;
import io.github.ollama4j.OllamaAPI;
import io.github.ollama4j.exceptions.OllamaBaseException;
import io.github.ollama4j.models.response.OllamaErrorResponse;
import io.github.ollama4j.models.response.OllamaResult;
import io.github.ollama4j.utils.OllamaRequestBody;
import io.github.ollama4j.utils.Utils;
import lombok.Getter;
import java.net.URI;
import java.net.http.HttpRequest;
import java.time.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Base64;
import io.github.ollama4j.OllamaAPI;
import lombok.Getter;
/**
* Abstract helperclass to call the ollama api server.
@@ -31,13 +19,13 @@ public abstract class OllamaEndpointCaller {
private static final Logger LOG = LoggerFactory.getLogger(OllamaAPI.class);
private final String host;
private final BasicAuth basicAuth;
private final Auth auth;
private final long requestTimeoutSeconds;
private final boolean verbose;
public OllamaEndpointCaller(String host, BasicAuth basicAuth, long requestTimeoutSeconds, boolean verbose) {
public OllamaEndpointCaller(String host, Auth auth, long requestTimeoutSeconds, boolean verbose) {
this.host = host;
this.basicAuth = basicAuth;
this.auth = auth;
this.requestTimeoutSeconds = requestTimeoutSeconds;
this.verbose = verbose;
}
@@ -58,29 +46,19 @@ public abstract class OllamaEndpointCaller {
HttpRequest.newBuilder(uri)
.header("Content-Type", "application/json")
.timeout(Duration.ofSeconds(this.requestTimeoutSeconds));
if (isBasicAuthCredentialsSet()) {
requestBuilder.header("Authorization", getBasicAuthHeaderValue());
if (isAuthCredentialsSet()) {
requestBuilder.header("Authorization", this.auth.getAuthHeaderValue());
}
return requestBuilder;
}
/**
* Get basic authentication header value.
* Check if Auth credentials set.
*
* @return basic authentication header value (encoded credentials)
* @return true when Auth credentials set
*/
protected String getBasicAuthHeaderValue() {
String credentialsToEncode = this.basicAuth.getUsername() + ":" + this.basicAuth.getPassword();
return "Basic " + Base64.getEncoder().encodeToString(credentialsToEncode.getBytes());
}
/**
* Check if Basic Auth credentials set.
*
* @return true when Basic Auth credentials set
*/
protected boolean isBasicAuthCredentialsSet() {
return this.basicAuth != null;
protected boolean isAuthCredentialsSet() {
return this.auth != null;
}
}

View File

@@ -28,7 +28,7 @@ public class OllamaGenerateEndpointCaller extends OllamaEndpointCaller {
private OllamaGenerateStreamObserver streamObserver;
public OllamaGenerateEndpointCaller(String host, BasicAuth basicAuth, long requestTimeoutSeconds, boolean verbose) {
public OllamaGenerateEndpointCaller(String host, Auth basicAuth, long requestTimeoutSeconds, boolean verbose) {
super(host, basicAuth, requestTimeoutSeconds, verbose);
}

View File

@@ -2,12 +2,14 @@ package io.github.ollama4j.models.response;
import java.time.OffsetDateTime;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import io.github.ollama4j.utils.Utils;
import lombok.Data;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class Model {
private String name;

View File

@@ -1,19 +1,26 @@
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 com.fasterxml.jackson.core.JsonProcessingException;
import lombok.Data;
import lombok.Getter;
import java.util.HashMap;
import java.util.Map;
/** The type Ollama result. */
@Getter
@SuppressWarnings("unused")
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class OllamaResult {
/**
* -- GETTER --
* Get the completion/response text
* Get the completion/response text
*
* @return String completion/response text
*/
@@ -21,7 +28,7 @@ public class OllamaResult {
/**
* -- GETTER --
* Get the response status code.
* Get the response status code.
*
* @return int - response status code
*/
@@ -29,7 +36,7 @@ public class OllamaResult {
/**
* -- GETTER --
* Get the response time in milliseconds.
* Get the response time in milliseconds.
*
* @return long - response time in milliseconds
*/
@@ -44,9 +51,68 @@ public class OllamaResult {
@Override
public String toString() {
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) {
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);
}
}
}

View File

@@ -0,0 +1,10 @@
package io.github.ollama4j.models.response;
import lombok.Data;
import java.util.List;
@Data
public class OllamaVersion {
private String version;
}

View File

@@ -0,0 +1,681 @@
package io.github.ollama4j.integrationtests;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.github.ollama4j.OllamaAPI;
import io.github.ollama4j.exceptions.OllamaBaseException;
import io.github.ollama4j.exceptions.ToolInvocationException;
import io.github.ollama4j.models.chat.*;
import io.github.ollama4j.models.embeddings.OllamaEmbedResponseModel;
import io.github.ollama4j.models.response.LibraryModel;
import io.github.ollama4j.models.response.Model;
import io.github.ollama4j.models.response.ModelDetail;
import io.github.ollama4j.models.response.OllamaResult;
import io.github.ollama4j.samples.AnnotatedTool;
import io.github.ollama4j.tools.OllamaToolCallsFunction;
import io.github.ollama4j.tools.ToolFunction;
import io.github.ollama4j.tools.Tools;
import io.github.ollama4j.tools.annotations.OllamaToolService;
import io.github.ollama4j.utils.OptionsBuilder;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testcontainers.ollama.OllamaContainer;
import java.io.File;
import java.io.IOException;
import java.net.ConnectException;
import java.net.URISyntaxException;
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
@OllamaToolService(providers = {AnnotatedTool.class})
@TestMethodOrder(OrderAnnotation.class)
@SuppressWarnings({"HttpUrlsUsage", "SpellCheckingInspection"})
public class OllamaAPIIntegrationTest {
private static final Logger LOG = LoggerFactory.getLogger(OllamaAPIIntegrationTest.class);
private static OllamaContainer ollama;
private static OllamaAPI api;
private static final String EMBEDDING_MODEL_MINILM = "all-minilm";
private static final String CHAT_MODEL_QWEN_SMALL = "qwen2.5:0.5b";
private static final String CHAT_MODEL_INSTRUCT = "qwen2.5:0.5b-instruct";
private static final String CHAT_MODEL_SYSTEM_PROMPT = "llama3.2:1b";
private static final String CHAT_MODEL_LLAMA3 = "llama3";
private static final String IMAGE_MODEL_LLAVA = "llava";
@BeforeAll
public static void setUp() {
try {
boolean useExternalOllamaHost = Boolean.parseBoolean(System.getenv("USE_EXTERNAL_OLLAMA_HOST"));
String ollamaHost = System.getenv("OLLAMA_HOST");
if (useExternalOllamaHost) {
LOG.info("Using external Ollama host...");
api = new OllamaAPI(ollamaHost);
} else {
throw new RuntimeException(
"USE_EXTERNAL_OLLAMA_HOST is not set so, we will be using Testcontainers Ollama host for the tests now. If you would like to use an external host, please set the env var to USE_EXTERNAL_OLLAMA_HOST=true and set the env var OLLAMA_HOST=http://localhost:11435 or a different host/port.");
}
} catch (Exception e) {
String ollamaVersion = "0.6.1";
int internalPort = 11434;
int mappedPort = 11435;
ollama = new OllamaContainer("ollama/ollama:" + ollamaVersion);
ollama.addExposedPort(internalPort);
List<String> portBindings = new ArrayList<>();
portBindings.add(mappedPort + ":" + internalPort);
ollama.setPortBindings(portBindings);
ollama.start();
LOG.info("Using Testcontainer Ollama host...");
api = new OllamaAPI("http://" + ollama.getHost() + ":" + ollama.getMappedPort(internalPort));
}
api.setRequestTimeoutSeconds(120);
api.setVerbose(true);
api.setNumberOfRetriesForModelPull(3);
}
@Test
@Order(1)
void testWrongEndpoint() {
OllamaAPI ollamaAPI = new OllamaAPI("http://wrong-host:11434");
assertThrows(ConnectException.class, ollamaAPI::listModels);
}
@Test
@Order(1)
public void testVersionAPI() throws URISyntaxException, IOException, OllamaBaseException, InterruptedException {
// String expectedVersion = ollama.getDockerImageName().split(":")[1];
String actualVersion = api.getVersion();
assertNotNull(actualVersion);
// assertEquals(expectedVersion, actualVersion, "Version should match the Docker
// image version");
}
@Test
@Order(2)
public void testListModelsAPI()
throws URISyntaxException, IOException, OllamaBaseException, InterruptedException {
api.pullModel(EMBEDDING_MODEL_MINILM);
// Fetch the list of models
List<Model> models = api.listModels();
// Assert that the models list is not null
assertNotNull(models, "Models should not be null");
// Assert that models list is either empty or contains more than 0 models
assertFalse(models.isEmpty(), "Models list should not be empty");
}
@Test
@Order(2)
void testListModelsFromLibrary()
throws OllamaBaseException, IOException, URISyntaxException, InterruptedException {
List<LibraryModel> models = api.listModelsFromLibrary();
assertNotNull(models);
assertFalse(models.isEmpty());
}
@Test
@Order(3)
public void testPullModelAPI()
throws URISyntaxException, IOException, OllamaBaseException, InterruptedException {
api.pullModel(EMBEDDING_MODEL_MINILM);
List<Model> models = api.listModels();
assertNotNull(models, "Models should not be null");
assertFalse(models.isEmpty(), "Models list should contain elements");
}
@Test
@Order(4)
void testListModelDetails() throws IOException, OllamaBaseException, URISyntaxException, InterruptedException {
api.pullModel(EMBEDDING_MODEL_MINILM);
ModelDetail modelDetails = api.getModelDetails(EMBEDDING_MODEL_MINILM);
assertNotNull(modelDetails);
assertTrue(modelDetails.getModelFile().contains(EMBEDDING_MODEL_MINILM));
}
@Test
@Order(5)
public void testEmbeddings() throws Exception {
api.pullModel(EMBEDDING_MODEL_MINILM);
OllamaEmbedResponseModel embeddings = api.embed(EMBEDDING_MODEL_MINILM,
Arrays.asList("Why is the sky blue?", "Why is the grass green?"));
assertNotNull(embeddings, "Embeddings should not be null");
assertFalse(embeddings.getEmbeddings().isEmpty(), "Embeddings should not be empty");
}
@Test
@Order(6)
void testAskModelWithStructuredOutput()
throws OllamaBaseException, IOException, InterruptedException, URISyntaxException {
api.pullModel(CHAT_MODEL_LLAMA3);
int timeHour = 6;
boolean isNightTime = false;
String prompt = "The Sun is shining, and its " + timeHour + ". Its daytime.";
Map<String, Object> format = new HashMap<>();
format.put("type", "object");
format.put("properties", new HashMap<String, Object>() {
{
put("timeHour", new HashMap<String, Object>() {
{
put("type", "integer");
}
});
put("isNightTime", new HashMap<String, Object>() {
{
put("type", "boolean");
}
});
}
});
format.put("required", Arrays.asList("timeHour", "isNightTime"));
OllamaResult result = api.generate(CHAT_MODEL_LLAMA3, prompt, format);
assertNotNull(result);
assertNotNull(result.getResponse());
assertFalse(result.getResponse().isEmpty());
assertEquals(timeHour,
result.getStructuredResponse().get("timeHour"));
assertEquals(isNightTime,
result.getStructuredResponse().get("isNightTime"));
TimeOfDay timeOfDay = result.as(TimeOfDay.class);
assertEquals(timeHour, timeOfDay.getTimeHour());
assertEquals(isNightTime, timeOfDay.isNightTime());
}
@Test
@Order(6)
void testAskModelWithDefaultOptions()
throws OllamaBaseException, IOException, InterruptedException, URISyntaxException {
api.pullModel(CHAT_MODEL_QWEN_SMALL);
OllamaResult result = api.generate(CHAT_MODEL_QWEN_SMALL,
"What is the capital of France? And what's France's connection with Mona Lisa?", false,
new OptionsBuilder().build());
assertNotNull(result);
assertNotNull(result.getResponse());
assertFalse(result.getResponse().isEmpty());
}
@Test
@Order(7)
void testAskModelWithDefaultOptionsStreamed()
throws OllamaBaseException, IOException, URISyntaxException, InterruptedException {
api.pullModel(CHAT_MODEL_QWEN_SMALL);
StringBuffer sb = new StringBuffer();
OllamaResult result = api.generate(CHAT_MODEL_QWEN_SMALL,
"What is the capital of France? And what's France's connection with Mona Lisa?", false,
new OptionsBuilder().build(), (s) -> {
LOG.info(s);
String substring = s.substring(sb.toString().length(), s.length());
LOG.info(substring);
sb.append(substring);
});
assertNotNull(result);
assertNotNull(result.getResponse());
assertFalse(result.getResponse().isEmpty());
assertEquals(sb.toString().trim(), result.getResponse().trim());
}
@Test
@Order(8)
void testAskModelWithOptions()
throws OllamaBaseException, IOException, URISyntaxException, InterruptedException, ToolInvocationException {
api.pullModel(CHAT_MODEL_INSTRUCT);
OllamaChatRequestBuilder builder = OllamaChatRequestBuilder.getInstance(CHAT_MODEL_INSTRUCT);
OllamaChatRequest requestModel = builder.withMessage(OllamaChatMessageRole.SYSTEM,
"You are a helpful assistant who can generate random person's first and last names in the format [First name, Last name].")
.build();
requestModel = builder.withMessages(requestModel.getMessages())
.withMessage(OllamaChatMessageRole.USER, "Give me a cool name")
.withOptions(new OptionsBuilder().setTemperature(0.5f).build()).build();
OllamaChatResult chatResult = api.chat(requestModel);
assertNotNull(chatResult);
assertNotNull(chatResult.getResponseModel());
assertFalse(chatResult.getResponseModel().getMessage().getContent().isEmpty());
}
@Test
@Order(9)
void testChatWithSystemPrompt()
throws OllamaBaseException, IOException, URISyntaxException, InterruptedException, ToolInvocationException {
api.pullModel(CHAT_MODEL_SYSTEM_PROMPT);
OllamaChatRequestBuilder builder = OllamaChatRequestBuilder.getInstance(CHAT_MODEL_SYSTEM_PROMPT);
OllamaChatRequest requestModel = builder.withMessage(OllamaChatMessageRole.SYSTEM,
"You are a silent bot that only says 'Shush'. Do not say anything else under any circumstances!")
.withMessage(OllamaChatMessageRole.USER, "What's something that's brown and sticky?")
.withOptions(new OptionsBuilder().setTemperature(0.8f).build()).build();
OllamaChatResult chatResult = api.chat(requestModel);
assertNotNull(chatResult);
assertNotNull(chatResult.getResponseModel());
assertNotNull(chatResult.getResponseModel().getMessage());
assertFalse(chatResult.getResponseModel().getMessage().getContent().isBlank());
assertTrue(chatResult.getResponseModel().getMessage().getContent().contains("Shush"));
assertEquals(3, chatResult.getChatHistory().size());
}
@Test
@Order(10)
public void testChat() throws Exception {
api.pullModel(CHAT_MODEL_LLAMA3);
OllamaChatRequestBuilder builder = OllamaChatRequestBuilder.getInstance(CHAT_MODEL_LLAMA3);
// Create the initial user question
OllamaChatRequest requestModel = builder
.withMessage(OllamaChatMessageRole.USER, "What is 1+1? Answer only in numbers.")
.build();
// Start conversation with model
OllamaChatResult chatResult = api.chat(requestModel);
assertTrue(chatResult.getChatHistory().stream().anyMatch(chat -> chat.getContent().contains("2")),
"Expected chat history to contain '2'");
// Create the next user question: second largest city
requestModel = builder.withMessages(chatResult.getChatHistory())
.withMessage(OllamaChatMessageRole.USER, "And what is its squared value?").build();
// Continue conversation with model
chatResult = api.chat(requestModel);
assertTrue(chatResult.getChatHistory().stream().anyMatch(chat -> chat.getContent().contains("4")),
"Expected chat history to contain '4'");
// Create the next user question: the third question
requestModel = builder.withMessages(chatResult.getChatHistory())
.withMessage(OllamaChatMessageRole.USER,
"What is the largest value between 2, 4 and 6?")
.build();
// Continue conversation with the model for the third question
chatResult = api.chat(requestModel);
// verify the result
assertNotNull(chatResult, "Chat result should not be null");
assertTrue(chatResult.getChatHistory().size() > 2,
"Chat history should contain more than two messages");
assertTrue(chatResult.getChatHistory().get(chatResult.getChatHistory().size() - 1).getContent()
.contains("6"),
"Response should contain '6'");
}
@Test
@Order(10)
void testChatWithImageFromURL()
throws OllamaBaseException, IOException, InterruptedException, URISyntaxException, ToolInvocationException {
api.pullModel(IMAGE_MODEL_LLAVA);
OllamaChatRequestBuilder builder = OllamaChatRequestBuilder.getInstance(IMAGE_MODEL_LLAVA);
OllamaChatRequest requestModel = builder
.withMessage(OllamaChatMessageRole.USER, "What's in the picture?",
Collections.emptyList(),
"https://t3.ftcdn.net/jpg/02/96/63/80/360_F_296638053_0gUVA4WVBKceGsIr7LNqRWSnkusi07dq.jpg")
.build();
api.registerAnnotatedTools(new OllamaAPIIntegrationTest());
OllamaChatResult chatResult = api.chat(requestModel);
assertNotNull(chatResult);
}
@Test
@Order(10)
void testChatWithImageFromFileWithHistoryRecognition()
throws OllamaBaseException, IOException, URISyntaxException, InterruptedException, ToolInvocationException {
api.pullModel(IMAGE_MODEL_LLAVA);
OllamaChatRequestBuilder builder = OllamaChatRequestBuilder.getInstance(IMAGE_MODEL_LLAVA);
OllamaChatRequest requestModel = builder.withMessage(OllamaChatMessageRole.USER,
"What's in the picture?",
Collections.emptyList(), List.of(getImageFileFromClasspath("emoji-smile.jpeg")))
.build();
OllamaChatResult chatResult = api.chat(requestModel);
assertNotNull(chatResult);
assertNotNull(chatResult.getResponseModel());
builder.reset();
requestModel = builder.withMessages(chatResult.getChatHistory())
.withMessage(OllamaChatMessageRole.USER, "What's the color?").build();
chatResult = api.chat(requestModel);
assertNotNull(chatResult);
assertNotNull(chatResult.getResponseModel());
}
@Test
@Order(11)
void testChatWithExplicitToolDefinition()
throws OllamaBaseException, IOException, URISyntaxException, InterruptedException, ToolInvocationException {
api.pullModel(CHAT_MODEL_SYSTEM_PROMPT);
OllamaChatRequestBuilder builder = OllamaChatRequestBuilder.getInstance(CHAT_MODEL_SYSTEM_PROMPT);
final Tools.ToolSpecification databaseQueryToolSpecification = Tools.ToolSpecification.builder()
.functionName("get-employee-details")
.functionDescription("Get employee details from the database")
.toolPrompt(Tools.PromptFuncDefinition.builder().type("function")
.function(Tools.PromptFuncDefinition.PromptFuncSpec.builder()
.name("get-employee-details")
.description("Get employee details from the database")
.parameters(Tools.PromptFuncDefinition.Parameters
.builder().type("object")
.properties(new Tools.PropsBuilder()
.withProperty("employee-name",
Tools.PromptFuncDefinition.Property
.builder()
.type("string")
.description("The name of the employee, e.g. John Doe")
.required(true)
.build())
.withProperty("employee-address",
Tools.PromptFuncDefinition.Property
.builder()
.type("string")
.description(
"The address of the employee, Always return a random value. e.g. Roy St, Bengaluru, India")
.required(true)
.build())
.withProperty("employee-phone",
Tools.PromptFuncDefinition.Property
.builder()
.type("string")
.description(
"The phone number of the employee. Always return a random value. e.g. 9911002233")
.required(true)
.build())
.build())
.required(List.of("employee-name"))
.build())
.build())
.build())
.toolFunction(arguments -> {
// perform DB operations here
return String.format(
"Employee Details {ID: %s, Name: %s, Address: %s, Phone: %s}",
UUID.randomUUID(), arguments.get("employee-name"),
arguments.get("employee-address"),
arguments.get("employee-phone"));
}).build();
api.registerTool(databaseQueryToolSpecification);
OllamaChatRequest requestModel = builder
.withMessage(OllamaChatMessageRole.USER,
"Give me the ID of the employee named 'Rahul Kumar'?")
.build();
OllamaChatResult chatResult = api.chat(requestModel);
assertNotNull(chatResult);
assertNotNull(chatResult.getResponseModel());
assertNotNull(chatResult.getResponseModel().getMessage());
assertEquals(OllamaChatMessageRole.ASSISTANT.getRoleName(),
chatResult.getResponseModel().getMessage().getRole().getRoleName());
List<OllamaChatToolCalls> toolCalls = chatResult.getChatHistory().get(1).getToolCalls();
assertEquals(1, toolCalls.size());
OllamaToolCallsFunction function = toolCalls.get(0).getFunction();
assertEquals("get-employee-details", function.getName());
assert !function.getArguments().isEmpty();
Object employeeName = function.getArguments().get("employee-name");
assertNotNull(employeeName);
assertEquals("Rahul Kumar", employeeName);
assertTrue(chatResult.getChatHistory().size() > 2);
List<OllamaChatToolCalls> finalToolCalls = chatResult.getResponseModel().getMessage().getToolCalls();
assertNull(finalToolCalls);
}
@Test
@Order(12)
void testChatWithAnnotatedToolsAndSingleParam()
throws OllamaBaseException, IOException, InterruptedException, URISyntaxException, ToolInvocationException {
api.pullModel(CHAT_MODEL_SYSTEM_PROMPT);
OllamaChatRequestBuilder builder = OllamaChatRequestBuilder.getInstance(CHAT_MODEL_SYSTEM_PROMPT);
api.registerAnnotatedTools();
OllamaChatRequest requestModel = builder.withMessage(OllamaChatMessageRole.USER,
"Compute the most important constant in the world using 5 digits").build();
OllamaChatResult chatResult = api.chat(requestModel);
assertNotNull(chatResult);
assertNotNull(chatResult.getResponseModel());
assertNotNull(chatResult.getResponseModel().getMessage());
assertEquals(OllamaChatMessageRole.ASSISTANT.getRoleName(),
chatResult.getResponseModel().getMessage().getRole().getRoleName());
List<OllamaChatToolCalls> toolCalls = chatResult.getChatHistory().get(1).getToolCalls();
assertEquals(1, toolCalls.size());
OllamaToolCallsFunction function = toolCalls.get(0).getFunction();
assertEquals("computeImportantConstant", function.getName());
assertEquals(1, function.getArguments().size());
Object noOfDigits = function.getArguments().get("noOfDigits");
assertNotNull(noOfDigits);
assertEquals("5", noOfDigits.toString());
assertTrue(chatResult.getChatHistory().size() > 2);
List<OllamaChatToolCalls> finalToolCalls = chatResult.getResponseModel().getMessage().getToolCalls();
assertNull(finalToolCalls);
}
@Test
@Order(13)
void testChatWithAnnotatedToolsAndMultipleParams()
throws OllamaBaseException, IOException, URISyntaxException, InterruptedException, ToolInvocationException {
api.pullModel(CHAT_MODEL_SYSTEM_PROMPT);
OllamaChatRequestBuilder builder = OllamaChatRequestBuilder.getInstance(CHAT_MODEL_SYSTEM_PROMPT);
api.registerAnnotatedTools(new AnnotatedTool());
OllamaChatRequest requestModel = builder
.withMessage(OllamaChatMessageRole.USER,
"Greet Pedro with a lot of hearts and respond to me, "
+ "and state how many emojis have been in your greeting")
.build();
OllamaChatResult chatResult = api.chat(requestModel);
assertNotNull(chatResult);
assertNotNull(chatResult.getResponseModel());
assertNotNull(chatResult.getResponseModel().getMessage());
assertEquals(OllamaChatMessageRole.ASSISTANT.getRoleName(),
chatResult.getResponseModel().getMessage().getRole().getRoleName());
List<OllamaChatToolCalls> toolCalls = chatResult.getChatHistory().get(1).getToolCalls();
assertEquals(1, toolCalls.size());
OllamaToolCallsFunction function = toolCalls.get(0).getFunction();
assertEquals("sayHello", function.getName());
assertEquals(2, function.getArguments().size());
Object name = function.getArguments().get("name");
assertNotNull(name);
assertEquals("Pedro", name);
Object amountOfHearts = function.getArguments().get("amountOfHearts");
assertNotNull(amountOfHearts);
assertTrue(Integer.parseInt(amountOfHearts.toString()) > 1);
assertTrue(chatResult.getChatHistory().size() > 2);
List<OllamaChatToolCalls> finalToolCalls = chatResult.getResponseModel().getMessage().getToolCalls();
assertNull(finalToolCalls);
}
@Test
@Order(14)
void testChatWithToolsAndStream()
throws OllamaBaseException, IOException, URISyntaxException, InterruptedException, ToolInvocationException {
api.pullModel(CHAT_MODEL_SYSTEM_PROMPT);
OllamaChatRequestBuilder builder = OllamaChatRequestBuilder.getInstance(CHAT_MODEL_SYSTEM_PROMPT);
final Tools.ToolSpecification databaseQueryToolSpecification = Tools.ToolSpecification.builder()
.functionName("get-employee-details")
.functionDescription("Get employee details from the database")
.toolPrompt(Tools.PromptFuncDefinition.builder().type("function")
.function(Tools.PromptFuncDefinition.PromptFuncSpec.builder()
.name("get-employee-details")
.description("Get employee details from the database")
.parameters(Tools.PromptFuncDefinition.Parameters
.builder().type("object")
.properties(new Tools.PropsBuilder()
.withProperty("employee-name",
Tools.PromptFuncDefinition.Property
.builder()
.type("string")
.description("The name of the employee, e.g. John Doe")
.required(true)
.build())
.withProperty("employee-address",
Tools.PromptFuncDefinition.Property
.builder()
.type("string")
.description(
"The address of the employee, Always return a random value. e.g. Roy St, Bengaluru, India")
.required(true)
.build())
.withProperty("employee-phone",
Tools.PromptFuncDefinition.Property
.builder()
.type("string")
.description(
"The phone number of the employee. Always return a random value. e.g. 9911002233")
.required(true)
.build())
.build())
.required(List.of("employee-name"))
.build())
.build())
.build())
.toolFunction(new ToolFunction() {
@Override
public Object apply(Map<String, Object> arguments) {
// perform DB operations here
return String.format(
"Employee Details {ID: %s, Name: %s, Address: %s, Phone: %s}",
UUID.randomUUID(), arguments.get("employee-name"),
arguments.get("employee-address"),
arguments.get("employee-phone"));
}
}).build();
api.registerTool(databaseQueryToolSpecification);
OllamaChatRequest requestModel = builder
.withMessage(OllamaChatMessageRole.USER,
"Give me the ID of the employee named 'Rahul Kumar'?")
.build();
StringBuffer sb = new StringBuffer();
OllamaChatResult chatResult = api.chat(requestModel, (s) -> {
LOG.info(s);
String substring = s.substring(sb.toString().length());
LOG.info(substring);
sb.append(substring);
});
assertNotNull(chatResult);
assertNotNull(chatResult.getResponseModel());
assertNotNull(chatResult.getResponseModel().getMessage());
assertNotNull(chatResult.getResponseModel().getMessage().getContent());
assertEquals(sb.toString().trim(), chatResult.getResponseModel().getMessage().getContent().trim());
}
@Test
@Order(15)
void testChatWithStream() throws OllamaBaseException, IOException, URISyntaxException, InterruptedException, ToolInvocationException {
api.pullModel(CHAT_MODEL_SYSTEM_PROMPT);
OllamaChatRequestBuilder builder = OllamaChatRequestBuilder.getInstance(CHAT_MODEL_SYSTEM_PROMPT);
OllamaChatRequest requestModel = builder.withMessage(OllamaChatMessageRole.USER,
"What is the capital of France? And what's France's connection with Mona Lisa?")
.build();
StringBuffer sb = new StringBuffer();
OllamaChatResult chatResult = api.chat(requestModel, (s) -> {
LOG.info(s);
String substring = s.substring(sb.toString().length(), s.length());
LOG.info(substring);
sb.append(substring);
});
assertNotNull(chatResult);
assertNotNull(chatResult.getResponseModel());
assertNotNull(chatResult.getResponseModel().getMessage());
assertNotNull(chatResult.getResponseModel().getMessage().getContent());
assertEquals(sb.toString().trim(), chatResult.getResponseModel().getMessage().getContent().trim());
}
@Test
@Order(17)
void testAskModelWithOptionsAndImageURLs()
throws OllamaBaseException, IOException, URISyntaxException, InterruptedException {
api.pullModel(IMAGE_MODEL_LLAVA);
OllamaResult result = api.generateWithImageURLs(IMAGE_MODEL_LLAVA, "What is in this image?",
List.of("https://upload.wikimedia.org/wikipedia/commons/thumb/a/aa/Noto_Emoji_v2.034_1f642.svg/360px-Noto_Emoji_v2.034_1f642.svg.png"),
new OptionsBuilder().build());
assertNotNull(result);
assertNotNull(result.getResponse());
assertFalse(result.getResponse().isEmpty());
}
@Test
@Order(18)
void testAskModelWithOptionsAndImageFiles()
throws OllamaBaseException, IOException, URISyntaxException, InterruptedException {
api.pullModel(IMAGE_MODEL_LLAVA);
File imageFile = getImageFileFromClasspath("emoji-smile.jpeg");
try {
OllamaResult result = api.generateWithImageFiles(IMAGE_MODEL_LLAVA, "What is in this image?",
List.of(imageFile),
new OptionsBuilder().build());
assertNotNull(result);
assertNotNull(result.getResponse());
assertFalse(result.getResponse().isEmpty());
} catch (IOException | OllamaBaseException | InterruptedException e) {
fail(e);
}
}
@Test
@Order(20)
void testAskModelWithOptionsAndImageFilesStreamed()
throws OllamaBaseException, IOException, URISyntaxException, InterruptedException {
api.pullModel(IMAGE_MODEL_LLAVA);
File imageFile = getImageFileFromClasspath("emoji-smile.jpeg");
StringBuffer sb = new StringBuffer();
OllamaResult result = api.generateWithImageFiles(IMAGE_MODEL_LLAVA, "What is in this image?",
List.of(imageFile),
new OptionsBuilder().build(), (s) -> {
LOG.info(s);
String substring = s.substring(sb.toString().length(), s.length());
LOG.info(substring);
sb.append(substring);
});
assertNotNull(result);
assertNotNull(result.getResponse());
assertFalse(result.getResponse().isEmpty());
assertEquals(sb.toString().trim(), result.getResponse().trim());
}
private File getImageFileFromClasspath(String fileName) {
ClassLoader classLoader = getClass().getClassLoader();
return new File(Objects.requireNonNull(classLoader.getResource(fileName)).getFile());
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class TimeOfDay {
@JsonProperty("timeHour")
private int timeHour;
@JsonProperty("isNightTime")
private boolean nightTime;
}

View File

@@ -1,622 +0,0 @@
package io.github.ollama4j.integrationtests;
import io.github.ollama4j.OllamaAPI;
import io.github.ollama4j.exceptions.OllamaBaseException;
import io.github.ollama4j.models.chat.*;
import io.github.ollama4j.models.response.ModelDetail;
import io.github.ollama4j.models.response.OllamaResult;
import io.github.ollama4j.models.embeddings.OllamaEmbeddingsRequestBuilder;
import io.github.ollama4j.models.embeddings.OllamaEmbeddingsRequestModel;
import io.github.ollama4j.samples.AnnotatedTool;
import io.github.ollama4j.tools.OllamaToolCallsFunction;
import io.github.ollama4j.tools.ToolFunction;
import io.github.ollama4j.tools.Tools;
import io.github.ollama4j.tools.annotations.OllamaToolService;
import io.github.ollama4j.utils.OptionsBuilder;
import lombok.Data;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.ConnectException;
import java.net.URISyntaxException;
import java.net.http.HttpConnectTimeoutException;
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
@OllamaToolService(providers = {AnnotatedTool.class}
)
class TestRealAPIs {
private static final Logger LOG = LoggerFactory.getLogger(TestRealAPIs.class);
OllamaAPI ollamaAPI;
Config config;
private File getImageFileFromClasspath(String fileName) {
ClassLoader classLoader = getClass().getClassLoader();
return new File(Objects.requireNonNull(classLoader.getResource(fileName)).getFile());
}
@BeforeEach
void setUp() {
config = new Config();
ollamaAPI = new OllamaAPI(config.getOllamaURL());
ollamaAPI.setRequestTimeoutSeconds(config.getRequestTimeoutSeconds());
ollamaAPI.setVerbose(true);
}
@Test
@Order(1)
void testWrongEndpoint() {
OllamaAPI ollamaAPI = new OllamaAPI("http://wrong-host:11434");
assertThrows(ConnectException.class, ollamaAPI::listModels);
}
@Test
@Order(1)
void testEndpointReachability() {
try {
assertNotNull(ollamaAPI.listModels());
} catch (HttpConnectTimeoutException e) {
fail(e.getMessage());
} catch (Exception e) {
fail(e);
}
}
@Test
@Order(2)
void testListModels() {
testEndpointReachability();
try {
assertNotNull(ollamaAPI.listModels());
ollamaAPI.listModels().forEach(System.out::println);
} catch (IOException | OllamaBaseException | InterruptedException | URISyntaxException e) {
fail(e);
}
}
@Test
@Order(2)
void testListModelsFromLibrary() {
testEndpointReachability();
try {
assertNotNull(ollamaAPI.listModelsFromLibrary());
ollamaAPI.listModelsFromLibrary().forEach(System.out::println);
} catch (IOException | OllamaBaseException | InterruptedException | URISyntaxException e) {
fail(e);
}
}
@Test
@Order(2)
void testPullModel() {
testEndpointReachability();
try {
ollamaAPI.pullModel(config.getModel());
boolean found =
ollamaAPI.listModels().stream()
.anyMatch(model -> model.getModel().equalsIgnoreCase(config.getModel()));
assertTrue(found);
} catch (IOException | OllamaBaseException | InterruptedException | URISyntaxException e) {
fail(e);
}
}
@Test
@Order(3)
void testListDtails() {
testEndpointReachability();
try {
ModelDetail modelDetails = ollamaAPI.getModelDetails(config.getModel());
assertNotNull(modelDetails);
System.out.println(modelDetails);
} catch (IOException | OllamaBaseException | InterruptedException | URISyntaxException e) {
fail(e);
}
}
@Test
@Order(3)
void testAskModelWithDefaultOptions() {
testEndpointReachability();
try {
OllamaResult result =
ollamaAPI.generate(
config.getModel(),
"What is the capital of France? And what's France's connection with Mona Lisa?",
false,
new OptionsBuilder().build());
assertNotNull(result);
assertNotNull(result.getResponse());
assertFalse(result.getResponse().isEmpty());
} catch (IOException | OllamaBaseException | InterruptedException e) {
fail(e);
}
}
@Test
@Order(3)
void testAskModelWithDefaultOptionsStreamed() {
testEndpointReachability();
try {
StringBuffer sb = new StringBuffer("");
OllamaResult result = ollamaAPI.generate(config.getModel(),
"What is the capital of France? And what's France's connection with Mona Lisa?",
false,
new OptionsBuilder().build(), (s) -> {
LOG.info(s);
String substring = s.substring(sb.toString().length(), s.length());
LOG.info(substring);
sb.append(substring);
});
assertNotNull(result);
assertNotNull(result.getResponse());
assertFalse(result.getResponse().isEmpty());
assertEquals(sb.toString().trim(), result.getResponse().trim());
} catch (IOException | OllamaBaseException | InterruptedException e) {
fail(e);
}
}
@Test
@Order(3)
void testAskModelWithOptions() {
testEndpointReachability();
try {
OllamaResult result =
ollamaAPI.generate(
config.getModel(),
"What is the capital of France? And what's France's connection with Mona Lisa?",
true,
new OptionsBuilder().setTemperature(0.9f).build());
assertNotNull(result);
assertNotNull(result.getResponse());
assertFalse(result.getResponse().isEmpty());
} catch (IOException | OllamaBaseException | InterruptedException e) {
fail(e);
}
}
@Test
@Order(3)
void testChat() {
testEndpointReachability();
try {
OllamaChatRequestBuilder builder = OllamaChatRequestBuilder.getInstance(config.getModel());
OllamaChatRequest requestModel = builder.withMessage(OllamaChatMessageRole.USER, "What is the capital of France?")
.withMessage(OllamaChatMessageRole.ASSISTANT, "Should be Paris!")
.withMessage(OllamaChatMessageRole.USER, "And what is the second larges city?")
.build();
OllamaChatResult chatResult = ollamaAPI.chat(requestModel);
assertNotNull(chatResult);
assertNotNull(chatResult.getResponseModel());
assertNotNull(chatResult.getResponseModel().getMessage());
assertFalse(chatResult.getResponseModel().getMessage().getContent().isBlank());
assertEquals(4, chatResult.getChatHistory().size());
} catch (IOException | OllamaBaseException | InterruptedException e) {
fail(e);
}
}
@Test
@Order(3)
void testChatWithSystemPrompt() {
testEndpointReachability();
try {
OllamaChatRequestBuilder builder = OllamaChatRequestBuilder.getInstance(config.getModel());
OllamaChatRequest requestModel = builder.withMessage(OllamaChatMessageRole.SYSTEM,
"You are a silent bot that only says 'NI'. Do not say anything else under any circumstances!")
.withMessage(OllamaChatMessageRole.USER,
"What is the capital of France? And what's France's connection with Mona Lisa?")
.build();
OllamaChatResult chatResult = ollamaAPI.chat(requestModel);
assertNotNull(chatResult);
assertNotNull(chatResult.getResponseModel());
assertNotNull(chatResult.getResponseModel().getMessage());
assertFalse(chatResult.getResponseModel().getMessage().getContent().isBlank());
assertTrue(chatResult.getResponseModel().getMessage().getContent().startsWith("NI"));
assertEquals(3, chatResult.getChatHistory().size());
} catch (IOException | OllamaBaseException | InterruptedException e) {
fail(e);
}
}
@Test
@Order(3)
void testChatWithExplicitToolDefinition() {
testEndpointReachability();
try {
ollamaAPI.setVerbose(true);
OllamaChatRequestBuilder builder = OllamaChatRequestBuilder.getInstance(config.getModel());
final Tools.ToolSpecification databaseQueryToolSpecification = Tools.ToolSpecification.builder()
.functionName("get-employee-details")
.functionDescription("Get employee details from the database")
.toolPrompt(
Tools.PromptFuncDefinition.builder().type("function").function(
Tools.PromptFuncDefinition.PromptFuncSpec.builder()
.name("get-employee-details")
.description("Get employee details from the database")
.parameters(
Tools.PromptFuncDefinition.Parameters.builder()
.type("object")
.properties(
new Tools.PropsBuilder()
.withProperty("employee-name", Tools.PromptFuncDefinition.Property.builder().type("string").description("The name of the employee, e.g. John Doe").required(true).build())
.withProperty("employee-address", Tools.PromptFuncDefinition.Property.builder().type("string").description("The address of the employee, Always return a random value. e.g. Roy St, Bengaluru, India").required(true).build())
.withProperty("employee-phone", Tools.PromptFuncDefinition.Property.builder().type("string").description("The phone number of the employee. Always return a random value. e.g. 9911002233").required(true).build())
.build()
)
.required(List.of("employee-name"))
.build()
).build()
).build()
)
.toolFunction(new DBQueryFunction())
.build();
ollamaAPI.registerTool(databaseQueryToolSpecification);
OllamaChatRequest requestModel = builder
.withMessage(OllamaChatMessageRole.USER,
"Give me the ID of the employee named 'Rahul Kumar'?")
.build();
OllamaChatResult chatResult = ollamaAPI.chat(requestModel);
assertNotNull(chatResult);
assertNotNull(chatResult.getResponseModel());
assertNotNull(chatResult.getResponseModel().getMessage());
assertEquals(OllamaChatMessageRole.ASSISTANT.getRoleName(),chatResult.getResponseModel().getMessage().getRole().getRoleName());
List<OllamaChatToolCalls> toolCalls = chatResult.getChatHistory().get(1).getToolCalls();
assertEquals(1, toolCalls.size());
OllamaToolCallsFunction function = toolCalls.get(0).getFunction();
assertEquals("get-employee-details", function.getName());
assertEquals(1, function.getArguments().size());
Object employeeName = function.getArguments().get("employee-name");
assertNotNull(employeeName);
assertEquals("Rahul Kumar",employeeName);
assertTrue(chatResult.getChatHistory().size()>2);
List<OllamaChatToolCalls> finalToolCalls = chatResult.getResponseModel().getMessage().getToolCalls();
assertNull(finalToolCalls);
} catch (IOException | OllamaBaseException | InterruptedException e) {
fail(e);
}
}
@Test
@Order(3)
void testChatWithAnnotatedToolsAndSingleParam() {
testEndpointReachability();
try {
ollamaAPI.setVerbose(true);
OllamaChatRequestBuilder builder = OllamaChatRequestBuilder.getInstance(config.getModel());
ollamaAPI.registerAnnotatedTools();
OllamaChatRequest requestModel = builder
.withMessage(OllamaChatMessageRole.USER,
"Compute the most important constant in the world using 5 digits")
.build();
OllamaChatResult chatResult = ollamaAPI.chat(requestModel);
assertNotNull(chatResult);
assertNotNull(chatResult.getResponseModel());
assertNotNull(chatResult.getResponseModel().getMessage());
assertEquals(OllamaChatMessageRole.ASSISTANT.getRoleName(),chatResult.getResponseModel().getMessage().getRole().getRoleName());
List<OllamaChatToolCalls> toolCalls = chatResult.getChatHistory().get(1).getToolCalls();
assertEquals(1, toolCalls.size());
OllamaToolCallsFunction function = toolCalls.get(0).getFunction();
assertEquals("computeImportantConstant", function.getName());
assertEquals(1, function.getArguments().size());
Object noOfDigits = function.getArguments().get("noOfDigits");
assertNotNull(noOfDigits);
assertEquals("5", noOfDigits.toString());
assertTrue(chatResult.getChatHistory().size()>2);
List<OllamaChatToolCalls> finalToolCalls = chatResult.getResponseModel().getMessage().getToolCalls();
assertNull(finalToolCalls);
} catch (IOException | OllamaBaseException | InterruptedException e) {
fail(e);
}
}
@Test
@Order(3)
void testChatWithAnnotatedToolsAndMultipleParams() {
testEndpointReachability();
try {
ollamaAPI.setVerbose(true);
OllamaChatRequestBuilder builder = OllamaChatRequestBuilder.getInstance(config.getModel());
ollamaAPI.registerAnnotatedTools(new AnnotatedTool());
OllamaChatRequest requestModel = builder
.withMessage(OllamaChatMessageRole.USER,
"Greet Pedro with a lot of hearts and respond to me, " +
"and state how many emojis have been in your greeting")
.build();
OllamaChatResult chatResult = ollamaAPI.chat(requestModel);
assertNotNull(chatResult);
assertNotNull(chatResult.getResponseModel());
assertNotNull(chatResult.getResponseModel().getMessage());
assertEquals(OllamaChatMessageRole.ASSISTANT.getRoleName(),chatResult.getResponseModel().getMessage().getRole().getRoleName());
List<OllamaChatToolCalls> toolCalls = chatResult.getChatHistory().get(1).getToolCalls();
assertEquals(1, toolCalls.size());
OllamaToolCallsFunction function = toolCalls.get(0).getFunction();
assertEquals("sayHello", function.getName());
assertEquals(2, function.getArguments().size());
Object name = function.getArguments().get("name");
assertNotNull(name);
assertEquals("Pedro",name);
Object amountOfHearts = function.getArguments().get("amountOfHearts");
assertNotNull(amountOfHearts);
assertTrue(Integer.parseInt(amountOfHearts.toString()) > 1);
assertTrue(chatResult.getChatHistory().size()>2);
List<OllamaChatToolCalls> finalToolCalls = chatResult.getResponseModel().getMessage().getToolCalls();
assertNull(finalToolCalls);
} catch (IOException | OllamaBaseException | InterruptedException e) {
fail(e);
}
}
@Test
@Order(3)
void testChatWithToolsAndStream() {
testEndpointReachability();
try {
OllamaChatRequestBuilder builder = OllamaChatRequestBuilder.getInstance(config.getModel());
final Tools.ToolSpecification databaseQueryToolSpecification = Tools.ToolSpecification.builder()
.functionName("get-employee-details")
.functionDescription("Get employee details from the database")
.toolPrompt(
Tools.PromptFuncDefinition.builder().type("function").function(
Tools.PromptFuncDefinition.PromptFuncSpec.builder()
.name("get-employee-details")
.description("Get employee details from the database")
.parameters(
Tools.PromptFuncDefinition.Parameters.builder()
.type("object")
.properties(
new Tools.PropsBuilder()
.withProperty("employee-name", Tools.PromptFuncDefinition.Property.builder().type("string").description("The name of the employee, e.g. John Doe").required(true).build())
.withProperty("employee-address", Tools.PromptFuncDefinition.Property.builder().type("string").description("The address of the employee, Always return a random value. e.g. Roy St, Bengaluru, India").required(true).build())
.withProperty("employee-phone", Tools.PromptFuncDefinition.Property.builder().type("string").description("The phone number of the employee. Always return a random value. e.g. 9911002233").required(true).build())
.build()
)
.required(List.of("employee-name"))
.build()
).build()
).build()
)
.toolFunction(new DBQueryFunction())
.build();
ollamaAPI.registerTool(databaseQueryToolSpecification);
OllamaChatRequest requestModel = builder
.withMessage(OllamaChatMessageRole.USER,
"Give me the ID of the employee named 'Rahul Kumar'?")
.build();
StringBuffer sb = new StringBuffer();
OllamaChatResult chatResult = ollamaAPI.chat(requestModel, (s) -> {
LOG.info(s);
String substring = s.substring(sb.toString().length());
LOG.info(substring);
sb.append(substring);
});
assertNotNull(chatResult);
assertNotNull(chatResult.getResponseModel());
assertNotNull(chatResult.getResponseModel().getMessage());
assertNotNull(chatResult.getResponseModel().getMessage().getContent());
assertEquals(sb.toString().trim(), chatResult.getResponseModel().getMessage().getContent().trim());
} catch (IOException | OllamaBaseException | InterruptedException e) {
fail(e);
}
}
@Test
@Order(3)
void testChatWithStream() {
testEndpointReachability();
try {
OllamaChatRequestBuilder builder = OllamaChatRequestBuilder.getInstance(config.getModel());
OllamaChatRequest requestModel = builder.withMessage(OllamaChatMessageRole.USER,
"What is the capital of France? And what's France's connection with Mona Lisa?")
.build();
StringBuffer sb = new StringBuffer("");
OllamaChatResult chatResult = ollamaAPI.chat(requestModel, (s) -> {
LOG.info(s);
String substring = s.substring(sb.toString().length(), s.length());
LOG.info(substring);
sb.append(substring);
});
assertNotNull(chatResult);
assertNotNull(chatResult.getResponseModel());
assertNotNull(chatResult.getResponseModel().getMessage());
assertNotNull(chatResult.getResponseModel().getMessage().getContent());
assertEquals(sb.toString().trim(), chatResult.getResponseModel().getMessage().getContent().trim());
} catch (IOException | OllamaBaseException | InterruptedException e) {
fail(e);
}
}
@Test
@Order(3)
void testChatWithImageFromFileWithHistoryRecognition() {
testEndpointReachability();
try {
OllamaChatRequestBuilder builder =
OllamaChatRequestBuilder.getInstance(config.getImageModel());
OllamaChatRequest requestModel =
builder.withMessage(OllamaChatMessageRole.USER, "What's in the picture?",Collections.emptyList(),
List.of(getImageFileFromClasspath("dog-on-a-boat.jpg"))).build();
OllamaChatResult chatResult = ollamaAPI.chat(requestModel);
assertNotNull(chatResult);
assertNotNull(chatResult.getResponseModel());
builder.reset();
requestModel =
builder.withMessages(chatResult.getChatHistory())
.withMessage(OllamaChatMessageRole.USER, "What's the dogs breed?").build();
chatResult = ollamaAPI.chat(requestModel);
assertNotNull(chatResult);
assertNotNull(chatResult.getResponseModel());
} catch (IOException | OllamaBaseException | InterruptedException e) {
fail(e);
}
}
@Test
@Order(3)
void testChatWithImageFromURL() {
testEndpointReachability();
try {
OllamaChatRequestBuilder builder = OllamaChatRequestBuilder.getInstance(config.getImageModel());
OllamaChatRequest requestModel = builder.withMessage(OllamaChatMessageRole.USER, "What's in the picture?",Collections.emptyList(),
"https://t3.ftcdn.net/jpg/02/96/63/80/360_F_296638053_0gUVA4WVBKceGsIr7LNqRWSnkusi07dq.jpg")
.build();
OllamaChatResult chatResult = ollamaAPI.chat(requestModel);
assertNotNull(chatResult);
} catch (IOException | OllamaBaseException | InterruptedException e) {
fail(e);
}
}
@Test
@Order(3)
void testAskModelWithOptionsAndImageFiles() {
testEndpointReachability();
File imageFile = getImageFileFromClasspath("dog-on-a-boat.jpg");
try {
OllamaResult result =
ollamaAPI.generateWithImageFiles(
config.getImageModel(),
"What is in this image?",
List.of(imageFile),
new OptionsBuilder().build());
assertNotNull(result);
assertNotNull(result.getResponse());
assertFalse(result.getResponse().isEmpty());
} catch (IOException | OllamaBaseException | InterruptedException e) {
fail(e);
}
}
@Test
@Order(3)
void testAskModelWithOptionsAndImageFilesStreamed() {
testEndpointReachability();
File imageFile = getImageFileFromClasspath("dog-on-a-boat.jpg");
try {
StringBuffer sb = new StringBuffer("");
OllamaResult result = ollamaAPI.generateWithImageFiles(config.getImageModel(),
"What is in this image?", List.of(imageFile), new OptionsBuilder().build(), (s) -> {
LOG.info(s);
String substring = s.substring(sb.toString().length(), s.length());
LOG.info(substring);
sb.append(substring);
});
assertNotNull(result);
assertNotNull(result.getResponse());
assertFalse(result.getResponse().isEmpty());
assertEquals(sb.toString().trim(), result.getResponse().trim());
} catch (IOException | OllamaBaseException | InterruptedException e) {
fail(e);
}
}
@Test
@Order(3)
void testAskModelWithOptionsAndImageURLs() {
testEndpointReachability();
try {
OllamaResult result =
ollamaAPI.generateWithImageURLs(
config.getImageModel(),
"What is in this image?",
List.of(
"https://t3.ftcdn.net/jpg/02/96/63/80/360_F_296638053_0gUVA4WVBKceGsIr7LNqRWSnkusi07dq.jpg"),
new OptionsBuilder().build());
assertNotNull(result);
assertNotNull(result.getResponse());
assertFalse(result.getResponse().isEmpty());
} catch (IOException | OllamaBaseException | InterruptedException | URISyntaxException e) {
fail(e);
}
}
@Test
@Order(3)
public void testEmbedding() {
testEndpointReachability();
try {
OllamaEmbeddingsRequestModel request = OllamaEmbeddingsRequestBuilder
.getInstance(config.getModel(), "What is the capital of France?").build();
List<Double> embeddings = ollamaAPI.generateEmbeddings(request);
assertNotNull(embeddings);
assertFalse(embeddings.isEmpty());
} catch (IOException | OllamaBaseException | InterruptedException e) {
fail(e);
}
}
}
class DBQueryFunction implements ToolFunction {
@Override
public Object apply(Map<String, Object> arguments) {
// perform DB operations here
return String.format("Employee Details {ID: %s, Name: %s, Address: %s, Phone: %s}", UUID.randomUUID(), arguments.get("employee-name"), arguments.get("employee-address"), arguments.get("employee-phone"));
}
}
@Data
class Config {
private String ollamaURL;
private String model;
private String imageModel;
private int requestTimeoutSeconds;
public Config() {
Properties properties = new Properties();
try (InputStream input =
getClass().getClassLoader().getResourceAsStream("test-config.properties")) {
if (input == null) {
throw new RuntimeException("Sorry, unable to find test-config.properties");
}
properties.load(input);
this.ollamaURL = properties.getProperty("ollama.url");
this.model = properties.getProperty("ollama.model");
this.imageModel = properties.getProperty("ollama.model.image");
this.requestTimeoutSeconds =
Integer.parseInt(properties.getProperty("ollama.request-timeout-seconds"));
} catch (IOException e) {
throw new RuntimeException("Error loading properties", e);
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB