diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 97ca4d0..1e95064 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,37 +1,36 @@ -name: SonarCloud +name: SonarQube on: push: branches: - master pull_request: - types: [ opened, synchronize, reopened ] + types: [opened, synchronize, reopened] jobs: build: name: Build and analyze runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - name: Set up JDK 17 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: 17 distribution: 'zulu' # Alternative distribution options are available - - name: Cache SonarCloud packages - uses: actions/cache@v3 + - name: Cache SonarQube packages + uses: actions/cache@v4 with: path: ~/.sonar/cache key: ${{ runner.os }}-sonar restore-keys: ${{ runner.os }}-sonar - name: Cache Gradle packages - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.gradle/caches key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} restore-keys: ${{ runner.os }}-gradle - name: Build and analyze env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: ./gradlew build sonar --info \ No newline at end of file + run: ./gradlew build sonar --info diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 8a60a21..1449dc2 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -47,42 +47,42 @@ jobs: # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support steps: - - name: Checkout repository - uses: actions/checkout@v4 + - name: Checkout repository + uses: actions/checkout@v4 - - uses: actions/setup-java@v1 - with: - java-version: 17 + - uses: actions/setup-java@v1 + with: + java-version: 17 - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. - # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - queries: security-extended + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + queries: security-extended - # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v3 + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v3 - # ℹ️ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - # If the Autobuild fails above, remove it and uncomment the following three lines. - # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. - # - run: | - # echo "Run, Build Application using script" - # ./location_of_script_within_repo/buildscript.sh + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 - with: - category: "/language:${{matrix.language}}" + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" diff --git a/CHANGELOG.md b/CHANGELOG.md index 8863a55..458d65b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ -# 1.1.0 +# 2.0.0 +- **BREAKING**: Major refactor of message handling for chat completions: + - The `nl.dannyj.mistral.models.completion.Message` class has been removed. + - Introduced `nl.dannyj.mistral.models.completion.message.ChatMessage` as an abstract base class for messages. + - New concrete message classes: `SystemMessage`, `UserMessage`, `AssistantMessage`, `ToolMessage`. + - Message content is now represented by `List` (e.g., `TextChunk`, `ImageURLChunk`) to support multi-modal inputs. + - `ChatCompletionRequest` now expects `List`. + - Streaming: `DeltaChoice` now contains a `nl.dannyj.mistral.models.completion.message.DeltaMessage` object which holds the actual delta (`role`, `content`, `toolCalls`). +- **BREAKING**: Changes to `nl.dannyj.mistral.models.completion.Choice`: + - The `finishReason` field type changed from `String` to `nl.dannyj.mistral.models.completion.FinishReason` (enum). + - The `logProbs` field has been removed. +- **BREAKING**: `encodingFormat` field removed from `EmbeddingRequest` as it is no longer supported by the API. +- **BREAKING**: The `nl.dannyj.mistral.models.model.ModelPermission` class has been removed. +- **BREAKING**: Package changes for core classes: + - `MessageChunk` moved to `nl.dannyj.mistral.models.completion.message.MessageChunk`. + - `MessageRole` moved to `nl.dannyj.mistral.models.completion.message.MessageRole`. +- Added support for function calling (tools). +- Added `nl.dannyj.mistral.models.model.ModelCapabilities` to provide model capability information (e.g., `supportsVision()`, `supportsFunctionCalling()`). +- Increased default read timeout to 120 seconds. +- Made it easier to configure timeouts ([issue #7](https://github.com/Dannyj1/mistral-java-client/issues/7)) - Update jackson-databind from 2.16.1 to 2.18.3 - Update jakarta.validation-api from 3.1.0-M1 to 3.1.1 -- Update okio from 3.8.0 to 3.11.0 \ No newline at end of file +- Update okio from 3.8.0 to 3.11.0 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 75fb4cc..82a3bbe 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community -standards, including sustained inappropriate behavior, harassment of an +standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 0000000..aada0ed --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,225 @@ +# Migration Guide: Updating Message Handling + +This guide outlines the necessary changes to migrate your code from the previous message handling structure (using the +`Message` class) to the new structure introduced to support multi-modal content and +tool calls, based on the abstract `ChatMessage` class. + +## Core Changes + +1. **Message Class Removed:** The old `Message` class has been removed. +2. **ChatMessage Hierarchy:** A new abstract base class `ChatMessage` has been + introduced. Specific message types now extend this class: + - `SystemMessage` + - `UserMessage` + - `AssistantMessage` + - `ToolMessage` +3. **Multi-Modal Content:** Messages (primarily `UserMessage`, `AssistantMessage`, `ToolMessage`) now use a + `List` for their `content` field to support text, images, etc. The + `ContentChunk` interface has implementations like `TextChunk`, + `ImageURLChunk`, etc. + - `getTextContent()` helper methods were introduced to prevent having to loop over `ContentChunk`s to find `TextChunk`s +4. **Tool Calls:** `AssistantMessage` now includes a `List toolCalls` field, and a `ToolMessage` message type was introduced to support function calling/tools. +5. **Streaming Deltas:** The `DeltaChoice` class (used in streaming responses via `MessageChunk`) now contains a + `DeltaMessage` object (accessible via `getDelta()`). This `DeltaMessage` + object, in turn, holds the partial delta fields: `role`, `content` (as `List`), and `toolCalls`. + +## Migration Steps + +### 1. Update `ChatCompletionRequest` Message List + +The `messages` field in `ChatCompletionRequest` now expects `List` instead of `List`. + +**Before:** + +```java +import nl.dannyj.mistral.models.completion.Message; + +// ... +List messages = ...; + ChatCompletionRequest request = ChatCompletionRequest.builder() + .messages(messages) + // ... + .build(); +``` + +**After:** + +```java +import nl.dannyj.mistral.models.completion.ChatMessage; + +// ... +List messages = ...; // See MessageListBuilder changes below + ChatCompletionRequest request = ChatCompletionRequest.builder() + .messages(messages) + // ... + .build(); +``` + +### 2. Update `MessageListBuilder` Usage + +The `MessageListBuilder` now works with `ChatMessage` and its subclasses. + +**Before:** + +```java +import nl.dannyj.mistral.models.completion.Message; +import nl.dannyj.mistral.builders.MessageListBuilder; + +// ... +List messages = new MessageListBuilder() + .system("System prompt") + .user("User query") + .assistant("Assistant response") + .build(); +``` + +**After:** + +```java +import nl.dannyj.mistral.models.completion.ChatMessage; +import nl.dannyj.mistral.builders.MessageListBuilder; + +// ... +List messages = new MessageListBuilder() + .system("System prompt") // Creates SystemMessage + .user("User query") // Creates UserMessage + .assistant("Assistant response") // Creates AssistantMessage (text only) + // .assistant(listOfToolCalls) // Use this overload for tool calls + // .tool("Tool result", "tool_call_id_123") // Creates ToolMessage + .build(); + +// Alternatively, add pre-constructed messages: +// messages = new MessageListBuilder() +// .message(new SystemMessage("System prompt")) +// .message(new UserMessage("User query")) +// .message(AssistantMessage.fromText("Assistant response")) // Factory method +// .build(); +``` + +### 3. Update Handling of Non-Streaming Responses (`ChatCompletionResponse`) + +The `message` field within the `Choice` object (obtained from `ChatCompletionResponse.getChoices()`) is now specifically +an `AssistantMessage`. + +**Before:** + +```java +ChatCompletionResponse response = client.createChatCompletion(request); +Message responseMsg = response.getChoices().get(0).getMessage(); +String content = responseMsg.getContent(); +System.out.println(content); +``` + +**After:** + +```java +import nl.dannyj.mistral.models.completion.message.AssistantMessage; +import nl.dannyj.mistral.models.completion.content.ContentChunk; +import nl.dannyj.mistral.models.completion.content.TextChunk; + +// ... +ChatCompletionResponse response = client.createChatCompletion(request); +AssistantMessage responseMsg = response.getChoices().get(0).getMessage(); + +// Extract text content (handle potential multi-modal content) +String textContent = ""; +if (responseMsg.getContent() != null) { + for (ContentChunk chunk : responseMsg.getContent()) { + if (chunk instanceof TextChunk textChunk) { + textContent += textChunk.getText(); // Append text from TextChunks + } + // Handle other chunk types (ImageURLChunk, etc.) if necessary + } +} +System.out.println(textContent); + +// Check for tool calls if applicable +if (responseMsg.getToolCalls() != null) { + // Process tool calls +} +``` + +### 4. Update Handling of Streaming Responses (`ChatCompletionChunkCallback`) + +The `onChunkReceived` method receives `MessageChunk`. The structure within `MessageChunk.getChoices().get(0)` (which is +a `DeltaChoice`) has changed. Access `role`, `content`, and `toolCalls` directly from the `DeltaChoice` object. + +**Before:** + +```java +client.createChatCompletionStream(request, new ChatCompletionChunkCallback() { + @Override + public void onChunkReceived(MessageChunk chunk) { + // Old structure assumed delta was nested in a Message object + String contentDelta = chunk.getChoices().get(0).getMessage().getContent(); + if (contentDelta != null) { + System.out.print(contentDelta); + } + } + // ... onComplete, onError +}); +``` + +**After:** + +```java +import nl.dannyj.mistral.models.completion.DeltaChoice; +import nl.dannyj.mistral.models.completion.message.DeltaMessage; // <-- Add this import +import nl.dannyj.mistral.models.completion.message.MessageChunk; // <-- Updated import path +import nl.dannyj.mistral.models.completion.content.ContentChunk; +import nl.dannyj.mistral.models.completion.content.TextChunk; +// ... +client.createChatCompletionStream(request, new ChatCompletionChunkCallback() { + @Override + public void onChunkReceived(MessageChunk chunk) { + if (chunk.getChoices() == null || chunk.getChoices().isEmpty()) return; + + DeltaChoice deltaChoice = chunk.getChoices().get(0); + DeltaMessage delta = deltaChoice.getDelta(); // <-- Get the DeltaMessage + + // Check for text content delta using the convenience method + String textDelta = delta.getTextContent(); + + if (textDelta != null) { + System.out.print(textDelta); + } + + } + // ... onComplete, onError +}); +``` + +### 5. Update Handling of `Choice` Object in `ChatCompletionResponse` + +The `Choice` class, which is part of the `ChatCompletionResponse.getChoices()`, has the following breaking changes: + +#### `finishReason` Type Change +- The `finishReason` field is now of type `FinishReason` (an enum) instead of a raw `String`. +- **Before:** `String finishReason = choice.getFinishReason();` +- **After:** `FinishReason finishReason = choice.getFinishReason();` +- To get the underlying string value (e.g., "stop", "length"), you can call `finishReason.getReason()`. + +#### Example: Handling `finishReason` (within the context of iterating choices from `ChatCompletionResponse`) + +**Before(inside loop processing `choice`):** +```java +String reason = choice.getFinishReason(); + if ("stop".equals(reason)) { + // ... + } +``` + +**After(inside loop processing `choice`):** +```java + import nl.dannyj.mistral.models.completion.FinishReason; // Ensure import +// ... +FinishReason reason = choice.getFinishReason(); +if (reason == FinishReason.STOP) { + // ... +} +// Or, to compare with the string value: +// if ("stop".equals(reason.getReason())) { ... } +``` + +#### `logProbs` Field Removal +- The `logProbs` field (previously a `String`) has been removed from the `Choice` class as it is no longer returned by the API. diff --git a/README.md b/README.md index 3e89d33..26a0963 100644 --- a/README.md +++ b/README.md @@ -4,29 +4,23 @@ **Mistral-java-client** is a Java client for the [Mistral AI](https://mistral.ai/) API. It allows you to easily interact with the Mistral AI API from your Java application. -Currently supports all chat completion models. At the time of writing these are: - -- open-mistral-7b -- open-mixtral-8x7b -- mistral-small-latest -- mistral-medium-latest -- mistral-large-latest -- mistral-embed +Supports all chat completion and embedding models available in the API. New models or models not listed here may be already supported without any updates to the library. # Supported APIs -Mistral-java-client is built against version 0.0.1 of the [Mistral AI API](https://docs.mistral.ai/api/). +Mistral-java-client is built against version 0.0.2 of the [Mistral AI API](https://docs.mistral.ai/api/). -- [Create Chat Completions](https://docs.mistral.ai/api/#operation/createChatCompletion) -- [List Available Models](https://docs.mistral.ai/api/#operation/listModels) -- [Create Embeddings](https://docs.mistral.ai/guides/embeddings/) +- [Chat Completion](https://docs.mistral.ai/api/#tag/chat/operation/chat_completion_v1_chat_completions_post) +- [List Models](https://docs.mistral.ai/api/#tag/models/operation/list_models_v1_models_get) +- [Embeddings](https://docs.mistral.ai/api/#tag/embeddings/operation/embeddings_v1_embeddings_post) # Requirements - Java 17 or higher -- A Mistral AI API Key (see the [Mistral documentation](https://docs.mistral.ai/#api-access) for more details on API +- A Mistral AI API Key (see the [Mistral Quickstart documentation](https://docs.mistral.ai/getting-started/quickstart/) + for more details on API access) # Installation @@ -39,13 +33,14 @@ repositories { } dependencies { - implementation 'com.github.Dannyj1:mistral-java-client:1.1.0' + implementation 'com.github.Dannyj1:mistral-java-client:2.0.0' } ``` ## Maven ```xml + jitpack.io @@ -54,9 +49,9 @@ dependencies { - com.github.Dannyj1 - mistral-java-client - 1.1.0 +com.github.Dannyj1 +mistral-java-client +2.0.0 ``` @@ -75,78 +70,219 @@ how to use the client to list available models and create chat completions. ## List Available Models ```java -// You can also put the API key in an environment variable called MISTRAL_API_KEY and remove the apiKey parameter given to the MistralClient constructor -String apiKey = "API_KEY_HERE"; +// You can also put the API key in an environment variable called MISTRAL_API_KEY and remove the API_KEY parameter given to the MistralClient constructor +String API_KEY = "API_KEY_HERE"; // Initialize the client. This should ideally only be done once. The instance should be re-used for multiple requests -MistralClient client = new MistralClient(apiKey); +MistralClient client = new MistralClient(API_KEY); -// Get a list of available models +// Get a list of available models. List models = client.listModels().getModels(); -// Loop through all available models and print their ID. The ID can be used to specify the model when creating chat completions -for (Model model :models) { - System.out.println(model.getId()); +// Loop through all available models and print their ID. The ID can be used to specify the model when creating chat completions. +for (Model model : models) { + // Only print models that support chat completion and vision. + if (model.getCapabilities().supportsChatCompletion() && model.getCapabilities().supportsVision()) { + System.out.println(model.getId()); + } } ``` Example output: -``` -open-mistral-7b -open-mixtral-8x7b +```text +pixtral-large-2411 +pixtral-large-latest +mistral-large-pixtral-2411 +pixtral-12b-2409 +pixtral-12b +pixtral-12b-latest +mistral-small-2503 mistral-small-latest -mistral-medium-latest -mistral-large-latest -mistral-embed ``` ## Chat Completions + The chat completion in this example code is blocking and will wait until the whole response is generated. See [Streaming Chat Completions](#streaming-chat-completions) for a way to stream chunks as they are being generated. + ```java -// You can also put the API key in an environment variable called MISTRAL_API_KEY and remove the apiKey parameter given to the MistralClient constructor -String apiKey = "API_KEY_HERE"; +// You can also put the API key in an environment variable called MISTRAL_API_KEY and remove the API_KEY parameter given to the MistralClient constructor +String API_KEY = "API_KEY_HERE"; +String MODEL_NAME = "mistral-small-latest"; // Initialize the client. This should ideally only be done once. The instance should be re-used for multiple requests -MistralClient client = new MistralClient(apiKey); -String model = "open-mistral-7b"; -// MessageListBuilder can be used to easily create a List object, with the method specifying the role. -// You can also manually create a list of Message objects -List messages = new MessageListBuilder() - .system("You are a helpful assistant.") - .user("Write a java program that prints 'Hello World!'.") +MistralClient client = new MistralClient(API_KEY); + +// MessageListBuilder can be used to easily create a List object, with the method specifying the role. +// You can also manually create a list of ChatMessage objects (e.g. SystemMessage, UserMessage, AssistantMessage) +// For UserMessage with multimodal content (e.g. images), or AssistantMessage with tool calls, you'd construct them directly. +List messages = new MessageListBuilder() + .system("You are a helpful Java assistant.") + .user("Write a java program that prints 'Hello World!' a random amount of times. Do not explain the code.") .build(); // Create a ChatCompletionRequest. A ChatCompletionRequest has to be passed to the createChatCompletion method. // It contains all the parameters needed to create a chat completion. Parameters not specified will be set to their default values. ChatCompletionRequest request = ChatCompletionRequest.builder() - .model(model) - .temperature(0.75) + .model(MODEL_NAME) .messages(messages) - .safePrompt(false) .build(); // Create a chat completion ChatCompletionResponse response = client.createChatCompletion(request); -// A response contains a list of choices. We get the first choice here and print it -Message firstChoice = response.getChoices().get(0).getMessage(); -System.out.println(firstChoice.getRole() + ":\n" + firstChoice.getContent() + "\n"); +// A response contains a list of choices. The message in a choice is an AssistantMessage. +// We get the first choice here and print its content. +// Content is a List. For simple text, we iterate and append text from TextChunk instances. +AssistantMessage firstChoice = response.getChoices().get(0).getMessage(); +String textContent = firstChoice.getTextContent(); + +if (textContent != null) { + System.out.println("Assistant:\n" + textContent); +} else { + System.out.println("No text content available in the assistant's response."); +} ``` Example output: -``` -ASSISTANT: -Here is a simple Java program that prints "Hello World!" to the console: -'''java -public class HelloWorld { +```java +Assistant: +import java.util.Random; + +public class HelloWorldRandom { public static void main(String[] args) { - System.out.println("Hello World!"); + Random rand = new Random(); + int times = rand.nextInt(10) + 1; + for (int i = 0; i < times; i++) { + System.out.println("Hello World!"); + } } } -''' +``` + +### Function Calling Example + +The following example shows how to use the function calling feature of the Mistral API. + +```java +String API_KEY = "API_KEY_HERE"; +String MODEL_NAME = "mistral-large-latest"; // Must be a model that supports function calling + +// Initialize the client. This should ideally only be done once. The instance should be re-used for multiple requests +MistralClient client = new MistralClient(API_KEY); + +// 1. Define the tool (function) schema as a compact JSON string +String weatherFunctionSchema = """ + { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA" + }, + "unit": { + "type": "string", + "enum": ["celsius", "fahrenheit"], + "description": "The temperature unit to use." + } + }, + "required": ["location", "unit"] + } + """; + +Tool weatherTool = Tool.builder() + .function(Function.builder() + .name("getCurrentWeather") + .description("Get the current weather in a given location") + .parameters(weatherFunctionSchema) + .build()) + .build(); + +// 2. Build initial message list and request +List messages = new ArrayList<>(new MessageListBuilder() + .user("What's the weather like in London?") + .build()); + +ChatCompletionRequest initialRequest = ChatCompletionRequest.builder() + .model(MODEL_NAME) + .messages(messages) + .tools(List.of(weatherTool)) + .toolChoice(ToolChoiceEnum.ANY) // Force the model to use a tool + .build(); + +System.out.println("Initial Request -> Model"); + +// 3. First API Call +ChatCompletionResponse response = client.createChatCompletion(initialRequest); + +AssistantMessage responseMessage = response.getChoices().get(0).getMessage(); +messages.add(responseMessage); // Add assistant's response (with tool call) to messages + +// 4. Check for tool calls and simulate execution +if (responseMessage.getToolCalls() != null && !responseMessage.getToolCalls().isEmpty()) { + System.out.println("Model Response -> Tool Call Requested"); + + ToolCall toolCall = responseMessage.getToolCalls().get(0); // Assuming one tool call for simplicity + FunctionCall functionCall = toolCall.getFunction(); + + System.out.println(" Function Name: " + functionCall.getName()); + System.out.println(" Arguments: " + functionCall.getArguments()); + + // 5. Simulate tool execution based on function name and arguments + String toolResultJson; + if ("getCurrentWeather".equals(functionCall.getName())) { + // In a real application, you would parse arguments and call your actual function + // Here, we just simulate a result + toolResultJson = "{\"temperature\": \"15\", \"unit\": \"celsius\", \"description\": \"Cloudy\"}"; + System.out.println(" Simulated Tool Result: " + toolResultJson); + } else { + System.out.println(" Unknown function called: " + functionCall.getName()); + toolResultJson = "{\"error\": \"Unknown function\"}"; + } + + // 6. Build follow-up request with tool result + messages.add(new ToolMessage(toolResultJson, toolCall.getId())); + + ChatCompletionRequest followUpRequest = ChatCompletionRequest.builder() + .model(MODEL_NAME) + .messages(messages) + .build(); + + System.out.println("Tool Result + History -> Model"); + + // 7. Second API Call + ChatCompletionResponse finalResponse = client.createChatCompletion(followUpRequest); + + // 8. Display final result + AssistantMessage finalAssistantMessage = finalResponse.getChoices().get(0).getMessage(); + String finalContent = finalAssistantMessage.getTextContent(); + System.out.println("Final Model Response:\n" + (finalContent != null ? finalContent : "No text content in final response.")); + +} else { + // Model decided not to call a tool + System.out.println("Model Response -> No Tool Call"); + + String textContent = responseMessage.getTextContent(); + System.out.println("Assistant:\n" + (textContent != null ? textContent : "No text content available.")); +} +System.out.println("--- End of Function Calling Example ---"); +``` + +Example output: + +```text +--- Function Calling Example --- +Initial Request -> Model +Model Response -> Tool Call Requested + Function Name: getCurrentWeather + Arguments: {"location": "London", "unit": "celsius"} + Simulated Tool Result: {"temperature": "15", "unit": "celsius", "description": "Cloudy"} +Tool Result + History -> Model +Final Model Response: +The weather in London is currently 15°C and cloudy. +--- End of Function Calling Example --- ``` ## Streaming Chat Completions @@ -155,49 +291,58 @@ The following example shows how to use a streaming chat completion. The API will being generated instead of waiting for the whole message to be generated. ```java -// You can also put the API key in an environment variable called MISTRAL_API_KEY and remove the apiKey parameter given to the MistralClient constructor -String apiKey = "API_KEY_HERE"; +// You can also put the API key in an environment variable called MISTRAL_API_KEY and remove the API_KEY parameter given to the MistralClient constructor +String API_KEY = "API_KEY_HERE"; +String MODEL_NAME = "mistral-small-latest"; // Initialize the client. This should ideally only be done once. The instance should be re-used for multiple requests -MistralClient client = new MistralClient(apiKey); -String model = "open-mistral-7b"; -List messages = new MessageListBuilder() - .system("You are a helpful programming assistant.") +MistralClient client = new MistralClient(API_KEY); +// Use the MessageListBuilder to easily build a List +List messages = new MessageListBuilder() + .system("You are a helpful Java assistant.") .user("Write a java program that prints the first `n` numbers of the Fibonacci sequence.") .build(); ChatCompletionRequest request = ChatCompletionRequest.builder() - .model(model) + .model(MODEL_NAME) .messages(messages) .stream(true) .build(); client.createChatCompletionStream(request, new ChatCompletionChunkCallback() { - @Override - public void onChunkReceived (MessageChunk chunk){ - // This method receives a chunk of the message as it is being generated. - System.out.print(chunk.getChoices().get(0).getMessage().getContent()); - } - - @Override - public void onComplete () { - // This method is called when the generation is fully completed. - System.out.println("\n\n== Generation Done! =="); - } - - @Override - public void onError (Exception e){ - // This method is called when an error occurs. Generation will be stopped and the other methods will not be called - e.printStackTrace(); - } + @Override + public void onChunkReceived(MessageChunk chunk) { + // This method receives a chunk of the message as it is being generated. + // The MessageChunk contains choices, and each choice (DeltaChoice) has the delta. + if (chunk.getChoices() == null || chunk.getChoices().isEmpty()) { + return; + } + + DeltaChoice deltaChoice = chunk.getChoices().get(0); + String textContent = deltaChoice.getTextContent(); + + System.out.print(textContent); + // If using tool calls, you might also check deltaChoice.getToolCalls() + } + + @Override + public void onComplete() { + // This method is called when the generation is fully completed. + System.out.println("\n\n== Generation Done! =="); + } + + @Override + public void onError(Exception e) { + // This method is called when an error occurs. Generation will be stopped and the other methods will not be called + e.printStackTrace(); + } }); ``` Example output: -``` +```java Here is a Java program that prints the first `n` numbers of the Fibonacci sequence: -'''java public class Fibonacci { public static void main(String[] args) { int n = Integer.parseInt(args[0]); // Get the number of Fibonacci numbers to print from command line argument @@ -214,7 +359,6 @@ public class Fibonacci { } } } -''' To run the program, save it as `Fibonacci.java` and compile it using the command `javac Fibonacci.java`. To run the program and print the first `n` numbers of the Fibonacci sequence, provide the number as a command line argument, e.g. `java Fibonacci 10` to print the first 10 numbers. @@ -224,14 +368,14 @@ To run the program, save it as `Fibonacci.java` and compile it using the command ## Embeddings ```java -// You can also put the API key in an environment variable called MISTRAL_API_KEY and remove the apiKey parameter given to the MistralClient constructor -String apiKey = "API_KEY_HERE"; +// You can also put the API key in an environment variable called MISTRAL_API_KEY and remove the API_KEY parameter given to the MistralClient constructor +String API_KEY = "API_KEY_HERE"; // Initialize the client. This should ideally only be done once. The instance should be re-used for multiple requests -MistralClient client = new MistralClient(apiKey); +MistralClient client = new MistralClient(API_KEY); List exampleTexts = List.of( - "This is a test sentence.", - "This is another test sentence." + "This is a test sentence.", + "This is another test sentence." ); EmbeddingRequest embeddingRequest = EmbeddingRequest.builder() @@ -241,21 +385,22 @@ EmbeddingRequest embeddingRequest = EmbeddingRequest.builder() EmbeddingResponse embeddingsResponse = client.createEmbedding(embeddingRequest); // Embeddings are returned as a list of FloatEmbedding objects. FloatEmbedding objects contain a list of floats per input string. -// See the Mistral documentation for more information: https://docs.mistral.ai/guides/embeddings/ +// See the Mistral documentation for more information: https://docs.mistral.ai/capabilities/embeddings/ List embeddings = embeddingsResponse.getData(); embeddings.forEach(embedding -> System.out.println(embedding.getEmbedding())); ``` Example output: -``` +```text [-0.028015137, 0.02532959, 0.042785645, ... , -0.020980835, 0.011947632, -0.0035934448] [-0.02015686, 0.04272461, 0.05529785, ... , -0.006855011, 0.009529114, -0.016448975] ``` # Roadmap -- [ ] Add support for Function Calls -- [ ] Handle rate limiting using some sort of queue system + +- [ ] Add support for all missing features (e.g. OCR) +- [ ] Handle rate limits - [ ] Unit tests # License diff --git a/SECURITY.md b/SECURITY.md index 588cac7..df3e3df 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,4 +2,6 @@ ## Reporting a Vulnerability -To report a vulnerability, please use the "privately reporting a security vulnerability" feature from GitHub. You can find the "Report a vulnerability" button under the security tab. For more information, see the [privately reporting a security vulnerability documentation](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability). +To report a vulnerability, please use the "privately reporting a security vulnerability" feature from GitHub. You can +find the "Report a vulnerability" button under the security tab. For more information, see +the [privately reporting a security vulnerability documentation](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability). diff --git a/build.gradle b/build.gradle index 866790b..fbc2998 100644 --- a/build.gradle +++ b/build.gradle @@ -1,12 +1,12 @@ plugins { id "java-library" id "maven-publish" - id "org.sonarqube" version "6.1.0.5360" + id "org.sonarqube" version "6.0.1.5171" id "io.freefair.lombok" version "8.13.1" } group = "nl.dannyj" -version = "1.1.0" +version = "2.0.0" repositories { mavenCentral() @@ -16,6 +16,7 @@ dependencies { implementation "com.squareup.okhttp3:okhttp:4.12.0" implementation "com.fasterxml.jackson.core:jackson-databind:2.18.3" implementation "jakarta.validation:jakarta.validation-api:3.1.1" + implementation 'jakarta.annotation:jakarta.annotation-api:3.0.0' implementation "org.hibernate.validator:hibernate-validator:8.0.2.Final" implementation "org.glassfish.expressly:expressly:5.0.0" implementation "com.squareup.okio:okio:3.11.0" @@ -46,4 +47,4 @@ sonar { property "sonar.organization", "danny1" property "sonar.host.url", "https://sonarcloud.io" } -} \ No newline at end of file +} diff --git a/src/main/java/nl/dannyj/mistral/MistralClient.java b/src/main/java/nl/dannyj/mistral/MistralClient.java index 9bb3280..0b7e24c 100644 --- a/src/main/java/nl/dannyj/mistral/MistralClient.java +++ b/src/main/java/nl/dannyj/mistral/MistralClient.java @@ -36,6 +36,7 @@ import okhttp3.OkHttpClient; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; /** * The MistralClient is the main class that interacts with all components of this library. @@ -62,7 +63,7 @@ public class MistralClient { */ public MistralClient(@NonNull String apiKey) { this.apiKey = apiKey; - this.httpClient = buildHttpClient(); + this.httpClient = buildHttpClient(120, 10, 10); this.objectMapper = buildObjectMapper(); this.mistralService = buildMistralService(); } @@ -72,7 +73,7 @@ public MistralClient(@NonNull String apiKey) { */ public MistralClient() { this.apiKey = System.getenv(API_KEY_ENV_VAR); - this.httpClient = buildHttpClient(); + this.httpClient = buildHttpClient(120, 10, 10); this.objectMapper = buildObjectMapper(); this.mistralService = buildMistralService(); } @@ -112,7 +113,52 @@ public MistralClient(@NonNull String apiKey, @NonNull OkHttpClient httpClient) { */ public MistralClient(@NonNull String apiKey, @NonNull ObjectMapper objectMapper) { this.apiKey = apiKey; - this.httpClient = buildHttpClient(); + this.httpClient = buildHttpClient(120, 10, 10); + this.objectMapper = objectMapper; + this.mistralService = buildMistralService(); + } + + /** + * Constructor that initializes the MistralClient with a provided API key and custom timeouts. + * + * @param apiKey The API key to be used for the Mistral AI API + * @param readTimeoutSeconds The read timeout in seconds + * @param connectTimeoutSeconds The connect timeout in seconds + * @param writeTimeoutSeconds The write timeout in seconds + */ + public MistralClient(@NonNull String apiKey, int readTimeoutSeconds, int connectTimeoutSeconds, int writeTimeoutSeconds) { + this.apiKey = apiKey; + this.httpClient = buildHttpClient(readTimeoutSeconds, connectTimeoutSeconds, writeTimeoutSeconds); + this.objectMapper = buildObjectMapper(); + this.mistralService = buildMistralService(); + } + + /** + * Default constructor that initializes the MistralClient with the API key from the environment variable "MISTRAL_API_KEY" and custom timeouts. + * + * @param readTimeoutSeconds The read timeout in seconds + * @param connectTimeoutSeconds The connect timeout in seconds + * @param writeTimeoutSeconds The write timeout in seconds + */ + public MistralClient(int readTimeoutSeconds, int connectTimeoutSeconds, int writeTimeoutSeconds) { + this.apiKey = System.getenv(API_KEY_ENV_VAR); + this.httpClient = buildHttpClient(readTimeoutSeconds, connectTimeoutSeconds, writeTimeoutSeconds); + this.objectMapper = buildObjectMapper(); + this.mistralService = buildMistralService(); + } + + /** + * Constructor that initializes the MistralClient with a provided API key, object mapper, and custom timeouts. + * + * @param apiKey The API key to be used for the Mistral AI API + * @param objectMapper The Jackson ObjectMapper to be used for serializing and deserializing JSON + * @param readTimeoutSeconds The read timeout in seconds + * @param connectTimeoutSeconds The connect timeout in seconds + * @param writeTimeoutSeconds The write timeout in seconds + */ + public MistralClient(@NonNull String apiKey, @NonNull ObjectMapper objectMapper, int readTimeoutSeconds, int connectTimeoutSeconds, int writeTimeoutSeconds) { + this.apiKey = apiKey; + this.httpClient = buildHttpClient(readTimeoutSeconds, connectTimeoutSeconds, writeTimeoutSeconds); this.objectMapper = objectMapper; this.mistralService = buildMistralService(); } @@ -147,7 +193,7 @@ public CompletableFuture createChatCompletionAsync(@NonN /** * This method is used to create an embedding using the Mistral AI API. - * The embeddings for the input strings. See the mistral documentation for more details on embeddings. + * The embeddings for the input strings. See the mistral documentation for more details on embeddings. * This is a blocking method. * * @param request The request to create an embedding. See {@link EmbeddingRequest}. @@ -161,7 +207,7 @@ public EmbeddingResponse createEmbedding(@NonNull EmbeddingRequest request) { /** * This method is used to create an embedding using the Mistral AI API. - * The embeddings for the input strings. See the mistral documentation for more details on embeddings. + * The embeddings for the input strings. See the mistral documentation for more details on embeddings. * This is a non-blocking/asynchronous method. * * @param request The request to create an embedding. See {@link EmbeddingRequest}. @@ -213,11 +259,13 @@ private MistralService buildMistralService() { * * @return A new instance of OkHttpClient */ - private OkHttpClient buildHttpClient() { + private OkHttpClient buildHttpClient(int readTimeoutSeconds, int connectTimeoutSeconds, int writeTimeoutSeconds) { MistralHeaderInterceptor mistralInterceptor = new MistralHeaderInterceptor(this); return new OkHttpClient.Builder() - .readTimeout(20, java.util.concurrent.TimeUnit.SECONDS) + .readTimeout(readTimeoutSeconds, TimeUnit.SECONDS) + .connectTimeout(connectTimeoutSeconds, TimeUnit.SECONDS) + .writeTimeout(writeTimeoutSeconds, TimeUnit.SECONDS) .addInterceptor(mistralInterceptor) .build(); } diff --git a/src/main/java/nl/dannyj/mistral/builders/MessageListBuilder.java b/src/main/java/nl/dannyj/mistral/builders/MessageListBuilder.java index c29faa9..966ab00 100644 --- a/src/main/java/nl/dannyj/mistral/builders/MessageListBuilder.java +++ b/src/main/java/nl/dannyj/mistral/builders/MessageListBuilder.java @@ -16,87 +16,118 @@ package nl.dannyj.mistral.builders; -import nl.dannyj.mistral.models.completion.Message; -import nl.dannyj.mistral.models.completion.MessageRole; +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import nl.dannyj.mistral.models.completion.message.AssistantMessage; +import nl.dannyj.mistral.models.completion.message.ChatMessage; +import nl.dannyj.mistral.models.completion.message.SystemMessage; +import nl.dannyj.mistral.models.completion.message.ToolMessage; +import nl.dannyj.mistral.models.completion.message.UserMessage; +import nl.dannyj.mistral.models.completion.tool.ToolCall; import java.util.ArrayList; import java.util.List; /** - * The MessageListBuilder class is a builder class for creating a list of Message objects. - * It provides methods to add messages of different roles (system, assistant, user) to the list. - * The build method returns the list of Message objects that have been added. + * A builder class for creating a list of {@link ChatMessage} objects for a chat completion request. + * Provides convenience methods for adding messages with different roles and content types. */ public class MessageListBuilder { - private final List messages; + private final List messages; /** - * Default constructor that initializes an empty list of Message objects. + * Default constructor that initializes an empty list of ChatMessage objects. */ public MessageListBuilder() { this.messages = new ArrayList<>(); } /** - * Constructor that initializes the list of Message objects with a provided list. + * Constructor that initializes the list of ChatMessage objects with a provided list. * - * @param messages The initial list of Message objects + * @param messages The initial list of ChatMessage objects. */ - public MessageListBuilder(List messages) { - this.messages = messages; + public MessageListBuilder(List messages) { + this.messages = new ArrayList<>(messages); } /** - * Adds a message with the system role to the list with the provided content. + * Adds a system message to the list with the provided content. * - * @param content The content of the system message - * @return The builder instance + * @param content The text content of the system message. Cannot be null. + * @return This builder instance. */ - public MessageListBuilder system(String content) { - this.messages.add(new Message(MessageRole.SYSTEM, content)); + public MessageListBuilder system(@NotNull String content) { + this.messages.add(new SystemMessage(content)); return this; } /** - * Adds a message with the assistant role to the list with the provided content. + * Adds an assistant message with text content to the list. * - * @param content The content of the assistant message - * @return The builder instance + * @param content The text content of the assistant message. Cannot be null. + * @return This builder instance. */ - public MessageListBuilder assistant(String content) { - this.messages.add(new Message(MessageRole.ASSISTANT, content)); + public MessageListBuilder assistant(@NotNull String content) { + this.messages.add(new AssistantMessage(content)); return this; } /** - * Adds a message with the user role to the list with the provided content. + * Adds an assistant message with tool calls to the list. * - * @param content The content of the user message - * @return The builder instance + * @param toolCalls The list of tool calls. Cannot be null or empty. + * @return This builder instance. */ - public MessageListBuilder user(String content) { - this.messages.add(new Message(MessageRole.USER, content)); + public MessageListBuilder assistant(@NotNull @NotEmpty List toolCalls) { + this.messages.add(new AssistantMessage(toolCalls)); return this; } /** - * Adds a custom Message object to the list. + * Adds a user message with text content to the list. * - * @param message The Message object to be added - * @return The builder instance + * @param content The text content of the user message. Cannot be null. + * @return This builder instance. */ - public MessageListBuilder message(Message message) { + public MessageListBuilder user(@NotNull String content) { + this.messages.add(new UserMessage(content)); + return this; + } + + /** + * Adds a tool message with text content to the list. + * + * @param content The text content of the tool call. Cannot be null. + * @param toolCallId The ID of the tool call this message responds to. Can be null. + * @return This builder instance. + */ + public MessageListBuilder tool(@NotNull String content, @Nullable String toolCallId) { + this.messages.add(new ToolMessage(content, toolCallId)); + return this; + } + + + /** + * Adds a pre-constructed ChatMessage object to the list. + * Useful for adding messages with complex content or specific configurations (e.g., AssistantMessage with both content and tool calls). + * + * @param message The ChatMessage object to be added. Cannot be null. + * @return This builder instance. + */ + public MessageListBuilder message(@NotNull ChatMessage message) { this.messages.add(message); return this; } /** - * Returns the list of Message objects that have been added. + * Returns the list of ChatMessage objects that have been added. * - * @return The list of Message objects + * @return The list of ChatMessage objects. */ - public List build() { + public List build() { return this.messages; } } diff --git a/src/main/java/nl/dannyj/mistral/models/completion/ChatCompletionRequest.java b/src/main/java/nl/dannyj/mistral/models/completion/ChatCompletionRequest.java index e75613a..b848d12 100644 --- a/src/main/java/nl/dannyj/mistral/models/completion/ChatCompletionRequest.java +++ b/src/main/java/nl/dannyj/mistral/models/completion/ChatCompletionRequest.java @@ -1,12 +1,40 @@ +/* + * Copyright 2024-2025 Danny Jelsma + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package nl.dannyj.mistral.models.completion; import com.fasterxml.jackson.annotation.JsonProperty; -import jakarta.validation.constraints.*; +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.PositiveOrZero; +import jakarta.validation.constraints.Size; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import nl.dannyj.mistral.MistralClient; import nl.dannyj.mistral.models.Request; +import nl.dannyj.mistral.models.completion.message.ChatMessage; +import nl.dannyj.mistral.models.completion.tool.SpecificToolChoice; +import nl.dannyj.mistral.models.completion.tool.Tool; +import nl.dannyj.mistral.models.completion.tool.ToolChoiceEnum; +import nl.dannyj.mistral.models.completion.tool.ToolChoiceOption; import nl.dannyj.mistral.net.ChatCompletionChunkCallback; import java.util.List; @@ -22,7 +50,7 @@ public class ChatCompletionRequest implements Request { /** - * ID of the model to use. You can use the List Available Models API to see all of your available models. + * ID of the model to use. You can use the List Available Models API ({@link MistralClient#listModels()}) to see all of your available models. * * @param model The model's ID. Can't be null or empty. * @return The model's ID. @@ -32,28 +60,18 @@ public class ChatCompletionRequest implements Request { private String model; /** - * The prompt(s) to generate completions for, encoded as a list of dict with role and content. - * Must contain at least one message and the first prompt role should be user or system. + * What sampling temperature to use, Mistral recommends this to be between 0.0 and 0.7. + * Higher values like 0.7 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. + * Mistral generally recommends altering this or top_p but not both. The default value varies depending on the model you are targeting. * - * @param messages The messages/conversation to generate completions for. Can't be null or empty. - * @return The messages/conversation to generate completions for. - */ - @NotNull - @Size(min = 1) - private List messages; - - /** - * What sampling temperature to use, between 0.0 and 1.0. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. - * We generally recommend altering this or top_p but not both. - * Defaults to 0.7. - * - * @param temperature The sampling temperature to use. Has to be between 0.0 and 1.0. + * @param temperature The sampling temperature to use. Has to be between 0.0 and 1.5. Null will default to the model's default value. * @return The sampling temperature to use. */ - @DecimalMin("0.0") - @DecimalMax("1.0") + @Nullable @Builder.Default - private Double temperature = 0.7; + @DecimalMin("0.0") + @DecimalMax("1.5") + private Double temperature = null; /** * Nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. @@ -66,21 +84,22 @@ public class ChatCompletionRequest implements Request { @JsonProperty("top_p") @DecimalMin("0.0") @DecimalMax("1.0") + @NotNull @Builder.Default private Double topP = 1.0; /** * The maximum number of tokens to generate in the completion. * The token count of your prompt plus max_tokens cannot exceed the model's context length. - * Defaults to 32000, which is the maximum value for all currently available models. * - * @param maxTokens The maximum number of tokens to generate in the completion. Has to be positive or zero. + * @param maxTokens The maximum number of tokens to generate in the completion. Has to be positive or zero. Null for the model's default value. * @return The maximum number of tokens to generate in the completion. */ @JsonProperty("max_tokens") - @Builder.Default @PositiveOrZero - private Integer maxTokens = 32000; + @Builder.Default + @Nullable + private Integer maxTokens = null; /** * Whether to stream back partial progress. When set to true, the {@link nl.dannyj.mistral.MistralClient#createChatCompletionStream(ChatCompletionRequest, ChatCompletionChunkCallback)} method has to be used. @@ -89,19 +108,18 @@ public class ChatCompletionRequest implements Request { * @return Whether to stream back partial progress. */ @Builder.Default - private Boolean stream = null; + @NotNull + private Boolean stream = false; /** - * Whether to inject a safety prompt before all conversations. - * Toggling the safe prompt will prepend your messages with the following system prompt: - * Always assist with care, respect, and truth. Respond with utmost utility yet securely. Avoid harmful, unethical, prejudiced, or negative content. Ensure replies promote fairness and positivity. + * Stop generation if this token is detected. Or if one of these tokens is detected when providing an array * - * @param safePrompt Whether to inject a safety prompt before all conversations. - * @return Whether to inject a safety prompt before all conversations. + * @param stop The stop sequence(s) to use. Use an empty List (default value) for no stop sequence. + * @return The stop sequence(s) to use. */ - @JsonProperty("safe_prompt") + @NotNull @Builder.Default - private boolean safePrompt = false; + private List stop = List.of(); /** * The seed to use for random sampling. If set, different calls will generate deterministic results. @@ -110,23 +128,137 @@ public class ChatCompletionRequest implements Request { * @return The seed to use for random sampling. */ @JsonProperty("random_seed") + @Nullable @Builder.Default private Long randomSeed = null; /** - * The response format of the completion request. Defaults to "text". - * Currently only available when using mistral small and mistral large models. For other models, this MUST be set to null. Otherwise, you may get a 422 Unprocessable Content error. + * The prompt(s) to generate completions for, encoded as a list of dict with role and content. + * Must contain at least one message and the first prompt role should be user or system. + * + * @param messages The list of messages representing the conversation history. Must contain at least one message. + * @return The list of messages. + */ + @NotNull + @Size(min = 1) + private List messages; + + /** + * An object specifying the format that the model must output. Setting to JSON_OBJECT enables JSON mode, which guarantees the message the model generates is in JSON. When using JSON mode you MUST also instruct the model to produce JSON yourself with a system or a user message. * * @param responseFormat The response format of the completion request. Currently only available when using mistral small and mistral large models. For other models, this MUST be set to null. * @return The response format of the completion request. */ @JsonProperty("response_format") - private ResponseFormat responseFormat = null; - - public static class ChatCompletionRequestBuilder { - public ChatCompletionRequestBuilder responseFormat(ResponseFormats responseFormat) { - this.responseFormat = new ResponseFormat(responseFormat); - return this; - } - } + @NotNull + @Builder.Default + private ResponseFormat responseFormat = new ResponseFormat(ResponseFormats.TEXT); + + /** + * Whether to inject a safety prompt before all conversations. + * Toggling the safe prompt will prepend your messages with the following system prompt: + * Always assist with care, respect, and truth. Respond with utmost utility yet securely. Avoid harmful, unethical, prejudiced, or negative content. Ensure replies promote fairness and positivity. + * + * @param safePrompt Whether to inject a safety prompt before all conversations. + * @return Whether to inject a safety prompt before all conversations. + */ + @JsonProperty("safe_prompt") + @Builder.Default + private boolean safePrompt = false; + + /** + * A list of tools the model may call. Currently, only functions are supported as a tool. + * Use this to provide a list of functions the model may generate JSON inputs for. + * Set to null or an empty list if no tools should be available. + * + * @param tools The list of tools. + * @return The list of tools. + */ + @Nullable + @JsonProperty("tools") + @Builder.Default + private List tools = null; + + /** + * Controls which function call(s) are made, if any. + * 'none' means the model will not call a function and instead generates a message. + * 'auto' means the model can pick between generating a message or calling a function. + * 'any' forces the model to call a function. + * Specifying a particular function via {@link SpecificToolChoice} forces the model to call that function. + * Defaults to 'auto'. + * + * @param toolChoice The tool choice option (can be {@link ToolChoiceEnum} or {@link SpecificToolChoice}). + * @return The tool choice option. + */ + @JsonProperty("tool_choice") + @Builder.Default + private ToolChoiceOption toolChoice = ToolChoiceEnum.AUTO; + + /** + * Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, + * increasing the model's likelihood to talk about new topics. + * Defaults to 0.0. + * + * @param presencePenalty The presence penalty. + * @return The presence penalty. + */ + @Nullable + @JsonProperty("presence_penalty") + @DecimalMin("-2.0") + @DecimalMax("2.0") + @Builder.Default + private Double presencePenalty = 0.0; + + /** + * Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, + * decreasing the model's likelihood to repeat the same line verbatim. + * Defaults to 0.0. + * + * @param frequencyPenalty The frequency penalty. + * @return The frequency penalty. + */ + @Nullable + @JsonProperty("frequency_penalty") + @DecimalMin("-2.0") + @DecimalMax("2.0") + @Builder.Default + private Double frequencyPenalty = 0.0; + + /** + * How many chat completion choices to generate for each input message. + * Note that you will be charged based on the number of generated tokens across all of the choices. + * Defaults to 1. + * + * @param n The number of choices to generate. + * @return The number of choices. + */ + @Nullable + @JsonProperty("n") + @Builder.Default + private Integer n = null; + + /** + * Provides predicted output to optimize response time. + * See the Predicted Outputs guide for more details. + * API might have its own default object if null. + * + * @param prediction The prediction object. + * @return The prediction object. + */ + @Nullable + @JsonProperty("prediction") + @Builder.Default + private Prediction prediction = null; + + /** + * Whether to allow parallel function calls. + * Defaults to true. + * + * @param parallelToolCalls Whether parallel tool calls are allowed. + * @return True if parallel tool calls are allowed, false otherwise. + */ + @JsonProperty("parallel_tool_calls") + @Builder.Default + private Boolean parallelToolCalls = true; + } diff --git a/src/main/java/nl/dannyj/mistral/models/completion/Choice.java b/src/main/java/nl/dannyj/mistral/models/completion/Choice.java index 2a199c2..e586c5c 100644 --- a/src/main/java/nl/dannyj/mistral/models/completion/Choice.java +++ b/src/main/java/nl/dannyj/mistral/models/completion/Choice.java @@ -21,9 +21,10 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.ToString; +import nl.dannyj.mistral.models.completion.message.AssistantMessage; /** - * Represents a choice in a chat completion. A choice contains the message that was generated and the reason for the completion to finish. + * Represents a choice in a chat completion response. A choice contains the generated assistant message and the reason the generation finished. */ @Getter @AllArgsConstructor @@ -41,9 +42,9 @@ public class Choice { /** * The message that was generated. * - * @return the message that was generated + * @return the assistant message that was generated */ - private Message message; + private AssistantMessage message; /** * Reason for the completion to finish. @@ -51,9 +52,6 @@ public class Choice { * @return the reason for the completion to finish */ @JsonProperty("finish_reason") - private String finishReason; - - @JsonProperty("logprobs") - private String logProbs; + private FinishReason finishReason; } diff --git a/src/main/java/nl/dannyj/mistral/models/completion/DeltaChoice.java b/src/main/java/nl/dannyj/mistral/models/completion/DeltaChoice.java index 55d6876..bef775d 100644 --- a/src/main/java/nl/dannyj/mistral/models/completion/DeltaChoice.java +++ b/src/main/java/nl/dannyj/mistral/models/completion/DeltaChoice.java @@ -16,14 +16,18 @@ package nl.dannyj.mistral.models.completion; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.annotation.Nullable; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.ToString; +import nl.dannyj.mistral.models.completion.message.DeltaMessage; /** - * Represents a choice of a streamed message chunk in a completion. A choice contains the message that was generated and the reason for the completion to finish. + * Represents a delta update within a choice during a streamed chat completion. + * Contains partial information about the message delta. */ @Getter @AllArgsConstructor @@ -39,22 +43,33 @@ public class DeltaChoice { private int index; /** - * The message that was generated. + * The delta containing partial message updates. * - * @return the message that was generated + * @param delta The delta message object. + * @return The delta message object. */ - @JsonProperty("delta") - private Message message; + private DeltaMessage delta; /** * Reason for the completion to finish. + * Can be null if the stream is not finished. * - * @return the reason for the completion to finish + * @return the reason for the completion to finish, or null. */ + @Nullable @JsonProperty("finish_reason") - private String finishReason; + private FinishReason finishReason; - @JsonProperty("logprobs") - private String logProbs; + /** + * Gets the text content of the delta. + * This is a convenience method that extracts the text from all TextChunks, ignoring other types of content. + * + * @return The concatenated text content of the message from the delta, or null if the delta is null. + */ + @Nullable + @JsonIgnore + public String getTextContent() { + return delta != null ? delta.getTextContent() : null; + } } diff --git a/src/main/java/nl/dannyj/mistral/models/completion/FinishReason.java b/src/main/java/nl/dannyj/mistral/models/completion/FinishReason.java new file mode 100644 index 0000000..6b85d6e --- /dev/null +++ b/src/main/java/nl/dannyj/mistral/models/completion/FinishReason.java @@ -0,0 +1,66 @@ +/* + * Copyright 2024-2025 Danny Jelsma + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nl.dannyj.mistral.models.completion; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Represents the reason why a chat completion generation finished. + */ +public enum FinishReason { + + /** + * The model hit a natural stop point or a provided stop sequence. + */ + STOP("stop"), + + /** + * The maximum number of tokens specified in the request was reached. + */ + LENGTH("length"), + + /** + * The context length of the model was exceeded. + */ + MODEL_LENGTH("model_length"), + + /** + * The generation stopped due to an error. + */ + ERROR("error"), + + /** + * The model decided to call one or more tools. + */ + TOOL_CALLS("tool_calls"); + + private final String reason; + + FinishReason(String reason) { + this.reason = reason; + } + + /** + * Gets the string representation of the finish reason. + * + * @return The finish reason as a string. + */ + @JsonValue + public String getReason() { + return reason; + } +} diff --git a/src/main/java/nl/dannyj/mistral/models/completion/Message.java b/src/main/java/nl/dannyj/mistral/models/completion/Prediction.java similarity index 52% rename from src/main/java/nl/dannyj/mistral/models/completion/Message.java rename to src/main/java/nl/dannyj/mistral/models/completion/Prediction.java index 38c1f87..28b1b0b 100644 --- a/src/main/java/nl/dannyj/mistral/models/completion/Message.java +++ b/src/main/java/nl/dannyj/mistral/models/completion/Prediction.java @@ -16,49 +16,39 @@ package nl.dannyj.mistral.models.completion; -import com.fasterxml.jackson.annotation.JsonProperty; import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; -import java.util.List; - /** - * A message in a conversation. A message contains the role of the message, and the content of the message. + * Represents the predicted output to optimize response time for ChatCompletionRequest. + * See the Predicted Outputs guide for more details. */ @Data -@AllArgsConstructor +@Builder @NoArgsConstructor -public class Message { +@AllArgsConstructor +public class Prediction { /** - * The role of the message. - * Currently, there are 3 roles: user, assistant, and system. + * The type of the prediction. Currently, only "content" is supported. * - * @param role The role of the message. - * @return The role of the message. + * @param type The type of the prediction. + * @return The type of the prediction. */ @NotNull - private MessageRole role; + @Builder.Default + private String type = "content"; /** - * The content of the message. + * The predicted content. * - * @param content The content of the message. - * @return The content of the message. + * @param content The predicted content string. + * @return The predicted content string. */ @NotNull - private String content; - - /** - * Unimplemented. Don't use. - */ - @JsonProperty("tool_calls") - private List toolCalls; - - public Message(MessageRole role, String content) { - this.role = role; - this.content = content; - } + @Builder.Default + private String content = ""; } diff --git a/src/main/java/nl/dannyj/mistral/models/completion/ResponseFormat.java b/src/main/java/nl/dannyj/mistral/models/completion/ResponseFormat.java index 7616e82..791e076 100644 --- a/src/main/java/nl/dannyj/mistral/models/completion/ResponseFormat.java +++ b/src/main/java/nl/dannyj/mistral/models/completion/ResponseFormat.java @@ -16,12 +16,15 @@ package nl.dannyj.mistral.models.completion; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.annotation.Nullable; import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.ToString; +import nl.dannyj.mistral.models.completion.tool.JsonSchema; @Getter @Setter @@ -42,4 +45,22 @@ public class ResponseFormat { @NotNull private ResponseFormats type = ResponseFormats.TEXT; + /** + * The JSON schema definition to use when type is JSON_SCHEMA. + * + * @param jsonSchema The JSON schema definition. + * @return The JSON schema definition. + */ + @JsonProperty("json_schema") + @Nullable + private JsonSchema jsonSchema = null; + + /** + * Constructor for creating a ResponseFormat object with a specified type. + * + * @param type The type of the response format. + */ + public ResponseFormat(ResponseFormats type) { + this.type = type; + } } diff --git a/src/main/java/nl/dannyj/mistral/models/completion/ResponseFormats.java b/src/main/java/nl/dannyj/mistral/models/completion/ResponseFormats.java index 1182599..67117af 100644 --- a/src/main/java/nl/dannyj/mistral/models/completion/ResponseFormats.java +++ b/src/main/java/nl/dannyj/mistral/models/completion/ResponseFormats.java @@ -24,7 +24,8 @@ public enum ResponseFormats { TEXT("text"), - JSON("json_object"); + JSON_OBJECT("json_object"), + JSON_SCHEMA("json_schema"); private final String format; diff --git a/src/main/java/nl/dannyj/mistral/models/completion/content/ContentChunk.java b/src/main/java/nl/dannyj/mistral/models/completion/content/ContentChunk.java new file mode 100644 index 0000000..4173b6b --- /dev/null +++ b/src/main/java/nl/dannyj/mistral/models/completion/content/ContentChunk.java @@ -0,0 +1,43 @@ +/* + * Copyright 2024-2025 Danny Jelsma + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nl.dannyj.mistral.models.completion.content; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +/** + * Represents a part of the message content. The content of a message can be composed of multiple chunks of different types. + */ +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = TextChunk.class, name = "text"), + @JsonSubTypes.Type(value = ImageURLChunk.class, name = "image_url"), + @JsonSubTypes.Type(value = DocumentURLChunk.class, name = "document_url"), + @JsonSubTypes.Type(value = ReferenceChunk.class, name = "reference") +}) +public interface ContentChunk { + + /** + * Gets the type identifier for this content chunk. + * + * @return The type string (e.g., "text", "image_url"). + */ + String getType(); +} diff --git a/src/main/java/nl/dannyj/mistral/models/completion/content/DocumentURLChunk.java b/src/main/java/nl/dannyj/mistral/models/completion/content/DocumentURLChunk.java new file mode 100644 index 0000000..d66d4b3 --- /dev/null +++ b/src/main/java/nl/dannyj/mistral/models/completion/content/DocumentURLChunk.java @@ -0,0 +1,68 @@ +/* + * Copyright 2024-2025 Danny Jelsma + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nl.dannyj.mistral.models.completion.content; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.net.URI; + +/** + * Represents a document URL part of the message content. + */ +@NoArgsConstructor +@AllArgsConstructor +public class DocumentURLChunk implements ContentChunk { + + /** + * The URI of the document. + * + * @param documentUrl The document URI. + * @return The document URI. + */ + @NotNull + @JsonProperty("document_url") + @Getter + private URI documentUrl; + + /** + * The optional filename of the document. + * + * @param documentName The filename. + * @return The filename, or null if not specified. + */ + @Nullable + @JsonProperty("document_name") + @Getter + private String documentName; + + /** + * Gets the type identifier for this content chunk. + * + * @return The type string "document_url". + */ + @Override + @JsonIgnore + public String getType() { + return "document_url"; + } +} diff --git a/src/main/java/nl/dannyj/mistral/models/completion/content/ImageURL.java b/src/main/java/nl/dannyj/mistral/models/completion/content/ImageURL.java new file mode 100644 index 0000000..9e7d3ef --- /dev/null +++ b/src/main/java/nl/dannyj/mistral/models/completion/content/ImageURL.java @@ -0,0 +1,52 @@ +/* + * Copyright 2024-2025 Danny Jelsma + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nl.dannyj.mistral.models.completion.content; + +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.net.URI; + +/** + * Represents the URL and optional detail level for an image within a message content chunk. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ImageURL { + + /** + * The URI of the image. Can be a standard web URI (http/https) or a data URI (e.g., data:image/png;base64,...). + * + * @param url The image URI. + * @return The image URI. + */ + @NotNull + private URI url; + + /** + * Specifies the detail level of the image. Valid values have not been documented, recommended to leave at null. + * + * @param detail The detail level string. + * @return The detail level string, or null if not specified. + */ + @Nullable + private String detail; +} diff --git a/src/main/java/nl/dannyj/mistral/models/completion/content/ImageURLChunk.java b/src/main/java/nl/dannyj/mistral/models/completion/content/ImageURLChunk.java new file mode 100644 index 0000000..63473f4 --- /dev/null +++ b/src/main/java/nl/dannyj/mistral/models/completion/content/ImageURLChunk.java @@ -0,0 +1,54 @@ +/* + * Copyright 2024-2025 Danny Jelsma + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nl.dannyj.mistral.models.completion.content; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * Represents an image URL part of the message content. + */ +@NoArgsConstructor +@AllArgsConstructor +public class ImageURLChunk implements ContentChunk { + + /** + * The image URL details. + * + * @param imageUrl The ImageURL object containing the URI and optional detail. + * @return The ImageURL object. + */ + @NotNull + @JsonProperty("image_url") + @Getter + private ImageURL imageUrl; + + /** + * Gets the type identifier for this content chunk. + * + * @return The type string "image_url". + */ + @Override + @JsonIgnore + public String getType() { + return "image_url"; + } +} diff --git a/src/main/java/nl/dannyj/mistral/models/completion/content/ReferenceChunk.java b/src/main/java/nl/dannyj/mistral/models/completion/content/ReferenceChunk.java new file mode 100644 index 0000000..1cd07d5 --- /dev/null +++ b/src/main/java/nl/dannyj/mistral/models/completion/content/ReferenceChunk.java @@ -0,0 +1,58 @@ +/* + * Copyright 2024-2025 Danny Jelsma + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nl.dannyj.mistral.models.completion.content; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * Represents a reference part of the message content, linking to specific source materials or documents. + */ +@NoArgsConstructor +@AllArgsConstructor +public class ReferenceChunk implements ContentChunk { + + /** + * A list of integer identifiers referencing related documents or sources. + * + * @param referenceIds The list of reference IDs. + * @return The list of reference IDs. + */ + @NotNull + @NotEmpty + @JsonProperty("reference_ids") + @Getter + private List referenceIds; + + /** + * Gets the type identifier for this content chunk. + * + * @return The type string "reference". + */ + @Override + @JsonIgnore + public String getType() { + return "reference"; + } +} diff --git a/src/main/java/nl/dannyj/mistral/models/completion/content/TextChunk.java b/src/main/java/nl/dannyj/mistral/models/completion/content/TextChunk.java new file mode 100644 index 0000000..3e93ef6 --- /dev/null +++ b/src/main/java/nl/dannyj/mistral/models/completion/content/TextChunk.java @@ -0,0 +1,52 @@ +/* + * Copyright 2024-2025 Danny Jelsma + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nl.dannyj.mistral.models.completion.content; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * Represents a text part of the message content. + */ +@NoArgsConstructor +@AllArgsConstructor +public class TextChunk implements ContentChunk { + + /** + * The actual text content of this chunk. + * + * @param text The text content. + * @return The text content. + */ + @NotNull + @Getter + private String text; + + /** + * Gets the type identifier for this content chunk. + * + * @return The type string "text". + */ + @Override + @JsonIgnore + public String getType() { + return "text"; + } +} diff --git a/src/main/java/nl/dannyj/mistral/models/completion/message/AssistantMessage.java b/src/main/java/nl/dannyj/mistral/models/completion/message/AssistantMessage.java new file mode 100644 index 0000000..462fa66 --- /dev/null +++ b/src/main/java/nl/dannyj/mistral/models/completion/message/AssistantMessage.java @@ -0,0 +1,109 @@ +/* + * Copyright 2024-2025 Danny Jelsma + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nl.dannyj.mistral.models.completion.message; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import nl.dannyj.mistral.models.completion.content.ContentChunk; +import nl.dannyj.mistral.models.completion.content.TextChunk; +import nl.dannyj.mistral.models.completion.tool.ToolCall; + +import java.util.Collections; +import java.util.List; + +/** + * Represents a message with the 'assistant' role in a chat conversation. + * Assistant messages contain the model's responses, which might include text content and/or tool calls. + */ +@EqualsAndHashCode(callSuper = false) +@NoArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class AssistantMessage extends ChatMessage { + + /** + * A list of tool calls requested by the model. Can be null if the message only contains text content. + * + * @param toolCalls The list of tool calls, or null. + * @return The list of tool calls, or null. + */ + @Nullable + @JsonProperty("tool_calls") + @Getter + @Setter + private List toolCalls; + + /** + * Indicates if this assistant message acts as a prefix to guide the model's subsequent response. + * Defaults to false. + * + * @param prefix True if this is a prefix message, false otherwise. + * @return The prefix flag state. + */ + @Getter + @Setter + private boolean prefix = false; + + /** + * Constructs an AssistantMessage with text content. + * + * @param textContent The text content. + */ + public AssistantMessage(@NotNull String textContent) { + this.content = Collections.singletonList(new TextChunk(textContent)); + this.toolCalls = null; + } + + /** + * Constructs an AssistantMessage with tool calls. + * + * @param toolCalls The list of tool calls. Cannot be null or empty. + */ + public AssistantMessage(@NotNull @NotEmpty List toolCalls) { + this.content = null; + this.toolCalls = toolCalls; + } + + /** + * Constructs an AssistantMessage with both text content and tool calls. + * Use setters if only one is needed or to set the prefix flag. + * + * @param content The list of content chunks. Can be null. + * @param toolCalls The list of tool calls. Can be null. + */ + public AssistantMessage(@Nullable List content, @Nullable List toolCalls) { + this.content = content; + this.toolCalls = toolCalls; + } + + /** + * Gets the role of this message. + * + * @return Always returns {@link MessageRole#ASSISTANT}. + */ + @Override + public MessageRole getRole() { + return MessageRole.ASSISTANT; + } + +} diff --git a/src/main/java/nl/dannyj/mistral/models/completion/message/ChatMessage.java b/src/main/java/nl/dannyj/mistral/models/completion/message/ChatMessage.java new file mode 100644 index 0000000..3007374 --- /dev/null +++ b/src/main/java/nl/dannyj/mistral/models/completion/message/ChatMessage.java @@ -0,0 +1,94 @@ +/* + * Copyright 2024-2025 Danny Jelsma + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nl.dannyj.mistral.models.completion.message; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import jakarta.annotation.Nullable; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import nl.dannyj.mistral.models.completion.content.ContentChunk; +import nl.dannyj.mistral.models.completion.content.TextChunk; +import nl.dannyj.mistral.serialization.ContentChunkListDeserializer; + +import java.util.List; + +/** + * Represents a single message in a chat conversation, serving as the base for different message types (system, user, assistant, tool). + * This class uses Jackson polymorphism based on the 'role' property found in the JSON. + */ +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "role" +) +@JsonSubTypes({ + @JsonSubTypes.Type(value = SystemMessage.class, name = "system"), + @JsonSubTypes.Type(value = UserMessage.class, name = "user"), + @JsonSubTypes.Type(value = AssistantMessage.class, name = "assistant"), + @JsonSubTypes.Type(value = ToolMessage.class, name = "tool") +}) +public abstract class ChatMessage { + + /** + * The content of the message. Can be null or a list of content chunks. + * + * @param content The list of content chunks, or null. + * @return The list of content chunks, or null. + */ + @Nullable + @JsonDeserialize(using = ContentChunkListDeserializer.class) + protected List content; + + /** + * Gets the role of the message sender (e.g., system, user, assistant). + * Subclasses must implement this to provide their specific role. + * + * @return The role of the message. + */ + public abstract MessageRole getRole(); + + /** + * Gets the text content of the message. + * This is a convenience method that extracts the text from all TextChunks, ignoring other types of content. + * + * @return The concatenated text content of the message, or null if no text content is present. + */ + @Nullable + @JsonIgnore + public String getTextContent() { + if (content == null) { + return null; + } + + StringBuilder textContent = new StringBuilder(); + + for (ContentChunk chunk : content) { + if (chunk instanceof TextChunk textChunk) { + textContent.append(textChunk.getText()); + } + } + + return textContent.toString(); + } + +} diff --git a/src/main/java/nl/dannyj/mistral/models/completion/message/DeltaMessage.java b/src/main/java/nl/dannyj/mistral/models/completion/message/DeltaMessage.java new file mode 100644 index 0000000..f3a86fa --- /dev/null +++ b/src/main/java/nl/dannyj/mistral/models/completion/message/DeltaMessage.java @@ -0,0 +1,97 @@ +/* + * Copyright 2024-2025 Danny Jelsma + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nl.dannyj.mistral.models.completion.message; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import jakarta.annotation.Nullable; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import nl.dannyj.mistral.models.completion.content.ContentChunk; +import nl.dannyj.mistral.models.completion.content.TextChunk; +import nl.dannyj.mistral.models.completion.tool.ToolCall; +import nl.dannyj.mistral.serialization.ContentChunkListDeserializer; + +import java.util.List; + +/** + * Represents the delta content within a streamed chat completion choice. + * Contains partial updates for role, content, or tool calls. + */ +@Getter +@AllArgsConstructor +@NoArgsConstructor +@ToString +public class DeltaMessage { + + /** + * The role of the message sender (e.g., assistant). Only sent if it changes or at the start. + * + * @param role The message role, or null if not present in this delta. + * @return The message role, or null. + */ + @Nullable + private MessageRole role; + + /** + * A part of the message content. Can be null if this delta updates role or tool calls. + * Can be a string or a list of ContentChunks. + * + * @param content The partial content list or string, or null. + * @return The partial content list or string, or null. + */ + @Nullable + @JsonDeserialize(using = ContentChunkListDeserializer.class) + private List content; + + /** + * A partial list of tool calls requested by the model. Can be null. + * + * @param toolCalls The partial list of tool calls, or null. + * @return The partial list of tool calls, or null. + */ + @Nullable + @JsonProperty("tool_calls") + private List toolCalls; + + /** + * Gets the text content of the delta message. + * This is a convenience method that extracts the text from all TextChunks, ignoring other types of content. + * + * @return The concatenated text content of the message, or null if no text content is present. + */ + @Nullable + @JsonIgnore + public String getTextContent() { + if (content == null) { + return null; + } + + StringBuilder textContent = new StringBuilder(); + + for (ContentChunk chunk : content) { + if (chunk instanceof TextChunk textChunk && textChunk.getText() != null) { + textContent.append(textChunk.getText()); + } + } + + return !textContent.isEmpty() ? textContent.toString() : null; + } +} diff --git a/src/main/java/nl/dannyj/mistral/models/completion/MessageChunk.java b/src/main/java/nl/dannyj/mistral/models/completion/message/MessageChunk.java similarity index 94% rename from src/main/java/nl/dannyj/mistral/models/completion/MessageChunk.java rename to src/main/java/nl/dannyj/mistral/models/completion/message/MessageChunk.java index 7bf5d44..80bfbde 100644 --- a/src/main/java/nl/dannyj/mistral/models/completion/MessageChunk.java +++ b/src/main/java/nl/dannyj/mistral/models/completion/message/MessageChunk.java @@ -14,11 +14,12 @@ * limitations under the License. */ -package nl.dannyj.mistral.models.completion; +package nl.dannyj.mistral.models.completion.message; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; +import nl.dannyj.mistral.models.completion.DeltaChoice; import nl.dannyj.mistral.models.usage.Usage; import java.util.List; diff --git a/src/main/java/nl/dannyj/mistral/models/completion/MessageRole.java b/src/main/java/nl/dannyj/mistral/models/completion/message/MessageRole.java similarity index 92% rename from src/main/java/nl/dannyj/mistral/models/completion/MessageRole.java rename to src/main/java/nl/dannyj/mistral/models/completion/message/MessageRole.java index 89087f2..23e374a 100644 --- a/src/main/java/nl/dannyj/mistral/models/completion/MessageRole.java +++ b/src/main/java/nl/dannyj/mistral/models/completion/message/MessageRole.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package nl.dannyj.mistral.models.completion; +package nl.dannyj.mistral.models.completion.message; import com.fasterxml.jackson.annotation.JsonValue; @@ -25,7 +25,8 @@ public enum MessageRole { SYSTEM("system"), ASSISTANT("assistant"), - USER("user"); + USER("user"), + TOOL("tool"); private final String role; diff --git a/src/main/java/nl/dannyj/mistral/models/completion/message/SystemMessage.java b/src/main/java/nl/dannyj/mistral/models/completion/message/SystemMessage.java new file mode 100644 index 0000000..83ffa46 --- /dev/null +++ b/src/main/java/nl/dannyj/mistral/models/completion/message/SystemMessage.java @@ -0,0 +1,49 @@ +/* + * Copyright 2024-2025 Danny Jelsma + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nl.dannyj.mistral.models.completion.message; + +import jakarta.annotation.Nullable; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import nl.dannyj.mistral.models.completion.content.TextChunk; + +import java.util.ArrayList; + +/** + * Represents a message with the 'system' role in a chat conversation. + * System messages are used to provide high-level instructions or context to the model. + */ +@EqualsAndHashCode(callSuper = false) +@NoArgsConstructor +public class SystemMessage extends ChatMessage { + + public SystemMessage(@Nullable String content) { + this.content = new ArrayList<>(); + + this.content.add(new TextChunk(content)); + } + + /** + * Gets the role of this message. + * + * @return Always returns {@link MessageRole#SYSTEM}. + */ + @Override + public MessageRole getRole() { + return MessageRole.SYSTEM; + } +} diff --git a/src/main/java/nl/dannyj/mistral/models/completion/message/ToolMessage.java b/src/main/java/nl/dannyj/mistral/models/completion/message/ToolMessage.java new file mode 100644 index 0000000..74c5622 --- /dev/null +++ b/src/main/java/nl/dannyj/mistral/models/completion/message/ToolMessage.java @@ -0,0 +1,97 @@ +/* + * Copyright 2024-2025 Danny Jelsma + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nl.dannyj.mistral.models.completion.message; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import nl.dannyj.mistral.models.completion.content.ContentChunk; +import nl.dannyj.mistral.models.completion.content.TextChunk; + +import java.util.Collections; +import java.util.List; + +/** + * Represents a message with the 'tool' role in a chat conversation. + * Tool messages are used to provide the results of a tool call back to the model. + */ +@EqualsAndHashCode(callSuper = false) +@NoArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ToolMessage extends ChatMessage { + + /** + * The ID of the tool call that this message is a response to. Can be null. + * + * @param toolCallId The tool call ID. + * @return The tool call ID. + */ + @Nullable + @JsonProperty("tool_call_id") + @Getter + @Setter + private String toolCallId; + + /** + * The name associated with the tool call. Can be null. + * + * @param name The name. + * @return The name. + */ + @Nullable + @Getter + @Setter + private String name; + + /** + * Constructs a ToolMessage with simple text content and the corresponding tool call ID. + * + * @param textContent The text content (result) of the tool call. Cannot be null. + * @param toolCallId The ID of the tool call this message responds to. Can be null. + */ + public ToolMessage(@NotNull String textContent, @Nullable String toolCallId) { + this.content = Collections.singletonList(new TextChunk(textContent)); + this.toolCallId = toolCallId; + } + + /** + * Constructs a ToolMessage with potentially complex content and the corresponding tool call ID. + * + * @param contentChunks The list of content chunks representing the tool result. Cannot be null or empty. + * @param toolCallId The ID of the tool call this message responds to. Can be null. + */ + public ToolMessage(@NotNull @jakarta.validation.constraints.NotEmpty List contentChunks, @Nullable String toolCallId) { + this.content = contentChunks; + this.toolCallId = toolCallId; + } + + + /** + * Gets the role of this message. + * + * @return Always returns {@link MessageRole#TOOL}. + */ + @Override + public MessageRole getRole() { + return MessageRole.TOOL; + } +} diff --git a/src/main/java/nl/dannyj/mistral/models/completion/message/UserMessage.java b/src/main/java/nl/dannyj/mistral/models/completion/message/UserMessage.java new file mode 100644 index 0000000..5bd7c4e --- /dev/null +++ b/src/main/java/nl/dannyj/mistral/models/completion/message/UserMessage.java @@ -0,0 +1,68 @@ +/* + * Copyright 2024-2025 Danny Jelsma + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nl.dannyj.mistral.models.completion.message; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import nl.dannyj.mistral.models.completion.content.ContentChunk; +import nl.dannyj.mistral.models.completion.content.TextChunk; + +import java.util.Collections; +import java.util.List; + +/** + * Represents a message with the 'user' role in a chat conversation. + * User messages contain the input provided by the end-user. + */ +@EqualsAndHashCode(callSuper = false) +@NoArgsConstructor +public class UserMessage extends ChatMessage { + + /** + * Constructs a new UserMessage with simple text content. + * + * @param textContent The text content for the user message. Cannot be null or empty. + */ + public UserMessage(@NotNull String textContent) { + if (textContent.isEmpty()) { + throw new IllegalArgumentException("User message text content cannot be empty."); + } + this.content = Collections.singletonList(new TextChunk(textContent)); + } + + /** + * Constructs a new UserMessage with a list of content chunks (for multi-modal input). + * + * @param contentChunks The list of content chunks. Cannot be null or empty. + */ + public UserMessage(@NotNull @NotEmpty List contentChunks) { + this.content = contentChunks; + } + + + /** + * Gets the role of this message. + * + * @return Always returns {@link MessageRole#USER}. + */ + @Override + public MessageRole getRole() { + return MessageRole.USER; + } +} diff --git a/src/main/java/nl/dannyj/mistral/models/completion/tool/Function.java b/src/main/java/nl/dannyj/mistral/models/completion/tool/Function.java new file mode 100644 index 0000000..933ba0a --- /dev/null +++ b/src/main/java/nl/dannyj/mistral/models/completion/tool/Function.java @@ -0,0 +1,84 @@ +/* + * Copyright 2024-2025 Danny Jelsma + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nl.dannyj.mistral.models.completion.tool; + +import com.fasterxml.jackson.annotation.JsonRawValue; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Represents the definition of a function that can be called by the model. + * This is used when defining tools available to the model in a ChatCompletionRequest. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Function { + + /** + * The name of the function to be called. Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length of 64. + * + * @param name The name of the function. + * @return The name of the function. + */ + @NotNull + @NotBlank + @Size(max = 64) + @Pattern(regexp = "^[a-zA-Z0-9_-]{1,64}$", message = "Function name must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length of 64.") + private String name; + + /** + * A description of what the function does, used by the model to choose when and how to call the function. + * + * @param description The description of the function. + * @return The description of the function. + */ + @Builder.Default + private String description = ""; + + /** + * The parameters the function accepts, described as a JSON Schema object. + * See the guide for more information. + * The user of this SDK should provide a valid JSON string representing the schema. + * + * @param parameters A JSON string representing the parameters schema. + * @return A JSON string representing the parameters schema. + */ + @NotNull + @NotBlank + @JsonRawValue + private String parameters; + + /** + * Whether to enable strict schema adherence for the function parameters. + * When true, the model will make a best effort to adhere to the JSON schema. + * See the guide for more details. + * Defaults to false. + * + * @param strict Whether strict schema adherence is enabled. + * @return True if strict schema adherence is enabled, false otherwise. + */ + @Builder.Default + private boolean strict = false; +} diff --git a/src/main/java/nl/dannyj/mistral/models/completion/tool/FunctionCall.java b/src/main/java/nl/dannyj/mistral/models/completion/tool/FunctionCall.java new file mode 100644 index 0000000..300fcd8 --- /dev/null +++ b/src/main/java/nl/dannyj/mistral/models/completion/tool/FunctionCall.java @@ -0,0 +1,50 @@ +/* + * Copyright 2024-2025 Danny Jelsma + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nl.dannyj.mistral.models.completion.tool; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Represents the function call requested by the model, including the function name and its arguments as a JSON string. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class FunctionCall { + + /** + * The name of the function to call. Cannot be blank. + * + * @param name The function name. + * @return The function name. + */ + @NotBlank + private String name; + + /** + * The arguments to call the function with, represented as a JSON string. + * + * @param arguments The function arguments as a JSON string. + * @return The function arguments JSON string. + */ + @NotNull + private String arguments; +} diff --git a/src/main/java/nl/dannyj/mistral/models/completion/tool/FunctionName.java b/src/main/java/nl/dannyj/mistral/models/completion/tool/FunctionName.java new file mode 100644 index 0000000..6611b86 --- /dev/null +++ b/src/main/java/nl/dannyj/mistral/models/completion/tool/FunctionName.java @@ -0,0 +1,46 @@ +/* + * Copyright 2024-2025 Danny Jelsma + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nl.dannyj.mistral.models.completion.tool; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Represents the name of a function, used when specifying a particular function for tool_choice. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class FunctionName { + + /** + * The name of the function. Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length of 64. + * + * @param name The name of the function. + * @return The name of the function. + */ + @NotNull + @NotBlank + @Size(max = 64) + @Pattern(regexp = "^[a-zA-Z0-9_-]{1,64}$", message = "Function name must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length of 64.") + private String name; +} diff --git a/src/main/java/nl/dannyj/mistral/models/completion/tool/JsonSchema.java b/src/main/java/nl/dannyj/mistral/models/completion/tool/JsonSchema.java new file mode 100644 index 0000000..c8296c1 --- /dev/null +++ b/src/main/java/nl/dannyj/mistral/models/completion/tool/JsonSchema.java @@ -0,0 +1,74 @@ +/* + * Copyright 2024-2025 Danny Jelsma + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nl.dannyj.mistral.models.completion.tool; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +/** + * Represents the JSON schema definition used within ResponseFormat when type is json_schema. + */ +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +@ToString +public class JsonSchema { + + /** + * The name of the schema. + * + * @param name The name of the schema. + * @return The name of the schema. + */ + @NotNull + private String name; + + /** + * An optional description of the schema. + * + * @param description The description of the schema. + * @return The description of the schema. + */ + private String description; + + /** + * The JSON schema definition, represented as a String. + * + * @param schema The string representing the JSON schema. + * @return The string representing the JSON schema. + */ + @NotNull + @JsonProperty("schema") + private String schema; + + /** + * Whether the schema should be strictly adhered to. Defaults to false. + * + * @param strict Whether the schema is strict. + * @return Whether the schema is strict. + */ + @Builder.Default + private boolean strict = false; +} diff --git a/src/main/java/nl/dannyj/mistral/models/completion/tool/SpecificToolChoice.java b/src/main/java/nl/dannyj/mistral/models/completion/tool/SpecificToolChoice.java new file mode 100644 index 0000000..df6a79e --- /dev/null +++ b/src/main/java/nl/dannyj/mistral/models/completion/tool/SpecificToolChoice.java @@ -0,0 +1,53 @@ +/* + * Copyright 2024-2025 Danny Jelsma + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nl.dannyj.mistral.models.completion.tool; + +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Represents the object variant for the 'tool_choice' parameter in a ChatCompletionRequest. + * This forces the model to call a specific function. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class SpecificToolChoice implements ToolChoiceOption { + + /** + * The type of the tool to be called. Currently, only "function" is supported. + * + * @param type The type of the tool. + * @return The type of the tool. + */ + @NotNull + @Builder.Default + private ToolType type = ToolType.FUNCTION; + + /** + * The details of the function to be called. + * + * @param function The function name. + * @return The function name. + */ + @NotNull + private FunctionName function; +} diff --git a/src/main/java/nl/dannyj/mistral/models/completion/tool/Tool.java b/src/main/java/nl/dannyj/mistral/models/completion/tool/Tool.java new file mode 100644 index 0000000..cb52862 --- /dev/null +++ b/src/main/java/nl/dannyj/mistral/models/completion/tool/Tool.java @@ -0,0 +1,52 @@ +/* + * Copyright 2024-2025 Danny Jelsma + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nl.dannyj.mistral.models.completion.tool; + +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Represents a tool that the model can call. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Tool { + + /** + * The type of the tool. Currently, only "function" is supported. + * + * @param type The type of the tool. + * @return The type of the tool. + */ + @NotNull + @Builder.Default + private ToolType type = ToolType.FUNCTION; + + /** + * The function definition. Required if type is "function". + * + * @param function The function definition. + * @return The function definition. + */ + @NotNull + private Function function; +} diff --git a/src/main/java/nl/dannyj/mistral/models/completion/tool/ToolCall.java b/src/main/java/nl/dannyj/mistral/models/completion/tool/ToolCall.java new file mode 100644 index 0000000..78f7e03 --- /dev/null +++ b/src/main/java/nl/dannyj/mistral/models/completion/tool/ToolCall.java @@ -0,0 +1,73 @@ +/* + * Copyright 2024-2025 Danny Jelsma + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nl.dannyj.mistral.models.completion.tool; + +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Represents a tool call requested by the model as part of an Assistant message. + * Based on the 'ToolCall' definition in the Mistral AI API specification. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ToolCall { + + /** + * The unique identifier for this specific tool call. + * This ID is required when sending a response back using a ToolMessage. + * + * @param id The tool call ID. + * @return The tool call ID, or null if not provided by the API. + */ + @Nullable + private String id; + + /** + * The type of the tool being called. Currently only 'function' is supported. + * Defaults to FUNCTION if not specified. + * + * @param type The tool type. + * @return The tool type. + */ + @NotNull + private ToolType type = ToolType.FUNCTION; + + /** + * The details of the function to be called. Required. + * + * @param function The function call details. + * @return The function call details. + */ + @NotNull + private FunctionCall function; + + /** + * The index of the tool call in the list, if multiple calls are made. + * Defaults to 0 if not specified by the API. + * + * @param index The index of the tool call. + * @return The index. + */ + @Min(0) + private int index = 0; +} diff --git a/src/main/java/nl/dannyj/mistral/models/completion/tool/ToolChoiceEnum.java b/src/main/java/nl/dannyj/mistral/models/completion/tool/ToolChoiceEnum.java new file mode 100644 index 0000000..01ff020 --- /dev/null +++ b/src/main/java/nl/dannyj/mistral/models/completion/tool/ToolChoiceEnum.java @@ -0,0 +1,65 @@ +/* + * Copyright 2024-2025 Danny Jelsma + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nl.dannyj.mistral.models.completion.tool; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Defines the possible string values for the 'tool_choice' parameter in a ChatCompletionRequest. + * It controls how the model responds to tool calls. + */ +public enum ToolChoiceEnum implements ToolChoiceOption { + + /** + * The model can choose to call a tool or not. This is the default behavior. + */ + AUTO("auto"), + + /** + * The model will not call any tools. + */ + NONE("none"), + + /** + * The model is forced to call at least one tool. + */ + ANY("any"), + + /** + * The model is forced to call a specific tool (not directly represented by this enum, + * but this value indicates a tool must be called, potentially specified by an object). + * In the context of the enum, it implies a tool must be called. + * The OpenAPI spec also lists "required" as a string option. + */ + REQUIRED("required"); + + private final String value; + + ToolChoiceEnum(String value) { + this.value = value; + } + + /** + * Gets the string representation of the tool choice option. + * + * @return The tool choice option as a string. + */ + @JsonValue + public String getValue() { + return value; + } +} diff --git a/src/main/java/nl/dannyj/mistral/models/completion/tool/ToolChoiceOption.java b/src/main/java/nl/dannyj/mistral/models/completion/tool/ToolChoiceOption.java new file mode 100644 index 0000000..4ac50dc --- /dev/null +++ b/src/main/java/nl/dannyj/mistral/models/completion/tool/ToolChoiceOption.java @@ -0,0 +1,28 @@ +/* + * Copyright 2024-2025 Danny Jelsma + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nl.dannyj.mistral.models.completion.tool; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import nl.dannyj.mistral.serialization.ToolChoiceOptionDeserializer; + +/** + * Represents the possible options for the 'tool_choice' parameter in a ChatCompletionRequest. + * This can be either a predefined string enum (ToolChoiceEnum) or an object specifying a function (SpecificToolChoice). + */ +@JsonDeserialize(using = ToolChoiceOptionDeserializer.class) +public sealed interface ToolChoiceOption permits ToolChoiceEnum, SpecificToolChoice { +} diff --git a/src/main/java/nl/dannyj/mistral/models/completion/tool/ToolType.java b/src/main/java/nl/dannyj/mistral/models/completion/tool/ToolType.java new file mode 100644 index 0000000..29b55ee --- /dev/null +++ b/src/main/java/nl/dannyj/mistral/models/completion/tool/ToolType.java @@ -0,0 +1,48 @@ +/* + * Copyright 2024-2025 Danny Jelsma + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nl.dannyj.mistral.models.completion.tool; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Defines the type of a tool. + * Currently, only "function" is supported. + */ +public enum ToolType { + + /** + * Represents a function tool. + */ + FUNCTION("function"); + + private final String type; + + ToolType(String type) { + this.type = type; + } + + /** + * Gets the string representation of the tool type. + * This value is used for JSON serialization. + * + * @return The tool type as a string. + */ + @JsonValue + public String getType() { + return type; + } +} diff --git a/src/main/java/nl/dannyj/mistral/models/embedding/EmbeddingRequest.java b/src/main/java/nl/dannyj/mistral/models/embedding/EmbeddingRequest.java index 910062c..6766cc7 100644 --- a/src/main/java/nl/dannyj/mistral/models/embedding/EmbeddingRequest.java +++ b/src/main/java/nl/dannyj/mistral/models/embedding/EmbeddingRequest.java @@ -16,7 +16,6 @@ package nl.dannyj.mistral.models.embedding; -import com.fasterxml.jackson.annotation.JsonProperty; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; @@ -58,16 +57,4 @@ public class EmbeddingRequest implements Request { @Size(min = 1) private List input; - /** - * The format of the output data. - * - * @param encodingFormat The format of the output data. Can only be "float" is valid for now. - * @return The format of the output data. - */ - @JsonProperty("encoding_format") - @Builder.Default - @NotNull - @NotBlank - private String encodingFormat = "float"; - } diff --git a/src/main/java/nl/dannyj/mistral/models/embedding/FloatEmbedding.java b/src/main/java/nl/dannyj/mistral/models/embedding/FloatEmbedding.java index 04a0741..0bd48d5 100644 --- a/src/main/java/nl/dannyj/mistral/models/embedding/FloatEmbedding.java +++ b/src/main/java/nl/dannyj/mistral/models/embedding/FloatEmbedding.java @@ -37,7 +37,7 @@ public class FloatEmbedding { private String object; /** - * The embeddings for the input strings. See the mistral documentation for more details on embeddings. + * The embeddings for the input strings. See the mistral documentation for more details on embeddings. * * @return the float embeddings for the input string */ diff --git a/src/main/java/nl/dannyj/mistral/models/model/Model.java b/src/main/java/nl/dannyj/mistral/models/model/Model.java index e3b67a0..8c1ba63 100644 --- a/src/main/java/nl/dannyj/mistral/models/model/Model.java +++ b/src/main/java/nl/dannyj/mistral/models/model/Model.java @@ -22,11 +22,11 @@ import lombok.NoArgsConstructor; import lombok.ToString; +import java.time.OffsetDateTime; import java.util.List; /** - * The Model class represents a model in the Mistral AI API. - * Most of these fields are undocumented. + * Represents a model available in the Mistral AI API. */ @Getter @NoArgsConstructor @@ -41,6 +41,11 @@ public class Model { */ private String id; + /** + * The object type, which is always "model". + * + * @return The object type. + */ private String object; /** @@ -51,16 +56,89 @@ public class Model { private long created; /** - * Owner of the model. + * Owner of the model. Defaults to "mistralai". * * @return The owner of the model. */ @JsonProperty("owned_by") private String ownedBy; - private String root; + /** + * The capabilities of the model. + * + * @return The model capabilities. + */ + private ModelCapabilities capabilities; + + /** + * The name of the model. Can be null. + * + * @return The name of the model. + */ + private String name; + + /** + * The description of the model. Can be null. + * + * @return The description of the model. + */ + private String description; + + /** + * The maximum context length supported by the model. Defaults to 32768. + * + * @return The maximum context length. + */ + @JsonProperty("max_context_length") + private Integer maxContextLength; + + /** + * A list of aliases for the model. Defaults to an empty list. + * + * @return The list of aliases. + */ + private List aliases; - private String parent; + /** + * The deprecation date and time for the model. Can be null. + * + * @return The deprecation date and time. + */ + private OffsetDateTime deprecation; + + /** + * The default sampling temperature for the model. Can be null. + * + * @return The default model temperature. + */ + @JsonProperty("default_model_temperature") + private Double defaultModelTemperature; + + /** + * The type of the model, e.g., "base" or "fine-tuned". + * + * @return The type of the model. + */ + private String type; - private List permission; + /** + * The ID of the fine-tuning job that created this model. Only present for fine-tuned models. + * + * @return The job ID. + */ + private String job; + + /** + * The root model ID from which this fine-tuned model was derived. Only present for fine-tuned models. + * + * @return The root model ID. + */ + private String root; + + /** + * Indicates if the fine-tuned model is archived. Defaults to false. Only present for fine-tuned models. + * + * @return True if the model is archived, false otherwise. + */ + private Boolean archived; } diff --git a/src/main/java/nl/dannyj/mistral/models/model/ModelCapabilities.java b/src/main/java/nl/dannyj/mistral/models/model/ModelCapabilities.java new file mode 100644 index 0000000..a61826a --- /dev/null +++ b/src/main/java/nl/dannyj/mistral/models/model/ModelCapabilities.java @@ -0,0 +1,103 @@ +/* + * Copyright 2024-2025 Danny Jelsma + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nl.dannyj.mistral.models.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * Represents the capabilities of a Mistral AI model. + */ +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class ModelCapabilities { + + /** + * Indicates if the model supports chat completions. + * + * @return True if chat completion is supported, false otherwise. + */ + @JsonProperty("completion_chat") + private Boolean completionChat; + + /** + * Indicates if the model supports fill-in-the-middle completions. + * + * @return True if FIM completion is supported, false otherwise. + */ + @JsonProperty("completion_fim") + private Boolean completionFim; + + /** + * Indicates if the model supports function calling. + * + * @return True if function calling is supported, false otherwise. + */ + @JsonProperty("function_calling") + private Boolean functionCalling; + + /** + * Indicates if the model supports fine-tuning. + * + * @return True if fine-tuning is supported, false otherwise. + */ + @JsonProperty("fine_tuning") + private Boolean fineTuning; + + /** + * Indicates if the model has vision capabilities. + * + * @return True if vision capabilities are supported, false otherwise. + */ + @JsonProperty("vision") + private Boolean vision; + + /** + * Indicates if the model has classification capabilities (relevant for fine-tuned models). + * + * @return True if classification capabilities are supported, false otherwise. + */ + @JsonProperty("classification") + private Boolean classification; + + public boolean supportsChatCompletion() { + return completionChat; + } + + public boolean supportsFimCompletion() { + return completionFim; + } + + public boolean supportsFunctionCalling() { + return functionCalling; + } + + public boolean supportsFineTuning() { + return fineTuning; + } + + public boolean supportsVision() { + return vision; + } + + public boolean supportsClassification() { + return classification; + } +} diff --git a/src/main/java/nl/dannyj/mistral/models/model/ModelPermission.java b/src/main/java/nl/dannyj/mistral/models/model/ModelPermission.java deleted file mode 100644 index 3368ed0..0000000 --- a/src/main/java/nl/dannyj/mistral/models/model/ModelPermission.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2024-2025 Danny Jelsma - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package nl.dannyj.mistral.models.model; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.ToString; - -/** - * All of these are currently not documented by the API docs. - */ -@Getter -@NoArgsConstructor -@AllArgsConstructor -@ToString -public class ModelPermission { - - private String id; - - private String object; - - private long created; - - @JsonProperty("allow_create_engine") - private boolean allowCreateEngine; - - @JsonProperty("allow_sampling") - private boolean allowSampling; - - @JsonProperty("allow_logprobs") - private boolean allowLogprobs; - - @JsonProperty("allow_search_indices") - private boolean allowSearchIndices; - - @JsonProperty("allow_view") - private boolean allowView; - - @JsonProperty("allow_fine_tuning") - private boolean allowFineTuning; - - private String organization; - - private String group; - - @JsonProperty("is_blocking") - private boolean isBlocking; - -} diff --git a/src/main/java/nl/dannyj/mistral/net/ChatCompletionChunkCallback.java b/src/main/java/nl/dannyj/mistral/net/ChatCompletionChunkCallback.java index ba62d80..f05619d 100644 --- a/src/main/java/nl/dannyj/mistral/net/ChatCompletionChunkCallback.java +++ b/src/main/java/nl/dannyj/mistral/net/ChatCompletionChunkCallback.java @@ -16,7 +16,7 @@ package nl.dannyj.mistral.net; -import nl.dannyj.mistral.models.completion.MessageChunk; +import nl.dannyj.mistral.models.completion.message.MessageChunk; /** * Interface for handling streaming chat completion requests. diff --git a/src/main/java/nl/dannyj/mistral/serialization/ContentChunkListDeserializer.java b/src/main/java/nl/dannyj/mistral/serialization/ContentChunkListDeserializer.java new file mode 100644 index 0000000..75d5b78 --- /dev/null +++ b/src/main/java/nl/dannyj/mistral/serialization/ContentChunkListDeserializer.java @@ -0,0 +1,70 @@ +/* + * Copyright 2024-2025 Danny Jelsma + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nl.dannyj.mistral.serialization; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.deser.ContextualDeserializer; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import nl.dannyj.mistral.models.completion.content.ContentChunk; +import nl.dannyj.mistral.models.completion.content.TextChunk; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +public class ContentChunkListDeserializer extends StdDeserializer> implements ContextualDeserializer { + + private JsonDeserializer defaultDeserializer; + + public ContentChunkListDeserializer() { + this(null); + } + + public ContentChunkListDeserializer(Class vc) { + super(vc); + } + + @Override + public List deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + if (p.currentToken() == JsonToken.VALUE_STRING) { + String textContent = p.getText(); + return Collections.singletonList(new TextChunk(textContent)); + } + + if (defaultDeserializer != null) { + return (List) defaultDeserializer.deserialize(p, ctxt); + } + + return ctxt.readValue(p, ctxt.constructType(List.class)); + } + + @Override + public JsonDeserializer createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException { + JavaType listType = ctxt.getContextualType(); + JsonDeserializer deser = ctxt.findContextualValueDeserializer(listType, property); + + ContentChunkListDeserializer contextualDeserializer = new ContentChunkListDeserializer(); + contextualDeserializer.defaultDeserializer = deser; + return contextualDeserializer; + } +} diff --git a/src/main/java/nl/dannyj/mistral/serialization/ToolChoiceOptionDeserializer.java b/src/main/java/nl/dannyj/mistral/serialization/ToolChoiceOptionDeserializer.java new file mode 100644 index 0000000..bfedb83 --- /dev/null +++ b/src/main/java/nl/dannyj/mistral/serialization/ToolChoiceOptionDeserializer.java @@ -0,0 +1,60 @@ +/* + * Copyright 2024-2025 Danny Jelsma + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nl.dannyj.mistral.serialization; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import nl.dannyj.mistral.models.completion.tool.SpecificToolChoice; +import nl.dannyj.mistral.models.completion.tool.ToolChoiceEnum; +import nl.dannyj.mistral.models.completion.tool.ToolChoiceOption; + +import java.io.IOException; + +/** + * Custom deserializer for the ToolChoiceOption sealed interface. + * It handles deserialization from either a JSON string (for ToolChoiceEnum) + * or a JSON object (for SpecificToolChoice). + */ +public class ToolChoiceOptionDeserializer extends JsonDeserializer { + + @Override + public ToolChoiceOption deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException { + ObjectMapper mapper = (ObjectMapper) jp.getCodec(); + JsonToken token = jp.currentToken(); + + if (token == JsonToken.VALUE_STRING) { + String enumValue = jp.getText().toUpperCase(); + try { + return ToolChoiceEnum.valueOf(enumValue); + } catch (IllegalArgumentException e) { + if ("ANY".equalsIgnoreCase(enumValue)) return ToolChoiceEnum.ANY; + if ("AUTO".equalsIgnoreCase(enumValue)) return ToolChoiceEnum.AUTO; + if ("NONE".equalsIgnoreCase(enumValue)) return ToolChoiceEnum.NONE; + if ("REQUIRED".equalsIgnoreCase(enumValue)) return ToolChoiceEnum.REQUIRED; + + throw ctxt.weirdStringException(enumValue, ToolChoiceEnum.class, "Not a valid ToolChoiceEnum value"); + } + } else if (token == JsonToken.START_OBJECT) { + return mapper.readValue(jp, SpecificToolChoice.class); + } + + return (ToolChoiceOption) ctxt.handleUnexpectedToken(ToolChoiceOption.class, jp); + } +} diff --git a/src/main/java/nl/dannyj/mistral/services/HttpService.java b/src/main/java/nl/dannyj/mistral/services/HttpService.java index 48eaca7..bc1735f 100644 --- a/src/main/java/nl/dannyj/mistral/services/HttpService.java +++ b/src/main/java/nl/dannyj/mistral/services/HttpService.java @@ -67,7 +67,7 @@ public String get(@NonNull String urlPath) { * Makes a POST request to the specified URL path with the provided body. * * @param urlPath The URL path to make the POST request to - * @param body The JSON body of the POST request + * @param body The JSON body of the POST request * @return The response body as a string */ public String post(@NonNull String urlPath, @NonNull String body) { @@ -108,7 +108,7 @@ private String executeRequest(Request request) { try (Response response = httpClient.newCall(request).execute()) { if (!response.isSuccessful()) { - throw new MistralAPIException("Received unexpected response code " + response.code() + ": " + response); + throw new MistralAPIException("Received unexpected response code " + response.code() + ": " + (response.body() != null ? response.body().string() : response)); } try (ResponseBody responseBody = response.body()) { diff --git a/src/main/java/nl/dannyj/mistral/services/MistralService.java b/src/main/java/nl/dannyj/mistral/services/MistralService.java index 8962fb9..b295189 100644 --- a/src/main/java/nl/dannyj/mistral/services/MistralService.java +++ b/src/main/java/nl/dannyj/mistral/services/MistralService.java @@ -31,9 +31,9 @@ import nl.dannyj.mistral.models.Response; import nl.dannyj.mistral.models.completion.ChatCompletionRequest; import nl.dannyj.mistral.models.completion.ChatCompletionResponse; -import nl.dannyj.mistral.models.completion.Message; -import nl.dannyj.mistral.models.completion.MessageChunk; -import nl.dannyj.mistral.models.completion.MessageRole; +import nl.dannyj.mistral.models.completion.message.ChatMessage; +import nl.dannyj.mistral.models.completion.message.MessageChunk; +import nl.dannyj.mistral.models.completion.message.MessageRole; import nl.dannyj.mistral.models.embedding.EmbeddingRequest; import nl.dannyj.mistral.models.embedding.EmbeddingResponse; import nl.dannyj.mistral.models.model.ListModelsResponse; @@ -88,7 +88,7 @@ public ChatCompletionResponse createChatCompletion(@NonNull ChatCompletionReques throw new IllegalArgumentException("The stream parameter is not supported for this method. Use createChatCompletionStream instead."); } - Message firstMessage = request.getMessages().get(0); + ChatMessage firstMessage = request.getMessages().get(0); MessageRole role = firstMessage.getRole(); if (firstMessage.getRole() == null || (!role.equals(MessageRole.USER) && !role.equals(MessageRole.SYSTEM))) { @@ -179,7 +179,7 @@ public CompletableFuture listModelsAsync() { /** * This method is used to create an embedding using the Mistral AI API. - * The embeddings for the input strings. See the mistral documentation for more details on embeddings. + * The embeddings for the input strings. See the mistral documentation for more details on embeddings. * This is a blocking method. * * @param request The request to create an embedding. See {@link EmbeddingRequest}. @@ -194,7 +194,7 @@ public EmbeddingResponse createEmbedding(@NonNull EmbeddingRequest request) { /** * This method is used to create an embedding using the Mistral AI API. - * The embeddings for the input strings. See the mistral documentation for more details on embeddings. + * The embeddings for the input strings. See the mistral documentation for more details on embeddings. * This is a non-blocking/asynchronous method. * * @param request The request to create an embedding. See {@link EmbeddingRequest}.