diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..175ca29 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,10 @@ +# See https://docs.github.com/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners + +# Default owners for everything in the repo +* @amithkoujalgi + +# Example for scoping ownership (uncomment and adjust as teams evolve) +# /docs/ @amithkoujalgi +# /src/ @amithkoujalgi + + diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..35f84f0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,59 @@ +name: Bug report +description: File a bug report +labels: [bug] +assignees: [] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + - type: input + id: version + attributes: + label: ollama4j version + description: e.g., 1.1.0 + placeholder: 1.1.0 + validations: + required: true + - type: input + id: java + attributes: + label: Java version + description: Output of `java -version` + placeholder: 11/17/21 + validations: + required: true + - type: input + id: environment + attributes: + label: Environment + description: OS, build tool, Docker/Testcontainers, etc. + placeholder: macOS 13, Maven 3.9.x, Docker 24.x + - type: textarea + id: what-happened + attributes: + label: What happened? + description: Also tell us what you expected to happen + validations: + required: true + - type: textarea + id: steps + attributes: + label: Steps to reproduce + description: Be as specific as possible + placeholder: | + 1. Setup ... + 2. Run ... + 3. Observe ... + validations: + required: true + - type: textarea + id: logs + attributes: + label: Relevant logs/stack traces + render: shell + - type: textarea + id: additional + attributes: + label: Additional context + diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..fdf6f43 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,6 @@ +blank_issues_enabled: false +contact_links: + - name: Questions / Discussions + url: https://github.com/ollama4j/ollama4j/discussions + about: Ask questions and discuss ideas here + diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..ab5e08a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,31 @@ +name: Feature request +description: Suggest an idea or enhancement +labels: [enhancement] +assignees: [] +body: + - type: markdown + attributes: + value: | + Thanks for suggesting an improvement! + - type: textarea + id: problem + attributes: + label: Is your feature request related to a problem? + description: A clear and concise description of the problem + placeholder: I'm frustrated when... + - type: textarea + id: solution + attributes: + label: Describe the solution you'd like + placeholder: I'd like... + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Describe alternatives you've considered + - type: textarea + id: context + attributes: + label: Additional context + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..06e6892 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,34 @@ +## Description + +Describe what this PR does and why. + +## Type of change + +- [ ] feat: New feature +- [ ] fix: Bug fix +- [ ] docs: Documentation update +- [ ] refactor: Refactoring +- [ ] test: Tests only +- [ ] build/ci: Build or CI changes + +## How has this been tested? + +Explain the testing done. Include commands, screenshots, logs. + +## Checklist + +- [ ] I ran `pre-commit run -a` locally +- [ ] `make build` succeeds locally +- [ ] Unit/integration tests added or updated as needed +- [ ] Docs updated (README/docs site) if user-facing changes +- [ ] PR title follows Conventional Commits + +## Breaking changes + +List any breaking changes and migration notes. + +## Related issues + +Fixes # + + diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 5990d9c..d683b02 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,11 +1,34 @@ # To get started with Dependabot version updates, you'll need to specify which -# package ecosystems to update and where the package manifests are located. -# Please see the documentation for all configuration options: -# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file +## package ecosystems to update and where the package manifests are located. +## Please see the documentation for all configuration options: +## https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file +# +#version: 2 +#updates: +# - package-ecosystem: "" # See documentation for possible values +# directory: "/" # Location of package manifests +# schedule: +# interval: "weekly" + version: 2 updates: - - package-ecosystem: "" # See documentation for possible values - directory: "/" # Location of package manifests + - package-ecosystem: "maven" + directory: "/" schedule: interval: "weekly" + open-pull-requests-limit: 5 + labels: ["dependencies"] + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 + labels: ["dependencies"] + - package-ecosystem: "npm" + directory: "/docs" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 + labels: ["dependencies"] +# diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..127f95b --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,44 @@ +name: CodeQL + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + schedule: + - cron: '0 3 * * 1' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + strategy: + fail-fast: false + matrix: + language: [ 'java', 'javascript' ] + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up JDK + if: matrix.language == 'java' + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '11' + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000..083f4d0 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,30 @@ +name: Pre-commit Check on PR + +on: + pull_request: + types: [opened, reopened, synchronize] + branches: + - main + +#on: +# pull_request: +# branches: [ main ] +# push: +# branches: [ main ] + +jobs: + run: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.x' + - name: Install pre-commit + run: | + python -m pip install --upgrade pip + pip install pre-commit + # - name: Run pre-commit + # run: | + # pre-commit run --all-files --show-diff-on-failure + diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000..ec1c66b --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,33 @@ +name: Mark stale issues and PRs + +on: + schedule: + - cron: '0 2 * * *' + +permissions: + issues: write + pull-requests: write + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v9 + with: + days-before-stale: 60 + days-before-close: 14 + stale-issue-label: 'stale' + stale-pr-label: 'stale' + exempt-issue-labels: 'pinned,security' + exempt-pr-labels: 'pinned,security' + stale-issue-message: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. + close-issue-message: > + Closing this stale issue. Feel free to reopen if this is still relevant. + stale-pr-message: > + This pull request has been automatically marked as stale due to inactivity. + It will be closed if no further activity occurs. + close-pr-message: > + Closing this stale pull request. Please reopen when you're ready to continue. + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..86e0f61 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,125 @@ +## Contributing to Ollama4j + +Thanks for your interest in contributing! This guide explains how to set up your environment, make changes, and submit pull requests. + +### Code of Conduct + +By participating, you agree to abide by our [Code of Conduct](CODE_OF_CONDUCT.md). + +### Quick Start + +Prerequisites: + +- Java 11+ +- Maven 3.8+ +- Docker (required for integration tests) +- Make (for convenience targets) +- pre-commit (for Git hooks) + +Setup: + +```bash +# 1) Fork the repo and clone your fork +git clone https://github.com//ollama4j.git +cd ollama4j + +# 2) Install and enable git hooks +pre-commit install --hook-type pre-commit --hook-type commit-msg + +# 3) Prepare dev environment (installs husk deps/tools if needed) +make dev +``` + +Build and test: + +```bash +# Build +make build + +# Run unit tests +make unit-tests + +# Run integration tests (requires Docker running) +make integration-tests +``` + +If you prefer raw Maven: + +```bash +# Unit tests profile +mvn -P unit-tests clean test + +# Integration tests profile (Docker required) +mvn -P integration-tests -DskipUnitTests=true clean verify +``` + +### Commit Style + +We use Conventional Commits. Commit messages and PR titles should follow: + +``` +(optional scope): + +[optional body] +[optional footer(s)] +``` + +Common types: `feat`, `fix`, `docs`, `refactor`, `test`, `build`, `chore`. + +Commit message formatting is enforced via `commitizen` through `pre-commit` hooks. + +### Pre-commit Hooks + +Before pushing, run: + +```bash +pre-commit run -a +``` + +Hooks will check for merge conflicts, large files, YAML/XML/JSON validity, line endings, and basic formatting. Fix reported issues before opening a PR. + +### Coding Guidelines + +- Target Java 11+; match existing style and formatting. +- Prefer clear, descriptive names over abbreviations. +- Add Javadoc for public APIs and non-obvious logic. +- Include meaningful tests for new features and bug fixes. +- Avoid introducing new dependencies without discussion. + +### Tests + +- Unit tests: place under `src/test/java/**/unittests/`. +- Integration tests: place under `src/test/java/**/integrationtests/` (uses Testcontainers; ensure Docker is running). + +### Documentation + +- Update `README.md`, Javadoc, and `docs/` when you change public APIs or user-facing behavior. +- Add example snippets where useful. Keep API references consistent with the website content when applicable. + +### Pull Requests + +Before opening a PR: + +- Ensure `make build` and all tests pass locally. +- Run `pre-commit run -a` and fix any issues. +- Keep PRs focused and reasonably small. Link related issues (e.g., "Closes #123"). +- Describe the change, rationale, and any trade-offs in the PR description. + +Review process: + +- Maintainers will review for correctness, scope, tests, and docs. +- You may be asked to iterate; please be responsive to comments. + +### Security + +If you discover a security issue, please do not open a public issue. Instead, email the maintainer at `koujalgi.amith@gmail.com` with details. + +### License + +By contributing, you agree that your contributions will be licensed under the project’s [MIT License](LICENSE). + +### Questions and Discussion + +Have questions or ideas? Open a GitHub Discussion or issue. We welcome feedback and proposals! + + diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..0806ce5 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,39 @@ +## Security Policy + +### Supported Versions + +We aim to support the latest released version of `ollama4j` and the most recent minor version prior to it. Older versions may receive fixes on a best-effort basis. + +### Reporting a Vulnerability + +Please do not open public GitHub issues for security vulnerabilities. + +Instead, email the maintainer at: + +``` +koujalgi.amith@gmail.com +``` + +Include as much detail as possible: + +- A clear description of the issue and impact +- Steps to reproduce or proof-of-concept +- Affected version(s) and environment +- Any suggested mitigations or patches + +You should receive an acknowledgement within 72 hours. We will work with you to validate the issue, determine severity, and prepare a fix. + +### Disclosure + +We follow a responsible disclosure process: + +1. Receive and validate report privately. +2. Develop and test a fix. +3. Coordinate a release that includes the fix. +4. Publicly credit the reporter (if desired) in release notes. + +### GPG Signatures + +Releases may be signed as part of our CI pipeline. If verification fails or you have concerns about release integrity, please contact us via the email above. + + diff --git a/docs/blog/2025-03-08-blog/index.md b/docs/blog/2025-03-08-blog/index.md index 5e38c9c..b702f39 100644 --- a/docs/blog/2025-03-08-blog/index.md +++ b/docs/blog/2025-03-08-blog/index.md @@ -373,7 +373,6 @@ public class CouchbaseToolCallingExample { String modelName = Utilities.getFromConfig("tools_model_mistral"); OllamaAPI ollamaAPI = new OllamaAPI(host); - ollamaAPI.setVerbose(false); ollamaAPI.setRequestTimeoutSeconds(60); Tools.ToolSpecification callSignFinderToolSpec = getCallSignFinderToolSpec(cluster, bucketName); diff --git a/docs/docs/apis-extras/verbosity.md b/docs/docs/apis-extras/verbosity.md deleted file mode 100644 index c8809c9..0000000 --- a/docs/docs/apis-extras/verbosity.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -sidebar_position: 1 ---- - -# Set Verbosity - -This API lets you set the verbosity of the Ollama client. - -## Try asking a question about the model. - -```java -import io.github.ollama4j.OllamaAPI; - -public class Main { - - public static void main(String[] args) { - - String host = "http://localhost:11434/"; - - OllamaAPI ollamaAPI = new OllamaAPI(host); - - ollamaAPI.setVerbose(true); - } -} -``` \ No newline at end of file diff --git a/docs/docs/intro.md b/docs/docs/intro.md index 59831c4..23ddb99 100644 --- a/docs/docs/intro.md +++ b/docs/docs/intro.md @@ -139,8 +139,6 @@ public class OllamaAPITest { OllamaAPI ollamaAPI = new OllamaAPI(host); - ollamaAPI.setVerbose(true); - boolean isOllamaServerReachable = ollamaAPI.ping(); System.out.println("Is Ollama server running: " + isOllamaServerReachable); diff --git a/src/main/java/io/github/ollama4j/OllamaAPI.java b/src/main/java/io/github/ollama4j/OllamaAPI.java index 6eedf8c..99f1f7a 100644 --- a/src/main/java/io/github/ollama4j/OllamaAPI.java +++ b/src/main/java/io/github/ollama4j/OllamaAPI.java @@ -1,7 +1,6 @@ package io.github.ollama4j; import com.fasterxml.jackson.core.JsonParseException; -import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import io.github.ollama4j.exceptions.OllamaBaseException; import io.github.ollama4j.exceptions.RoleNotFoundException; @@ -55,7 +54,7 @@ import java.util.stream.Collectors; @SuppressWarnings({"DuplicatedCode", "resource"}) public class OllamaAPI { - private static final Logger logger = LoggerFactory.getLogger(OllamaAPI.class); + private static final Logger LOG = LoggerFactory.getLogger(OllamaAPI.class); private final String host; private Auth auth; @@ -71,16 +70,6 @@ public class OllamaAPI { @Setter private long requestTimeoutSeconds = 10; - /** - * Enables or disables verbose logging of responses. - *

- * If set to {@code true}, the API will log detailed information about requests - * and responses. - * Default is {@code true}. - */ - @Setter - private boolean verbose = true; - /** * The maximum number of retries for tool calls during chat interactions. *

@@ -123,9 +112,7 @@ public class OllamaAPI { } else { this.host = host; } - if (this.verbose) { - logger.info("Ollama API initialized with host: {}", this.host); - } + LOG.info("Ollama API initialized with host: {}", this.host); } /** @@ -463,7 +450,7 @@ public class OllamaAPI { int attempt = currentRetry + 1; if (attempt < maxRetries) { long backoffMillis = baseDelayMillis * (1L << currentRetry); - logger.error("Failed to pull model {}, retrying in {}s... (attempt {}/{})", + LOG.error("Failed to pull model {}, retrying in {}s... (attempt {}/{})", modelName, backoffMillis / 1000, attempt, maxRetries); try { Thread.sleep(backoffMillis); @@ -472,7 +459,7 @@ public class OllamaAPI { throw ie; } } else { - logger.error("Failed to pull model {} after {} attempts, no more retries.", modelName, maxRetries); + LOG.error("Failed to pull model {} after {} attempts, no more retries.", modelName, maxRetries); } } @@ -502,21 +489,19 @@ public class OllamaAPI { } if (modelPullResponse.getStatus() != null) { - if (verbose) { - logger.info("{}: {}", modelName, modelPullResponse.getStatus()); - } + LOG.info("{}: {}", modelName, modelPullResponse.getStatus()); // Check if status is "success" and set success flag to true. if ("success".equalsIgnoreCase(modelPullResponse.getStatus())) { success = true; } } } else { - logger.error("Received null response for model pull."); + LOG.error("Received null response for model pull."); } } } if (!success) { - logger.error("Model pull failed or returned invalid status."); + LOG.error("Model pull failed or returned invalid status."); throw new OllamaBaseException("Model pull failed or returned invalid status."); } if (statusCode != 200) { @@ -625,9 +610,7 @@ public class OllamaAPI { if (responseString.contains("error")) { throw new OllamaBaseException(responseString); } - if (verbose) { - logger.info(responseString); - } + LOG.debug(responseString); } /** @@ -663,9 +646,7 @@ public class OllamaAPI { if (responseString.contains("error")) { throw new OllamaBaseException(responseString); } - if (verbose) { - logger.info(responseString); - } + LOG.debug(responseString); } /** @@ -697,9 +678,7 @@ public class OllamaAPI { if (responseString.contains("error")) { throw new OllamaBaseException(responseString); } - if (verbose) { - logger.info(responseString); - } + LOG.debug(responseString); } /** @@ -967,15 +946,14 @@ public class OllamaAPI { .header(Constants.HttpConstants.HEADER_KEY_CONTENT_TYPE, Constants.HttpConstants.APPLICATION_JSON) .POST(HttpRequest.BodyPublishers.ofString(jsonData)).build(); - if (verbose) { - try { - String prettyJson = Utils.getObjectMapper().writerWithDefaultPrettyPrinter() - .writeValueAsString(Utils.getObjectMapper().readValue(jsonData, Object.class)); - logger.info("Asking model:\n{}", prettyJson); - } catch (Exception e) { - logger.info("Asking model: {}", jsonData); - } + try { + String prettyJson = Utils.getObjectMapper().writerWithDefaultPrettyPrinter() + .writeValueAsString(Utils.getObjectMapper().readValue(jsonData, Object.class)); + LOG.debug("Asking model:\n{}", prettyJson); + } catch (Exception e) { + LOG.debug("Asking model: {}", jsonData); } + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); int statusCode = response.statusCode(); String responseBody = response.body(); @@ -996,15 +974,11 @@ public class OllamaAPI { ollamaResult.setPromptEvalDuration(structuredResult.getPromptEvalDuration()); ollamaResult.setEvalCount(structuredResult.getEvalCount()); ollamaResult.setEvalDuration(structuredResult.getEvalDuration()); - if (verbose) { - logger.info("Model response:\n{}", ollamaResult); - } + LOG.debug("Model response:\n{}", ollamaResult); return ollamaResult; } else { - if (verbose) { - logger.info("Model response:\n{}", - Utils.getObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(responseBody)); - } + LOG.debug("Model response:\n{}", + Utils.getObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(responseBody)); throw new OllamaBaseException(statusCode + " - " + responseBody); } } @@ -1055,9 +1029,9 @@ public class OllamaAPI { if (!toolsResponse.isEmpty()) { try { // Try to parse the string to see if it's a valid JSON - JsonNode jsonNode = objectMapper.readTree(toolsResponse); + objectMapper.readTree(toolsResponse); } catch (JsonParseException e) { - logger.warn("Response from model does not contain any tool calls. Returning the response as is."); + LOG.warn("Response from model does not contain any tool calls. Returning the response as is."); return toolResult; } toolFunctionCallSpecs = objectMapper.readValue(toolsResponse, @@ -1361,8 +1335,7 @@ public class OllamaAPI { */ public OllamaChatResult chatStreaming(OllamaChatRequest request, OllamaTokenHandler tokenHandler) throws OllamaBaseException, IOException, InterruptedException, ToolInvocationException { - OllamaChatEndpointCaller requestCaller = new OllamaChatEndpointCaller(host, auth, requestTimeoutSeconds, - verbose); + OllamaChatEndpointCaller requestCaller = new OllamaChatEndpointCaller(host, auth, requestTimeoutSeconds); OllamaChatResult result; // add all registered tools to Request @@ -1417,9 +1390,7 @@ public class OllamaAPI { */ public void registerTool(Tools.ToolSpecification toolSpecification) { toolRegistry.addTool(toolSpecification.getFunctionName(), toolSpecification); - if (this.verbose) { - logger.debug("Registered tool: {}", toolSpecification.getFunctionName()); - } + LOG.debug("Registered tool: {}", toolSpecification.getFunctionName()); } /** @@ -1444,9 +1415,7 @@ public class OllamaAPI { */ public void deregisterTools() { toolRegistry.clear(); - if (this.verbose) { - logger.debug("All tools have been deregistered."); - } + LOG.debug("All tools have been deregistered."); } /** @@ -1621,8 +1590,7 @@ public class OllamaAPI { private OllamaResult generateSyncForOllamaRequestModel(OllamaGenerateRequest ollamaRequestModel, OllamaStreamHandler thinkingStreamHandler, OllamaStreamHandler responseStreamHandler) throws OllamaBaseException, IOException, InterruptedException { - OllamaGenerateEndpointCaller requestCaller = new OllamaGenerateEndpointCaller(host, auth, requestTimeoutSeconds, - verbose); + OllamaGenerateEndpointCaller requestCaller = new OllamaGenerateEndpointCaller(host, auth, requestTimeoutSeconds); OllamaResult result; if (responseStreamHandler != null) { ollamaRequestModel.setStream(true); @@ -1663,9 +1631,7 @@ public class OllamaAPI { String methodName = toolFunctionCallSpec.getName(); Map arguments = toolFunctionCallSpec.getArguments(); ToolFunction function = toolRegistry.getToolFunction(methodName); - if (verbose) { - logger.debug("Invoking function {} with arguments {}", methodName, arguments); - } + LOG.debug("Invoking function {} with arguments {}", methodName, arguments); if (function == null) { throw new ToolNotFoundException( "No such tool: " + methodName + ". Please register the tool before invoking it."); diff --git a/src/main/java/io/github/ollama4j/impl/ConsoleOutputStreamHandler.java b/src/main/java/io/github/ollama4j/impl/ConsoleOutputStreamHandler.java index d990006..b5b3da8 100644 --- a/src/main/java/io/github/ollama4j/impl/ConsoleOutputStreamHandler.java +++ b/src/main/java/io/github/ollama4j/impl/ConsoleOutputStreamHandler.java @@ -1,10 +1,14 @@ package io.github.ollama4j.impl; import io.github.ollama4j.models.generate.OllamaStreamHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class ConsoleOutputStreamHandler implements OllamaStreamHandler { + private static final Logger LOG = LoggerFactory.getLogger(ConsoleOutputStreamHandler.class); + @Override public void accept(String message) { - System.out.print(message); + LOG.info(message); } } diff --git a/src/main/java/io/github/ollama4j/models/request/OllamaChatEndpointCaller.java b/src/main/java/io/github/ollama4j/models/request/OllamaChatEndpointCaller.java index 724e028..49b4a28 100644 --- a/src/main/java/io/github/ollama4j/models/request/OllamaChatEndpointCaller.java +++ b/src/main/java/io/github/ollama4j/models/request/OllamaChatEndpointCaller.java @@ -31,8 +31,8 @@ public class OllamaChatEndpointCaller extends OllamaEndpointCaller { private OllamaTokenHandler tokenHandler; - public OllamaChatEndpointCaller(String host, Auth auth, long requestTimeoutSeconds, boolean verbose) { - super(host, auth, requestTimeoutSeconds, verbose); + public OllamaChatEndpointCaller(String host, Auth auth, long requestTimeoutSeconds) { + super(host, auth, requestTimeoutSeconds); } @Override @@ -91,7 +91,7 @@ public class OllamaChatEndpointCaller extends OllamaEndpointCaller { .POST( body.getBodyPublisher()); HttpRequest request = requestBuilder.build(); - if (isVerbose()) LOG.info("Asking model: {}", body); + LOG.debug("Asking model: {}", body); HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream()); @@ -150,7 +150,7 @@ public class OllamaChatEndpointCaller extends OllamaEndpointCaller { } OllamaChatResult ollamaResult = new OllamaChatResult(ollamaChatResponseModel, body.getMessages()); - if (isVerbose()) LOG.info("Model response: " + ollamaResult); + LOG.debug("Model response: {}", ollamaResult); return ollamaResult; } } diff --git a/src/main/java/io/github/ollama4j/models/request/OllamaEndpointCaller.java b/src/main/java/io/github/ollama4j/models/request/OllamaEndpointCaller.java index c7bdba0..50247ae 100644 --- a/src/main/java/io/github/ollama4j/models/request/OllamaEndpointCaller.java +++ b/src/main/java/io/github/ollama4j/models/request/OllamaEndpointCaller.java @@ -1,10 +1,7 @@ package io.github.ollama4j.models.request; -import io.github.ollama4j.OllamaAPI; import io.github.ollama4j.utils.Constants; import lombok.Getter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.net.URI; import java.net.http.HttpRequest; @@ -16,18 +13,14 @@ import java.time.Duration; @Getter public abstract class OllamaEndpointCaller { - private static final Logger LOG = LoggerFactory.getLogger(OllamaAPI.class); - private final String host; private final Auth auth; private final long requestTimeoutSeconds; - private final boolean verbose; - public OllamaEndpointCaller(String host, Auth auth, long requestTimeoutSeconds, boolean verbose) { + public OllamaEndpointCaller(String host, Auth auth, long requestTimeoutSeconds) { this.host = host; this.auth = auth; this.requestTimeoutSeconds = requestTimeoutSeconds; - this.verbose = verbose; } protected abstract String getEndpointSuffix(); diff --git a/src/main/java/io/github/ollama4j/models/request/OllamaGenerateEndpointCaller.java b/src/main/java/io/github/ollama4j/models/request/OllamaGenerateEndpointCaller.java index a63a384..2c70f62 100644 --- a/src/main/java/io/github/ollama4j/models/request/OllamaGenerateEndpointCaller.java +++ b/src/main/java/io/github/ollama4j/models/request/OllamaGenerateEndpointCaller.java @@ -29,8 +29,8 @@ public class OllamaGenerateEndpointCaller extends OllamaEndpointCaller { private OllamaGenerateStreamObserver responseStreamObserver; - public OllamaGenerateEndpointCaller(String host, Auth basicAuth, long requestTimeoutSeconds, boolean verbose) { - super(host, basicAuth, requestTimeoutSeconds, verbose); + public OllamaGenerateEndpointCaller(String host, Auth basicAuth, long requestTimeoutSeconds) { + super(host, basicAuth, requestTimeoutSeconds); } @Override @@ -80,7 +80,7 @@ public class OllamaGenerateEndpointCaller extends OllamaEndpointCaller { URI uri = URI.create(getHost() + getEndpointSuffix()); HttpRequest.Builder requestBuilder = getRequestBuilderDefault(uri).POST(body.getBodyPublisher()); HttpRequest request = requestBuilder.build(); - if (isVerbose()) LOG.info("Asking model: {}", body); + LOG.debug("Asking model: {}", body); HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream()); int statusCode = response.statusCode(); @@ -132,7 +132,7 @@ public class OllamaGenerateEndpointCaller extends OllamaEndpointCaller { ollamaResult.setEvalCount(ollamaGenerateResponseModel.getEvalCount()); ollamaResult.setEvalDuration(ollamaGenerateResponseModel.getEvalDuration()); - if (isVerbose()) LOG.info("Model response: {}", ollamaResult); + LOG.debug("Model response: {}", ollamaResult); return ollamaResult; } } diff --git a/src/test/java/io/github/ollama4j/integrationtests/OllamaAPIIntegrationTest.java b/src/test/java/io/github/ollama4j/integrationtests/OllamaAPIIntegrationTest.java index 497fe9c..3b3b74a 100644 --- a/src/test/java/io/github/ollama4j/integrationtests/OllamaAPIIntegrationTest.java +++ b/src/test/java/io/github/ollama4j/integrationtests/OllamaAPIIntegrationTest.java @@ -75,7 +75,6 @@ class OllamaAPIIntegrationTest { api = new OllamaAPI("http://" + ollama.getHost() + ":" + ollama.getMappedPort(internalPort)); } api.setRequestTimeoutSeconds(120); - api.setVerbose(true); api.setNumberOfRetriesForModelPull(5); } diff --git a/src/test/java/io/github/ollama4j/integrationtests/WithAuth.java b/src/test/java/io/github/ollama4j/integrationtests/WithAuth.java index b349ce3..821a23e 100644 --- a/src/test/java/io/github/ollama4j/integrationtests/WithAuth.java +++ b/src/test/java/io/github/ollama4j/integrationtests/WithAuth.java @@ -61,7 +61,6 @@ public class WithAuth { api = new OllamaAPI("http://" + nginx.getHost() + ":" + nginx.getMappedPort(NGINX_PORT)); api.setRequestTimeoutSeconds(120); - api.setVerbose(true); api.setNumberOfRetriesForModelPull(3); String ollamaUrl = "http://" + ollama.getHost() + ":" + ollama.getMappedPort(OLLAMA_INTERNAL_PORT); diff --git a/src/test/java/io/github/ollama4j/unittests/TestAnnotations.java b/src/test/java/io/github/ollama4j/unittests/TestAnnotations.java new file mode 100644 index 0000000..6f2d18c --- /dev/null +++ b/src/test/java/io/github/ollama4j/unittests/TestAnnotations.java @@ -0,0 +1,50 @@ +package io.github.ollama4j.unittests; + +import io.github.ollama4j.tools.annotations.OllamaToolService; +import io.github.ollama4j.tools.annotations.ToolProperty; +import io.github.ollama4j.tools.annotations.ToolSpec; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; + +import static org.junit.jupiter.api.Assertions.*; + +class TestAnnotations { + + @OllamaToolService(providers = {SampleProvider.class}) + static class SampleToolService { + } + + static class SampleProvider { + @ToolSpec(name = "sum", desc = "adds two numbers") + public int sum(@ToolProperty(name = "a", desc = "first addend") int a, + @ToolProperty(name = "b", desc = "second addend", required = false) int b) { + return a + b; + } + } + + @Test + void testOllamaToolServiceProvidersPresent() throws Exception { + OllamaToolService ann = SampleToolService.class.getAnnotation(OllamaToolService.class); + assertNotNull(ann); + assertArrayEquals(new Class[]{SampleProvider.class}, ann.providers()); + } + + @Test + void testToolPropertyMetadataOnParameters() throws Exception { + Method m = SampleProvider.class.getDeclaredMethod("sum", int.class, int.class); + Parameter[] params = m.getParameters(); + ToolProperty p0 = params[0].getAnnotation(ToolProperty.class); + ToolProperty p1 = params[1].getAnnotation(ToolProperty.class); + assertNotNull(p0); + assertEquals("a", p0.name()); + assertEquals("first addend", p0.desc()); + assertTrue(p0.required()); + + assertNotNull(p1); + assertEquals("b", p1.name()); + assertEquals("second addend", p1.desc()); + assertFalse(p1.required()); + } +} diff --git a/src/test/java/io/github/ollama4j/unittests/TestAuth.java b/src/test/java/io/github/ollama4j/unittests/TestAuth.java new file mode 100644 index 0000000..b618b51 --- /dev/null +++ b/src/test/java/io/github/ollama4j/unittests/TestAuth.java @@ -0,0 +1,26 @@ +package io.github.ollama4j.unittests; + +import io.github.ollama4j.models.request.BasicAuth; +import io.github.ollama4j.models.request.BearerAuth; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class TestAuth { + + @Test + void testBasicAuthHeaderEncoding() { + BasicAuth auth = new BasicAuth("alice", "s3cr3t"); + String header = auth.getAuthHeaderValue(); + assertTrue(header.startsWith("Basic ")); + // "alice:s3cr3t" base64 is "YWxpY2U6czNjcjN0" + assertEquals("Basic YWxpY2U6czNjcjN0", header); + } + + @Test + void testBearerAuthHeaderFormat() { + BearerAuth auth = new BearerAuth("abc.def.ghi"); + String header = auth.getAuthHeaderValue(); + assertEquals("Bearer abc.def.ghi", header); + } +} diff --git a/src/test/java/io/github/ollama4j/unittests/TestBooleanToJsonFormatFlagSerializer.java b/src/test/java/io/github/ollama4j/unittests/TestBooleanToJsonFormatFlagSerializer.java new file mode 100644 index 0000000..7aeb915 --- /dev/null +++ b/src/test/java/io/github/ollama4j/unittests/TestBooleanToJsonFormatFlagSerializer.java @@ -0,0 +1,43 @@ +package io.github.ollama4j.unittests; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import io.github.ollama4j.utils.BooleanToJsonFormatFlagSerializer; +import io.github.ollama4j.utils.Utils; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class TestBooleanToJsonFormatFlagSerializer { + + static class Holder { + @JsonSerialize(using = BooleanToJsonFormatFlagSerializer.class) + public Boolean formatJson; + } + + @Test + void testSerializeTrueWritesJsonString() throws JsonProcessingException { + ObjectMapper mapper = Utils.getObjectMapper().copy(); + mapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY); + + Holder holder = new Holder(); + holder.formatJson = true; + + String json = mapper.writeValueAsString(holder); + assertEquals("{\"formatJson\":\"json\"}", json); + } + + @Test + void testSerializeFalseOmittedByIsEmpty() throws JsonProcessingException { + ObjectMapper mapper = Utils.getObjectMapper().copy(); + mapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY); + + Holder holder = new Holder(); + holder.formatJson = false; + + String json = mapper.writeValueAsString(holder); + assertEquals("{}", json); + } +} diff --git a/src/test/java/io/github/ollama4j/unittests/TestFileToBase64Serializer.java b/src/test/java/io/github/ollama4j/unittests/TestFileToBase64Serializer.java new file mode 100644 index 0000000..15b2298 --- /dev/null +++ b/src/test/java/io/github/ollama4j/unittests/TestFileToBase64Serializer.java @@ -0,0 +1,32 @@ +package io.github.ollama4j.unittests; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import io.github.ollama4j.utils.FileToBase64Serializer; +import io.github.ollama4j.utils.Utils; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class TestFileToBase64Serializer { + + static class Holder { + @JsonSerialize(using = FileToBase64Serializer.class) + public List images; + } + + @Test + public void testSerializeByteArraysToBase64Array() throws JsonProcessingException { + ObjectMapper mapper = Utils.getObjectMapper(); + + Holder holder = new Holder(); + holder.images = List.of("hello".getBytes(), "world".getBytes()); + + String json = mapper.writeValueAsString(holder); + // Base64 of "hello" = aGVsbG8=, of "world" = d29ybGQ= + assertEquals("{\"images\":[\"aGVsbG8=\",\"d29ybGQ=\"]}", json); + } +} diff --git a/src/test/java/io/github/ollama4j/unittests/TestOllamaChatMessage.java b/src/test/java/io/github/ollama4j/unittests/TestOllamaChatMessage.java new file mode 100644 index 0000000..8e2bab6 --- /dev/null +++ b/src/test/java/io/github/ollama4j/unittests/TestOllamaChatMessage.java @@ -0,0 +1,22 @@ +package io.github.ollama4j.unittests; + +import io.github.ollama4j.models.chat.OllamaChatMessage; +import io.github.ollama4j.models.chat.OllamaChatMessageRole; +import org.json.JSONObject; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class TestOllamaChatMessage { + + @Test + void testToStringProducesJson() { + OllamaChatMessage msg = new OllamaChatMessage(OllamaChatMessageRole.USER, "hello", null, null, null); + String json = msg.toString(); + JSONObject obj = new JSONObject(json); + assertEquals("user", obj.getString("role")); + assertEquals("hello", obj.getString("content")); + assertTrue(obj.has("tool_calls")); + // thinking and images may or may not be present depending on null handling, just ensure no exception + } +} diff --git a/src/test/java/io/github/ollama4j/unittests/TestOllamaChatMessageRole.java b/src/test/java/io/github/ollama4j/unittests/TestOllamaChatMessageRole.java new file mode 100644 index 0000000..6bdbc03 --- /dev/null +++ b/src/test/java/io/github/ollama4j/unittests/TestOllamaChatMessageRole.java @@ -0,0 +1,44 @@ +package io.github.ollama4j.unittests; + +import io.github.ollama4j.exceptions.RoleNotFoundException; +import io.github.ollama4j.models.chat.OllamaChatMessageRole; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class TestOllamaChatMessageRole { + + @Test + void testStaticRolesRegistered() throws Exception { + List roles = OllamaChatMessageRole.getRoles(); + assertTrue(roles.contains(OllamaChatMessageRole.SYSTEM)); + assertTrue(roles.contains(OllamaChatMessageRole.USER)); + assertTrue(roles.contains(OllamaChatMessageRole.ASSISTANT)); + assertTrue(roles.contains(OllamaChatMessageRole.TOOL)); + + assertEquals("system", OllamaChatMessageRole.SYSTEM.toString()); + assertEquals("user", OllamaChatMessageRole.USER.toString()); + assertEquals("assistant", OllamaChatMessageRole.ASSISTANT.toString()); + assertEquals("tool", OllamaChatMessageRole.TOOL.toString()); + + assertSame(OllamaChatMessageRole.SYSTEM, OllamaChatMessageRole.getRole("system")); + assertSame(OllamaChatMessageRole.USER, OllamaChatMessageRole.getRole("user")); + assertSame(OllamaChatMessageRole.ASSISTANT, OllamaChatMessageRole.getRole("assistant")); + assertSame(OllamaChatMessageRole.TOOL, OllamaChatMessageRole.getRole("tool")); + } + + @Test + void testCustomRoleCreationAndLookup() throws Exception { + OllamaChatMessageRole custom = OllamaChatMessageRole.newCustomRole("myrole"); + assertEquals("myrole", custom.toString()); + // custom roles are registered globally (per current implementation), so lookup should succeed + assertSame(custom, OllamaChatMessageRole.getRole("myrole")); + } + + @Test + void testGetRoleThrowsOnUnknown() { + assertThrows(RoleNotFoundException.class, () -> OllamaChatMessageRole.getRole("does-not-exist")); + } +} diff --git a/src/test/java/io/github/ollama4j/unittests/TestOllamaChatRequestBuilder.java b/src/test/java/io/github/ollama4j/unittests/TestOllamaChatRequestBuilder.java new file mode 100644 index 0000000..20ab81c --- /dev/null +++ b/src/test/java/io/github/ollama4j/unittests/TestOllamaChatRequestBuilder.java @@ -0,0 +1,49 @@ +package io.github.ollama4j.unittests; + +import io.github.ollama4j.models.chat.OllamaChatMessage; +import io.github.ollama4j.models.chat.OllamaChatMessageRole; +import io.github.ollama4j.models.chat.OllamaChatRequest; +import io.github.ollama4j.models.chat.OllamaChatRequestBuilder; +import org.junit.jupiter.api.Test; + +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.*; + +class TestOllamaChatRequestBuilder { + + @Test + void testResetClearsMessagesButKeepsModelAndThink() { + OllamaChatRequestBuilder builder = OllamaChatRequestBuilder.getInstance("my-model") + .withThinking(true) + .withMessage(OllamaChatMessageRole.USER, "first"); + + OllamaChatRequest beforeReset = builder.build(); + assertEquals("my-model", beforeReset.getModel()); + assertTrue(beforeReset.isThink()); + assertEquals(1, beforeReset.getMessages().size()); + + builder.reset(); + OllamaChatRequest afterReset = builder.build(); + assertEquals("my-model", afterReset.getModel()); + assertTrue(afterReset.isThink()); + assertNotNull(afterReset.getMessages()); + assertEquals(0, afterReset.getMessages().size()); + } + + @Test + void testImageUrlFailuresAreIgnoredAndDoNotBreakBuild() { + // Provide clearly invalid URL, builder logs a warning and continues + OllamaChatRequest req = OllamaChatRequestBuilder.getInstance("m") + .withMessage(OllamaChatMessageRole.USER, "hi", Collections.emptyList(), + "ht!tp://invalid url \n not a uri") + .build(); + + assertNotNull(req.getMessages()); + assertEquals(1, req.getMessages().size()); + OllamaChatMessage msg = req.getMessages().get(0); + // images list will be initialized only if any valid URL was added; for invalid URL list can be null + // We just assert that builder didn't crash and message is present with content + assertEquals("hi", msg.getContent()); + } +} diff --git a/src/test/java/io/github/ollama4j/unittests/TestOllamaRequestBody.java b/src/test/java/io/github/ollama4j/unittests/TestOllamaRequestBody.java new file mode 100644 index 0000000..204e1bc --- /dev/null +++ b/src/test/java/io/github/ollama4j/unittests/TestOllamaRequestBody.java @@ -0,0 +1,58 @@ +package io.github.ollama4j.unittests; + +import io.github.ollama4j.utils.OllamaRequestBody; +import io.github.ollama4j.utils.Utils; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.Flow; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class TestOllamaRequestBody { + + static class SimpleRequest implements OllamaRequestBody { + public String name; + public int value; + + SimpleRequest(String name, int value) { + this.name = name; + this.value = value; + } + } + + @Test + void testGetBodyPublisherProducesSerializedJson() throws IOException { + SimpleRequest req = new SimpleRequest("abc", 123); + + var publisher = req.getBodyPublisher(); + + StringBuilder data = new StringBuilder(); + publisher.subscribe(new Flow.Subscriber<>() { + @Override + public void onSubscribe(Flow.Subscription subscription) { + subscription.request(Long.MAX_VALUE); + } + + @Override + public void onNext(ByteBuffer item) { + data.append(StandardCharsets.UTF_8.decode(item)); + } + + @Override + public void onError(Throwable throwable) { + } + + @Override + public void onComplete() { + } + }); + + // Trigger the publishing by converting it to a string via the same mapper for determinism + String expected = Utils.getObjectMapper().writeValueAsString(req); + // Due to asynchronous nature, expected content already delivered synchronously by StringPublisher + assertEquals(expected, data.toString()); + } +} diff --git a/src/test/java/io/github/ollama4j/unittests/TestOllamaToolsResult.java b/src/test/java/io/github/ollama4j/unittests/TestOllamaToolsResult.java new file mode 100644 index 0000000..5ff36be --- /dev/null +++ b/src/test/java/io/github/ollama4j/unittests/TestOllamaToolsResult.java @@ -0,0 +1,46 @@ +package io.github.ollama4j.unittests; + +import io.github.ollama4j.models.response.OllamaResult; +import io.github.ollama4j.tools.OllamaToolsResult; +import io.github.ollama4j.tools.ToolFunctionCallSpec; +import org.junit.jupiter.api.Test; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +public class TestOllamaToolsResult { + + @Test + public void testGetToolResultsTransformsMapToList() { + ToolFunctionCallSpec spec1 = new ToolFunctionCallSpec("fn1", Map.of("a", 1)); + ToolFunctionCallSpec spec2 = new ToolFunctionCallSpec("fn2", Map.of("b", 2)); + + Map toolMap = new LinkedHashMap<>(); + toolMap.put(spec1, "r1"); + toolMap.put(spec2, 123); + + OllamaToolsResult tr = new OllamaToolsResult(new OllamaResult("", null, 0L, 200), toolMap); + + List list = tr.getToolResults(); + assertEquals(2, list.size()); + assertEquals("fn1", list.get(0).getFunctionName()); + assertEquals(Map.of("a", 1), list.get(0).getFunctionArguments()); + assertEquals("r1", list.get(0).getResult()); + + assertEquals("fn2", list.get(1).getFunctionName()); + assertEquals(Map.of("b", 2), list.get(1).getFunctionArguments()); + assertEquals(123, list.get(1).getResult()); + } + + @Test + public void testGetToolResultsReturnsEmptyListWhenNull() { + OllamaToolsResult tr = new OllamaToolsResult(); + tr.setToolResults(null); + List list = tr.getToolResults(); + assertNotNull(list); + assertTrue(list.isEmpty()); + } +} diff --git a/src/test/java/io/github/ollama4j/unittests/TestOptionsAndUtils.java b/src/test/java/io/github/ollama4j/unittests/TestOptionsAndUtils.java new file mode 100644 index 0000000..63efc71 --- /dev/null +++ b/src/test/java/io/github/ollama4j/unittests/TestOptionsAndUtils.java @@ -0,0 +1,92 @@ +package io.github.ollama4j.unittests; + +import io.github.ollama4j.utils.Options; +import io.github.ollama4j.utils.OptionsBuilder; +import io.github.ollama4j.utils.PromptBuilder; +import io.github.ollama4j.utils.Utils; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +class TestOptionsAndUtils { + + @Test + void testOptionsBuilderSetsValues() { + Options options = new OptionsBuilder() + .setMirostat(1) + .setMirostatEta(0.2f) + .setMirostatTau(4.5f) + .setNumCtx(1024) + .setNumGqa(8) + .setNumGpu(2) + .setNumThread(6) + .setRepeatLastN(32) + .setRepeatPenalty(1.2f) + .setTemperature(0.7f) + .setSeed(42) + .setStop("STOP") + .setTfsZ(1.5f) + .setNumPredict(256) + .setTopK(50) + .setTopP(0.95f) + .setMinP(0.05f) + .setCustomOption("custom_param", 123) + .build(); + + Map map = options.getOptionsMap(); + assertEquals(1, map.get("mirostat")); + assertEquals(0.2f, (Float) map.get("mirostat_eta"), 0.0001); + assertEquals(4.5f, (Float) map.get("mirostat_tau"), 0.0001); + assertEquals(1024, map.get("num_ctx")); + assertEquals(8, map.get("num_gqa")); + assertEquals(2, map.get("num_gpu")); + assertEquals(6, map.get("num_thread")); + assertEquals(32, map.get("repeat_last_n")); + assertEquals(1.2f, (Float) map.get("repeat_penalty"), 0.0001); + assertEquals(0.7f, (Float) map.get("temperature"), 0.0001); + assertEquals(42, map.get("seed")); + assertEquals("STOP", map.get("stop")); + assertEquals(1.5f, (Float) map.get("tfs_z"), 0.0001); + assertEquals(256, map.get("num_predict")); + assertEquals(50, map.get("top_k")); + assertEquals(0.95f, (Float) map.get("top_p"), 0.0001); + assertEquals(0.05f, (Float) map.get("min_p"), 0.0001); + assertEquals(123, map.get("custom_param")); + } + + @Test + void testOptionsBuilderRejectsUnsupportedCustomType() { + OptionsBuilder builder = new OptionsBuilder(); + assertThrows(IllegalArgumentException.class, () -> builder.setCustomOption("bad", new Object())); + } + + @Test + void testPromptBuilderBuildsExpectedString() { + String prompt = new PromptBuilder() + .add("Hello") + .addLine(", world!") + .addSeparator() + .add("Continue.") + .build(); + + String expected = "Hello, world!\n\n--------------------------------------------------\nContinue."; + assertEquals(expected, prompt); + } + + @Test + void testUtilsGetObjectMapperSingletonAndModule() { + assertSame(Utils.getObjectMapper(), Utils.getObjectMapper()); + // Basic serialization sanity check with JavaTimeModule registered + assertDoesNotThrow(() -> Utils.getObjectMapper().writeValueAsString(java.time.OffsetDateTime.now())); + } + + @Test + void testGetFileFromClasspath() { + File f = Utils.getFileFromClasspath("test-config.properties"); + assertTrue(f.exists()); + assertTrue(f.getName().contains("test-config.properties")); + } +} diff --git a/src/test/java/io/github/ollama4j/unittests/TestReflectionalToolFunction.java b/src/test/java/io/github/ollama4j/unittests/TestReflectionalToolFunction.java new file mode 100644 index 0000000..9bd47a7 --- /dev/null +++ b/src/test/java/io/github/ollama4j/unittests/TestReflectionalToolFunction.java @@ -0,0 +1,86 @@ +package io.github.ollama4j.unittests; + +import io.github.ollama4j.tools.ReflectionalToolFunction; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Method; +import java.math.BigDecimal; +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +class TestReflectionalToolFunction { + + public static class SampleToolHolder { + public String combine(Integer i, Boolean b, BigDecimal d, String s) { + return String.format("i=%s,b=%s,d=%s,s=%s", i, b, d, s); + } + + public void alwaysThrows() { + throw new IllegalStateException("boom"); + } + } + + @Test + void testApplyInvokesMethodWithTypeCasting() throws Exception { + SampleToolHolder holder = new SampleToolHolder(); + Method method = SampleToolHolder.class.getMethod("combine", Integer.class, Boolean.class, BigDecimal.class, String.class); + + LinkedHashMap propDef = new LinkedHashMap<>(); + // preserve order to match method parameters + propDef.put("i", "java.lang.Integer"); + propDef.put("b", "java.lang.Boolean"); + propDef.put("d", "java.math.BigDecimal"); + propDef.put("s", "java.lang.String"); + + ReflectionalToolFunction fn = new ReflectionalToolFunction(holder, method, propDef); + + Map args = Map.of( + "i", "42", + "b", "true", + "d", "3.14", + "s", 123 // not a string; should be toString()'d by implementation + ); + + Object result = fn.apply(args); + assertEquals("i=42,b=true,d=3.14,s=123", result); + } + + @Test + void testTypeCastNullsWhenClassOrValueIsNull() throws Exception { + SampleToolHolder holder = new SampleToolHolder(); + Method method = SampleToolHolder.class.getMethod("combine", Integer.class, Boolean.class, BigDecimal.class, String.class); + + LinkedHashMap propDef = new LinkedHashMap<>(); + propDef.put("i", null); // className null -> expect null passed + propDef.put("b", "java.lang.Boolean"); + propDef.put("d", "java.math.BigDecimal"); + propDef.put("s", "java.lang.String"); + + ReflectionalToolFunction fn = new ReflectionalToolFunction(holder, method, propDef); + + Map args = new LinkedHashMap<>(); + args.put("i", "100"); // ignored -> becomes null due to null className + args.put("b", null); // value null -> expect null passed + args.put("d", "1.00"); + args.put("s", "ok"); + + Object result = fn.apply(args); + assertEquals("i=null,b=null,d=1.00,s=ok", result); + } + + @Test + void testExceptionsAreWrappedWithMeaningfulMessage() throws Exception { + SampleToolHolder holder = new SampleToolHolder(); + Method throwsMethod = SampleToolHolder.class.getMethod("alwaysThrows"); + + LinkedHashMap propDef = new LinkedHashMap<>(); + + ReflectionalToolFunction fn = new ReflectionalToolFunction(holder, throwsMethod, propDef); + + RuntimeException ex = assertThrows(RuntimeException.class, () -> fn.apply(Map.of())); + assertTrue(ex.getMessage().contains("Failed to invoke tool: alwaysThrows")); + assertNotNull(ex.getCause()); + } +} diff --git a/src/test/java/io/github/ollama4j/unittests/TestToolRegistry.java b/src/test/java/io/github/ollama4j/unittests/TestToolRegistry.java new file mode 100644 index 0000000..b4d20e1 --- /dev/null +++ b/src/test/java/io/github/ollama4j/unittests/TestToolRegistry.java @@ -0,0 +1,48 @@ +package io.github.ollama4j.unittests; + +import io.github.ollama4j.tools.ToolFunction; +import io.github.ollama4j.tools.ToolRegistry; +import io.github.ollama4j.tools.Tools; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +class TestToolRegistry { + + @Test + void testAddAndGetToolFunction() { + ToolRegistry registry = new ToolRegistry(); + ToolFunction fn = args -> "ok:" + args.get("x"); + + Tools.ToolSpecification spec = Tools.ToolSpecification.builder() + .functionName("test") + .functionDescription("desc") + .toolFunction(fn) + .build(); + + registry.addTool("test", spec); + ToolFunction retrieved = registry.getToolFunction("test"); + assertNotNull(retrieved); + assertEquals("ok:42", retrieved.apply(Map.of("x", 42))); + } + + @Test + void testGetUnknownReturnsNull() { + ToolRegistry registry = new ToolRegistry(); + assertNull(registry.getToolFunction("nope")); + } + + @Test + void testClearRemovesAll() { + ToolRegistry registry = new ToolRegistry(); + registry.addTool("a", Tools.ToolSpecification.builder().toolFunction(args -> 1).build()); + registry.addTool("b", Tools.ToolSpecification.builder().toolFunction(args -> 2).build()); + assertFalse(registry.getRegisteredSpecs().isEmpty()); + registry.clear(); + assertTrue(registry.getRegisteredSpecs().isEmpty()); + assertNull(registry.getToolFunction("a")); + assertNull(registry.getToolFunction("b")); + } +} diff --git a/src/test/java/io/github/ollama4j/unittests/TestToolsPromptBuilder.java b/src/test/java/io/github/ollama4j/unittests/TestToolsPromptBuilder.java new file mode 100644 index 0000000..3b273e7 --- /dev/null +++ b/src/test/java/io/github/ollama4j/unittests/TestToolsPromptBuilder.java @@ -0,0 +1,64 @@ +package io.github.ollama4j.unittests; + +import com.fasterxml.jackson.core.JsonProcessingException; +import io.github.ollama4j.tools.Tools; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +class TestToolsPromptBuilder { + + @Test + void testPromptBuilderIncludesToolsAndPrompt() throws JsonProcessingException { + Tools.PromptFuncDefinition.Property cityProp = Tools.PromptFuncDefinition.Property.builder() + .type("string") + .description("city name") + .required(true) + .build(); + + Tools.PromptFuncDefinition.Property unitsProp = Tools.PromptFuncDefinition.Property.builder() + .type("string") + .description("units") + .enumValues(List.of("metric", "imperial")) + .required(false) + .build(); + + Tools.PromptFuncDefinition.Parameters params = Tools.PromptFuncDefinition.Parameters.builder() + .type("object") + .properties(Map.of("city", cityProp, "units", unitsProp)) + .build(); + + Tools.PromptFuncDefinition.PromptFuncSpec spec = Tools.PromptFuncDefinition.PromptFuncSpec.builder() + .name("getWeather") + .description("Get weather for a city") + .parameters(params) + .build(); + + Tools.PromptFuncDefinition def = Tools.PromptFuncDefinition.builder() + .type("function") + .function(spec) + .build(); + + Tools.ToolSpecification toolSpec = Tools.ToolSpecification.builder() + .functionName("getWeather") + .functionDescription("Get weather for a city") + .toolPrompt(def) + .build(); + + Tools.PromptBuilder pb = new Tools.PromptBuilder() + .withToolSpecification(toolSpec) + .withPrompt("Tell me the weather."); + + String built = pb.build(); + assertTrue(built.contains("[AVAILABLE_TOOLS]")); + assertTrue(built.contains("[/AVAILABLE_TOOLS]")); + assertTrue(built.contains("[INST]")); + assertTrue(built.contains("Tell me the weather.")); + assertTrue(built.contains("\"name\":\"getWeather\"")); + assertTrue(built.contains("\"required\":[\"city\"]")); + assertTrue(built.contains("\"enum\":[\"metric\",\"imperial\"]")); + } +} diff --git a/src/test/java/io/github/ollama4j/unittests/jackson/AbstractSerializationTest.java b/src/test/java/io/github/ollama4j/unittests/jackson/AbstractSerializationTest.java index 09a5d67..8476ca0 100644 --- a/src/test/java/io/github/ollama4j/unittests/jackson/AbstractSerializationTest.java +++ b/src/test/java/io/github/ollama4j/unittests/jackson/AbstractSerializationTest.java @@ -30,7 +30,7 @@ public abstract class AbstractSerializationTest { } protected void assertEqualsAfterUnmarshalling(T unmarshalledObject, - T req) { + T req) { assertEquals(req, unmarshalledObject); } } diff --git a/src/test/java/io/github/ollama4j/unittests/jackson/TestChatRequestSerialization.java b/src/test/java/io/github/ollama4j/unittests/jackson/TestChatRequestSerialization.java index 003538e..984bc22 100644 --- a/src/test/java/io/github/ollama4j/unittests/jackson/TestChatRequestSerialization.java +++ b/src/test/java/io/github/ollama4j/unittests/jackson/TestChatRequestSerialization.java @@ -34,8 +34,8 @@ public class TestChatRequestSerialization extends AbstractSerializationTest { - OllamaChatRequest req = builder.withMessage(OllamaChatMessageRole.USER, "Some prompt") - .withOptions(b.setCustomOption("cust_obj", new Object()).build()) - .build(); + OllamaChatRequest req = builder.withMessage(OllamaChatMessageRole.USER, "Some prompt") + .withOptions(b.setCustomOption("cust_obj", new Object()).build()) + .build(); }); } @@ -109,7 +109,7 @@ public class TestChatRequestSerialization extends AbstractSerializationTest { - private OllamaEmbedRequestBuilder builder; + private OllamaEmbedRequestBuilder builder; - @BeforeEach - public void init() { - builder = OllamaEmbedRequestBuilder.getInstance("DummyModel","DummyPrompt"); - } + @BeforeEach + public void init() { + builder = OllamaEmbedRequestBuilder.getInstance("DummyModel", "DummyPrompt"); + } - @Test + @Test public void testRequestOnlyMandatoryFields() { OllamaEmbedRequestModel req = builder.build(); String jsonRequest = serialize(req); - assertEqualsAfterUnmarshalling(deserialize(jsonRequest,OllamaEmbedRequestModel.class), req); + assertEqualsAfterUnmarshalling(deserialize(jsonRequest, OllamaEmbedRequestModel.class), req); } - @Test - public void testRequestWithOptions() { - OptionsBuilder b = new OptionsBuilder(); - OllamaEmbedRequestModel req = builder - .withOptions(b.setMirostat(1).build()).build(); + @Test + public void testRequestWithOptions() { + OptionsBuilder b = new OptionsBuilder(); + OllamaEmbedRequestModel req = builder + .withOptions(b.setMirostat(1).build()).build(); - String jsonRequest = serialize(req); - OllamaEmbedRequestModel deserializeRequest = deserialize(jsonRequest,OllamaEmbedRequestModel.class); - assertEqualsAfterUnmarshalling(deserializeRequest, req); - assertEquals(1, deserializeRequest.getOptions().get("mirostat")); - } + String jsonRequest = serialize(req); + OllamaEmbedRequestModel deserializeRequest = deserialize(jsonRequest, OllamaEmbedRequestModel.class); + assertEqualsAfterUnmarshalling(deserializeRequest, req); + assertEquals(1, deserializeRequest.getOptions().get("mirostat")); + } }