Addressing more checkstyle violations

- Enable checkstyle on more modules and adressing violations
review
This commit is contained in:
Soby Chacko
2024-10-28 22:26:34 -04:00
committed by Mark Pollack
parent cdc1cecb57
commit e72ab6ba25
368 changed files with 3244 additions and 2817 deletions

View File

@@ -36,6 +36,10 @@
<developerConnection>git@github.com:spring-projects/spring-ai.git</developerConnection>
</scm>
<properties>
<disable.checks>false</disable.checks>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>

View File

@@ -16,7 +16,7 @@
package org.springframework.ai.reader.pdf;
import java.awt.*;
import java.awt.Rectangle;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@@ -112,7 +112,7 @@ public class PagePdfDocumentReader implements DocumentReader {
for (PDPage page : this.document.getDocumentCatalog().getPages()) {
lastPage = page;
if (counter % logFrequency == 0 && counter / logFrequency < 10) {
this.logger.info("Processing PDF page: {}", (counter + 1));
logger.info("Processing PDF page: {}", (counter + 1));
}
counter++;
@@ -154,7 +154,7 @@ public class PagePdfDocumentReader implements DocumentReader {
readDocuments.add(toDocument(lastPage, pageTextGroupList.stream().collect(Collectors.joining()),
startPageNumber, pageNumber));
}
this.logger.info("Processing {} pages", totalPages);
logger.info("Processing {} pages", totalPages);
return readDocuments;
}

View File

@@ -16,7 +16,7 @@
package org.springframework.ai.reader.pdf;
import java.awt.*;
import java.awt.Rectangle;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
@@ -133,7 +133,7 @@ public class ParagraphPdfDocumentReader implements DocumentReader {
List<Document> documents = new ArrayList<>(paragraphs.size());
if (!CollectionUtils.isEmpty(paragraphs)) {
this.logger.info("Start processing paragraphs from PDF");
logger.info("Start processing paragraphs from PDF");
Iterator<Paragraph> itr = paragraphs.iterator();
var current = itr.next();
@@ -152,7 +152,7 @@ public class ParagraphPdfDocumentReader implements DocumentReader {
}
}
}
this.logger.info("End processing paragraphs from PDF");
logger.info("End processing paragraphs from PDF");
return documents;
}

View File

@@ -45,9 +45,11 @@ public class PdfReaderRuntimeHints implements RuntimeHintsRegistrar {
"/org/apache/pdfbox/resources/icc/**", "/org/apache/pdfbox/resources/text/**",
"/org/apache/pdfbox/resources/ttf/**", "/org/apache/pdfbox/resources/version.properties");
for (var pattern : patterns)
for (var resourceMatch : resolver.getResources(pattern))
for (var pattern : patterns) {
for (var resourceMatch : resolver.getResources(pattern)) {
hints.resources().registerResource(resourceMatch);
}
}
}
catch (IOException e) {

View File

@@ -27,7 +27,7 @@ import org.springframework.util.Assert;
*
* @author Christian Tzolov
*/
public class PdfDocumentReaderConfig {
public final class PdfDocumentReaderConfig {
public static final int ALL_PAGES = 0;
@@ -65,7 +65,7 @@ public class PdfDocumentReaderConfig {
return builder().build();
}
public static class Builder {
public static final class Builder {
private int pagesPerDocument = 1;

View File

@@ -0,0 +1,87 @@
/*
* Copyright 2023-2024 the original author or authors.
*
* 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
*
* https://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 org.springframework.ai.reader.pdf.layout;
class Character {
private char characterValue;
private int index;
private boolean isCharacterPartOfPreviousWord;
private boolean isFirstCharacterOfAWord;
private boolean isCharacterAtTheBeginningOfNewLine;
private boolean isCharacterCloseToPreviousWord;
Character(char characterValue, int index, boolean isCharacterPartOfPreviousWord, boolean isFirstCharacterOfAWord,
boolean isCharacterAtTheBeginningOfNewLine, boolean isCharacterPartOfASentence) {
this.characterValue = characterValue;
this.index = index;
this.isCharacterPartOfPreviousWord = isCharacterPartOfPreviousWord;
this.isFirstCharacterOfAWord = isFirstCharacterOfAWord;
this.isCharacterAtTheBeginningOfNewLine = isCharacterAtTheBeginningOfNewLine;
this.isCharacterCloseToPreviousWord = isCharacterPartOfASentence;
if (ForkPDFLayoutTextStripper.DEBUG) {
System.out.println(this.toString());
}
}
public char getCharacterValue() {
return this.characterValue;
}
public int getIndex() {
return this.index;
}
public void setIndex(int index) {
this.index = index;
}
public boolean isCharacterPartOfPreviousWord() {
return this.isCharacterPartOfPreviousWord;
}
public boolean isFirstCharacterOfAWord() {
return this.isFirstCharacterOfAWord;
}
public boolean isCharacterAtTheBeginningOfNewLine() {
return this.isCharacterAtTheBeginningOfNewLine;
}
public boolean isCharacterCloseToPreviousWord() {
return this.isCharacterCloseToPreviousWord;
}
public String toString() {
String toString = "";
toString += this.index;
toString += " ";
toString += this.characterValue;
toString += " isCharacterPartOfPreviousWord=" + this.isCharacterPartOfPreviousWord;
toString += " isFirstCharacterOfAWord=" + this.isFirstCharacterOfAWord;
toString += " isCharacterAtTheBeginningOfNewLine=" + this.isCharacterAtTheBeginningOfNewLine;
toString += " isCharacterPartOfASentence=" + this.isCharacterCloseToPreviousWord;
toString += " isCharacterCloseToPreviousWord=" + this.isCharacterCloseToPreviousWord;
return toString;
}
}

View File

@@ -0,0 +1,109 @@
/*
* Copyright 2023-2024 the original author or authors.
*
* 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
*
* https://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 org.springframework.ai.reader.pdf.layout;
import org.apache.pdfbox.text.TextPosition;
class CharacterFactory {
private TextPosition previousTextPosition;
private boolean firstCharacterOfLineFound;
private boolean isCharacterPartOfPreviousWord;
private boolean isFirstCharacterOfAWord;
private boolean isCharacterAtTheBeginningOfNewLine;
private boolean isCharacterCloseToPreviousWord;
CharacterFactory(boolean firstCharacterOfLineFound) {
this.firstCharacterOfLineFound = firstCharacterOfLineFound;
}
public Character createCharacterFromTextPosition(final TextPosition textPosition,
final TextPosition previousTextPosition) {
this.setPreviousTextPosition(previousTextPosition);
this.isCharacterPartOfPreviousWord = this.isCharacterPartOfPreviousWord(textPosition);
this.isFirstCharacterOfAWord = this.isFirstCharacterOfAWord(textPosition);
this.isCharacterAtTheBeginningOfNewLine = this.isCharacterAtTheBeginningOfNewLine(textPosition);
this.isCharacterCloseToPreviousWord = this.isCharacterCloseToPreviousWord(textPosition);
char character = this.getCharacterFromTextPosition(textPosition);
int index = (int) textPosition.getX() / ForkPDFLayoutTextStripper.OUTPUT_SPACE_CHARACTER_WIDTH_IN_PT;
return new Character(character, index, this.isCharacterPartOfPreviousWord, this.isFirstCharacterOfAWord,
this.isCharacterAtTheBeginningOfNewLine, this.isCharacterCloseToPreviousWord);
}
private boolean isCharacterAtTheBeginningOfNewLine(final TextPosition textPosition) {
if (!this.firstCharacterOfLineFound) {
return true;
}
TextPosition previousTextPosition = this.getPreviousTextPosition();
float previousTextYPosition = previousTextPosition.getY();
return (Math.round(textPosition.getY()) < Math.round(previousTextYPosition));
}
private boolean isFirstCharacterOfAWord(final TextPosition textPosition) {
if (!this.firstCharacterOfLineFound) {
return true;
}
double numberOfSpaces = this.numberOfSpacesBetweenTwoCharacters(this.previousTextPosition, textPosition);
return (numberOfSpaces > 1) || this.isCharacterAtTheBeginningOfNewLine(textPosition);
}
private boolean isCharacterCloseToPreviousWord(final TextPosition textPosition) {
if (!this.firstCharacterOfLineFound) {
return false;
}
double numberOfSpaces = this.numberOfSpacesBetweenTwoCharacters(this.previousTextPosition, textPosition);
return (numberOfSpaces > 1 && numberOfSpaces <= ForkPDFLayoutTextStripper.OUTPUT_SPACE_CHARACTER_WIDTH_IN_PT);
}
private boolean isCharacterPartOfPreviousWord(final TextPosition textPosition) {
TextPosition previousTextPosition = this.getPreviousTextPosition();
if (previousTextPosition.getUnicode().equals(" ")) {
return false;
}
double numberOfSpaces = this.numberOfSpacesBetweenTwoCharacters(previousTextPosition, textPosition);
return (numberOfSpaces <= 1);
}
private double numberOfSpacesBetweenTwoCharacters(final TextPosition textPosition1,
final TextPosition textPosition2) {
double previousTextXPosition = textPosition1.getX();
double previousTextWidth = textPosition1.getWidth();
double previousTextEndXPosition = (previousTextXPosition + previousTextWidth);
double numberOfSpaces = Math.abs(Math.round(textPosition2.getX() - previousTextEndXPosition));
return numberOfSpaces;
}
private char getCharacterFromTextPosition(final TextPosition textPosition) {
String string = textPosition.getUnicode();
char character = string.charAt(0);
return character;
}
private TextPosition getPreviousTextPosition() {
return this.previousTextPosition;
}
private void setPreviousTextPosition(final TextPosition previousTextPosition) {
this.previousTextPosition = previousTextPosition;
}
}

View File

@@ -13,17 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* 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 org.springframework.ai.reader.pdf.layout;
@@ -217,274 +206,3 @@ public class ForkPDFLayoutTextStripper extends PDFTextStripper {
}
}
class TextLine {
private static final char SPACE_CHARACTER = ' ';
private int lineLength;
private String line;
private int lastIndex;
public TextLine(int lineLength) {
this.line = "";
this.lineLength = lineLength / ForkPDFLayoutTextStripper.OUTPUT_SPACE_CHARACTER_WIDTH_IN_PT;
this.completeLineWithSpaces();
}
public void writeCharacterAtIndex(final Character character) {
character.setIndex(this.computeIndexForCharacter(character));
int index = character.getIndex();
char characterValue = character.getCharacterValue();
if (this.indexIsInBounds(index) && this.line.charAt(index) == SPACE_CHARACTER) {
this.line = this.line.substring(0, index) + characterValue
+ this.line.substring(index + 1, this.getLineLength());
}
}
public int getLineLength() {
return this.lineLength;
}
public String getLine() {
return this.line;
}
private int computeIndexForCharacter(final Character character) {
int index = character.getIndex();
boolean isCharacterPartOfPreviousWord = character.isCharacterPartOfPreviousWord();
boolean isCharacterAtTheBeginningOfNewLine = character.isCharacterAtTheBeginningOfNewLine();
boolean isCharacterCloseToPreviousWord = character.isCharacterCloseToPreviousWord();
if (!this.indexIsInBounds(index)) {
return -1;
}
else {
if (isCharacterPartOfPreviousWord && !isCharacterAtTheBeginningOfNewLine) {
index = this.findMinimumIndexWithSpaceCharacterFromIndex(index);
}
else if (isCharacterCloseToPreviousWord) {
if (this.line.charAt(index) != SPACE_CHARACTER) {
index = index + 1;
}
else {
index = this.findMinimumIndexWithSpaceCharacterFromIndex(index) + 1;
}
}
index = this.getNextValidIndex(index, isCharacterPartOfPreviousWord);
return index;
}
}
private boolean isSpaceCharacterAtIndex(int index) {
return this.line.charAt(index) != SPACE_CHARACTER;
}
private boolean isNewIndexGreaterThanLastIndex(int index) {
int lastIndex = this.getLastIndex();
return (index > lastIndex);
}
private int getNextValidIndex(int index, boolean isCharacterPartOfPreviousWord) {
int nextValidIndex = index;
int lastIndex = this.getLastIndex();
if (!this.isNewIndexGreaterThanLastIndex(index)) {
nextValidIndex = lastIndex + 1;
}
if (!isCharacterPartOfPreviousWord && this.isSpaceCharacterAtIndex(index - 1)) {
nextValidIndex = nextValidIndex + 1;
}
this.setLastIndex(nextValidIndex);
return nextValidIndex;
}
private int findMinimumIndexWithSpaceCharacterFromIndex(int index) {
int newIndex = index;
while (newIndex >= 0 && this.line.charAt(newIndex) == SPACE_CHARACTER) {
newIndex = newIndex - 1;
}
return newIndex + 1;
}
private boolean indexIsInBounds(int index) {
return (index >= 0 && index < this.lineLength);
}
private void completeLineWithSpaces() {
for (int i = 0; i < this.getLineLength(); ++i) {
this.line += SPACE_CHARACTER;
}
}
private int getLastIndex() {
return this.lastIndex;
}
private void setLastIndex(int lastIndex) {
this.lastIndex = lastIndex;
}
}
class Character {
private char characterValue;
private int index;
private boolean isCharacterPartOfPreviousWord;
private boolean isFirstCharacterOfAWord;
private boolean isCharacterAtTheBeginningOfNewLine;
private boolean isCharacterCloseToPreviousWord;
public Character(char characterValue, int index, boolean isCharacterPartOfPreviousWord,
boolean isFirstCharacterOfAWord, boolean isCharacterAtTheBeginningOfNewLine,
boolean isCharacterPartOfASentence) {
this.characterValue = characterValue;
this.index = index;
this.isCharacterPartOfPreviousWord = isCharacterPartOfPreviousWord;
this.isFirstCharacterOfAWord = isFirstCharacterOfAWord;
this.isCharacterAtTheBeginningOfNewLine = isCharacterAtTheBeginningOfNewLine;
this.isCharacterCloseToPreviousWord = isCharacterPartOfASentence;
if (ForkPDFLayoutTextStripper.DEBUG) {
System.out.println(this.toString());
}
}
public char getCharacterValue() {
return this.characterValue;
}
public int getIndex() {
return this.index;
}
public void setIndex(int index) {
this.index = index;
}
public boolean isCharacterPartOfPreviousWord() {
return this.isCharacterPartOfPreviousWord;
}
public boolean isFirstCharacterOfAWord() {
return this.isFirstCharacterOfAWord;
}
public boolean isCharacterAtTheBeginningOfNewLine() {
return this.isCharacterAtTheBeginningOfNewLine;
}
public boolean isCharacterCloseToPreviousWord() {
return this.isCharacterCloseToPreviousWord;
}
public String toString() {
String toString = "";
toString += this.index;
toString += " ";
toString += this.characterValue;
toString += " isCharacterPartOfPreviousWord=" + this.isCharacterPartOfPreviousWord;
toString += " isFirstCharacterOfAWord=" + this.isFirstCharacterOfAWord;
toString += " isCharacterAtTheBeginningOfNewLine=" + this.isCharacterAtTheBeginningOfNewLine;
toString += " isCharacterPartOfASentence=" + this.isCharacterCloseToPreviousWord;
toString += " isCharacterCloseToPreviousWord=" + this.isCharacterCloseToPreviousWord;
return toString;
}
}
class CharacterFactory {
private TextPosition previousTextPosition;
private boolean firstCharacterOfLineFound;
private boolean isCharacterPartOfPreviousWord;
private boolean isFirstCharacterOfAWord;
private boolean isCharacterAtTheBeginningOfNewLine;
private boolean isCharacterCloseToPreviousWord;
public CharacterFactory(boolean firstCharacterOfLineFound) {
this.firstCharacterOfLineFound = firstCharacterOfLineFound;
}
public Character createCharacterFromTextPosition(final TextPosition textPosition,
final TextPosition previousTextPosition) {
this.setPreviousTextPosition(previousTextPosition);
this.isCharacterPartOfPreviousWord = this.isCharacterPartOfPreviousWord(textPosition);
this.isFirstCharacterOfAWord = this.isFirstCharacterOfAWord(textPosition);
this.isCharacterAtTheBeginningOfNewLine = this.isCharacterAtTheBeginningOfNewLine(textPosition);
this.isCharacterCloseToPreviousWord = this.isCharacterCloseToPreviousWord(textPosition);
char character = this.getCharacterFromTextPosition(textPosition);
int index = (int) textPosition.getX() / ForkPDFLayoutTextStripper.OUTPUT_SPACE_CHARACTER_WIDTH_IN_PT;
return new Character(character, index, this.isCharacterPartOfPreviousWord, this.isFirstCharacterOfAWord,
this.isCharacterAtTheBeginningOfNewLine, this.isCharacterCloseToPreviousWord);
}
private boolean isCharacterAtTheBeginningOfNewLine(final TextPosition textPosition) {
if (!this.firstCharacterOfLineFound) {
return true;
}
TextPosition previousTextPosition = this.getPreviousTextPosition();
float previousTextYPosition = previousTextPosition.getY();
return (Math.round(textPosition.getY()) < Math.round(previousTextYPosition));
}
private boolean isFirstCharacterOfAWord(final TextPosition textPosition) {
if (!this.firstCharacterOfLineFound) {
return true;
}
double numberOfSpaces = this.numberOfSpacesBetweenTwoCharacters(this.previousTextPosition, textPosition);
return (numberOfSpaces > 1) || this.isCharacterAtTheBeginningOfNewLine(textPosition);
}
private boolean isCharacterCloseToPreviousWord(final TextPosition textPosition) {
if (!this.firstCharacterOfLineFound) {
return false;
}
double numberOfSpaces = this.numberOfSpacesBetweenTwoCharacters(this.previousTextPosition, textPosition);
return (numberOfSpaces > 1 && numberOfSpaces <= ForkPDFLayoutTextStripper.OUTPUT_SPACE_CHARACTER_WIDTH_IN_PT);
}
private boolean isCharacterPartOfPreviousWord(final TextPosition textPosition) {
TextPosition previousTextPosition = this.getPreviousTextPosition();
if (previousTextPosition.getUnicode().equals(" ")) {
return false;
}
double numberOfSpaces = this.numberOfSpacesBetweenTwoCharacters(previousTextPosition, textPosition);
return (numberOfSpaces <= 1);
}
private double numberOfSpacesBetweenTwoCharacters(final TextPosition textPosition1,
final TextPosition textPosition2) {
double previousTextXPosition = textPosition1.getX();
double previousTextWidth = textPosition1.getWidth();
double previousTextEndXPosition = (previousTextXPosition + previousTextWidth);
double numberOfSpaces = Math.abs(Math.round(textPosition2.getX() - previousTextEndXPosition));
return numberOfSpaces;
}
private char getCharacterFromTextPosition(final TextPosition textPosition) {
String string = textPosition.getUnicode();
char character = string.charAt(0);
return character;
}
private TextPosition getPreviousTextPosition() {
return this.previousTextPosition;
}
private void setPreviousTextPosition(final TextPosition previousTextPosition) {
this.previousTextPosition = previousTextPosition;
}
}

View File

@@ -0,0 +1,127 @@
/*
* Copyright 2023-2024 the original author or authors.
*
* 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
*
* https://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 org.springframework.ai.reader.pdf.layout;
class TextLine {
private static final char SPACE_CHARACTER = ' ';
private int lineLength;
private String line;
private int lastIndex;
TextLine(int lineLength) {
this.line = "";
this.lineLength = lineLength / ForkPDFLayoutTextStripper.OUTPUT_SPACE_CHARACTER_WIDTH_IN_PT;
this.completeLineWithSpaces();
}
public void writeCharacterAtIndex(final Character character) {
character.setIndex(this.computeIndexForCharacter(character));
int index = character.getIndex();
char characterValue = character.getCharacterValue();
if (this.indexIsInBounds(index) && this.line.charAt(index) == SPACE_CHARACTER) {
this.line = this.line.substring(0, index) + characterValue
+ this.line.substring(index + 1, this.getLineLength());
}
}
public int getLineLength() {
return this.lineLength;
}
public String getLine() {
return this.line;
}
private int computeIndexForCharacter(final Character character) {
int index = character.getIndex();
boolean isCharacterPartOfPreviousWord = character.isCharacterPartOfPreviousWord();
boolean isCharacterAtTheBeginningOfNewLine = character.isCharacterAtTheBeginningOfNewLine();
boolean isCharacterCloseToPreviousWord = character.isCharacterCloseToPreviousWord();
if (!this.indexIsInBounds(index)) {
return -1;
}
else {
if (isCharacterPartOfPreviousWord && !isCharacterAtTheBeginningOfNewLine) {
index = this.findMinimumIndexWithSpaceCharacterFromIndex(index);
}
else if (isCharacterCloseToPreviousWord) {
if (this.line.charAt(index) != SPACE_CHARACTER) {
index = index + 1;
}
else {
index = this.findMinimumIndexWithSpaceCharacterFromIndex(index) + 1;
}
}
index = this.getNextValidIndex(index, isCharacterPartOfPreviousWord);
return index;
}
}
private boolean isSpaceCharacterAtIndex(int index) {
return this.line.charAt(index) != SPACE_CHARACTER;
}
private boolean isNewIndexGreaterThanLastIndex(int index) {
int lastIndex = this.getLastIndex();
return (index > lastIndex);
}
private int getNextValidIndex(int index, boolean isCharacterPartOfPreviousWord) {
int nextValidIndex = index;
int lastIndex = this.getLastIndex();
if (!this.isNewIndexGreaterThanLastIndex(index)) {
nextValidIndex = lastIndex + 1;
}
if (!isCharacterPartOfPreviousWord && this.isSpaceCharacterAtIndex(index - 1)) {
nextValidIndex = nextValidIndex + 1;
}
this.setLastIndex(nextValidIndex);
return nextValidIndex;
}
private int findMinimumIndexWithSpaceCharacterFromIndex(int index) {
int newIndex = index;
while (newIndex >= 0 && this.line.charAt(newIndex) == SPACE_CHARACTER) {
newIndex = newIndex - 1;
}
return newIndex + 1;
}
private boolean indexIsInBounds(int index) {
return (index >= 0 && index < this.lineLength);
}
private void completeLineWithSpaces() {
for (int i = 0; i < this.getLineLength(); ++i) {
this.line += SPACE_CHARACTER;
}
}
private int getLastIndex() {
return this.lastIndex;
}
private void setLastIndex(int lastIndex) {
this.lastIndex = lastIndex;
}
}

View File

@@ -31,20 +31,20 @@ public class ParagraphPdfDocumentReaderTests {
@Test
public void testPdfWithoutToc() {
assertThatThrownBy(() -> {
assertThatThrownBy(() ->
new ParagraphPdfDocumentReader("classpath:/sample1.pdf",
PdfDocumentReaderConfig.builder()
.withPageTopMargin(0)
.withPageBottomMargin(0)
.withPageExtractedTextFormatter(ExtractedTextFormatter.builder()
.withNumberOfTopTextLinesToDelete(0)
.withNumberOfBottomTextLinesToDelete(3)
.withNumberOfTopPagesToSkipBeforeDelete(0)
.build())
.withPagesPerDocument(1)
.build());
}).isInstanceOf(IllegalArgumentException.class)
new ParagraphPdfDocumentReader("classpath:/sample1.pdf",
PdfDocumentReaderConfig.builder()
.withPageTopMargin(0)
.withPageBottomMargin(0)
.withPageExtractedTextFormatter(ExtractedTextFormatter.builder()
.withNumberOfTopTextLinesToDelete(0)
.withNumberOfBottomTextLinesToDelete(3)
.withNumberOfTopPagesToSkipBeforeDelete(0)
.build())
.withPagesPerDocument(1)
.build()))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining(
"Document outline (e.g. TOC) is null. Make sure the PDF document has a table of contents (TOC). If not, consider the PagePdfDocumentReader or the TikaDocumentReader instead.");

View File

@@ -37,6 +37,10 @@
<developerConnection>git@github.com:spring-projects/spring-ai.git</developerConnection>
</scm>
<properties>
<disable.checks>false</disable.checks>
</properties>
<dependencies>
<!-- production dependencies -->
@@ -106,4 +110,4 @@
</dependency>
</dependencies>
</project>
</project>

View File

@@ -272,9 +272,7 @@ public class AnthropicChatModel extends AbstractToolCallSupport implements ChatM
return Mono.just(chatResponse);
})
.doOnError(observation::error)
.doFinally(s -> {
observation.stop();
})
.doFinally(s -> observation.stop())
.contextWrite(ctx -> ctx.put(ObservationThreadLocalAccessor.KEY, observation));
// @formatter:on
@@ -292,10 +290,8 @@ public class AnthropicChatModel extends AbstractToolCallSupport implements ChatM
List<Generation> generations = chatCompletion.content()
.stream()
.filter(content -> content.type() != ContentBlock.Type.TOOL_USE)
.map(content -> {
return new Generation(new AssistantMessage(content.text(), Map.of()),
ChatGenerationMetadata.from(chatCompletion.stopReason(), null));
})
.map(content -> new Generation(new AssistantMessage(content.text(), Map.of()),
ChatGenerationMetadata.from(chatCompletion.stopReason(), null)))
.toList();
List<Generation> allGenerations = new ArrayList<>(generations);

View File

@@ -37,8 +37,9 @@ public class AnthropicRuntimeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(@NonNull RuntimeHints hints, @Nullable ClassLoader classLoader) {
var mcs = MemberCategory.values();
for (var tr : findJsonAnnotatedClassesInPackage(AnthropicApi.class))
for (var tr : findJsonAnnotatedClassesInPackage(AnthropicApi.class)) {
hints.reflection().registerType(tr, mcs);
}
}
}

View File

@@ -256,8 +256,10 @@ public class AnthropicApi {
public enum Role {
// @formatter:off
@JsonProperty("user") USER,
@JsonProperty("assistant") ASSISTANT
@JsonProperty("user")
USER,
@JsonProperty("assistant")
ASSISTANT
// @formatter:on
}
@@ -318,7 +320,7 @@ public class AnthropicApi {
/**
* Artifically created event to aggregate tool use events.
*/
TOOL_USE_AGGREATE;
TOOL_USE_AGGREATE
}
@@ -383,7 +385,8 @@ public class AnthropicApi {
* optionally return results back to the model using tool_result content blocks.
*/
@JsonInclude(Include.NON_NULL)
public record ChatCompletionRequest( // @formatter:off
public record ChatCompletionRequest(
// @formatter:off
@JsonProperty("model") String model,
@JsonProperty("messages") List<AnthropicMessage> messages,
@JsonProperty("system") String system,
@@ -428,7 +431,7 @@ public class AnthropicApi {
}
public static class ChatCompletionRequestBuilder {
public static final class ChatCompletionRequestBuilder {
private String model;
@@ -559,9 +562,10 @@ public class AnthropicApi {
* types.
*/
@JsonInclude(Include.NON_NULL)
public record AnthropicMessage( // @formatter:off
@JsonProperty("content") List<ContentBlock> content,
@JsonProperty("role") Role role) {
public record AnthropicMessage(
// @formatter:off
@JsonProperty("content") List<ContentBlock> content,
@JsonProperty("role") Role role) {
// @formatter:on
}
@@ -574,7 +578,8 @@ public class AnthropicApi {
* responses.
*/
@JsonInclude(Include.NON_NULL)
public record ContentBlock( // @formatter:off
public record ContentBlock(
// @formatter:off
@JsonProperty("type") Type type,
@JsonProperty("source") Source source,
@JsonProperty("text") String text,
@@ -682,7 +687,8 @@ public class AnthropicApi {
* @param data The base64-encoded data of the content.
*/
@JsonInclude(Include.NON_NULL)
public record Source( // @formatter:off
public record Source(
// @formatter:off
@JsonProperty("type") String type,
@JsonProperty("media_type") String mediaType,
@JsonProperty("data") String data) {
@@ -701,7 +707,8 @@ public class AnthropicApi {
///////////////////////////////////////
@JsonInclude(Include.NON_NULL)
public record Tool(// @formatter:off
public record Tool(
// @formatter:off
@JsonProperty("name") String name,
@JsonProperty("description") String description,
@JsonProperty("input_schema") Map<String, Object> inputSchema) {
@@ -724,7 +731,8 @@ public class AnthropicApi {
* @param usage Input and output token usage.
*/
@JsonInclude(Include.NON_NULL)
public record ChatCompletionResponse( // @formatter:off
public record ChatCompletionResponse(
// @formatter:off
@JsonProperty("id") String id,
@JsonProperty("type") String type,
@JsonProperty("role") Role role,
@@ -745,9 +753,10 @@ public class AnthropicApi {
* @param outputTokens The number of output tokens which were used. completion).
*/
@JsonInclude(Include.NON_NULL)
public record Usage( // @formatter:off
@JsonProperty("input_tokens") Integer inputTokens,
@JsonProperty("output_tokens") Integer outputTokens) {
public record Usage(
// @formatter:off
@JsonProperty("input_tokens") Integer inputTokens,
@JsonProperty("output_tokens") Integer outputTokens) {
// @formatter:off
}
@@ -828,10 +837,11 @@ public class AnthropicApi {
// MESSAGE START EVENT
@JsonInclude(Include.NON_NULL)
public record ContentBlockStartEvent(// @formatter:off
@JsonProperty("type") EventType type,
@JsonProperty("index") Integer index,
@JsonProperty("content_block") ContentBlockBody contentBlock) implements StreamEvent {
public record ContentBlockStartEvent(
// @formatter:off
@JsonProperty("type") EventType type,
@JsonProperty("index") Integer index,
@JsonProperty("content_block") ContentBlockBody contentBlock) implements StreamEvent {
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type",
visible = true)
@@ -854,17 +864,18 @@ public class AnthropicApi {
@JsonProperty("type") String type,
@JsonProperty("text") String text) implements ContentBlockBody {
}
}// @formatter:on
}
// @formatter:on
// MESSAGE DELTA EVENT
@JsonInclude(Include.NON_NULL)
public record ContentBlockDeltaEvent(// @formatter:off
@JsonProperty("type") EventType type,
@JsonProperty("index") Integer index,
public record ContentBlockDeltaEvent(
// @formatter:off
@JsonProperty("type") EventType type,
@JsonProperty("index") Integer index,
@JsonProperty("delta") ContentBlockDeltaBody delta) implements StreamEvent {
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type",
visible = true)
@JsonSubTypes({ @JsonSubTypes.Type(value = ContentBlockDeltaText.class, name = "text_delta"),
@@ -884,66 +895,78 @@ public class AnthropicApi {
@JsonProperty("type") String type,
@JsonProperty("partial_json") String partialJson) implements ContentBlockDeltaBody {
}
}// @formatter:on
}
// @formatter:on
// MESSAGE STOP EVENT
@JsonInclude(Include.NON_NULL)
public record ContentBlockStopEvent(// @formatter:off
@JsonProperty("type") EventType type,
@JsonProperty("index") Integer index) implements StreamEvent {
}// @formatter:on
public record ContentBlockStopEvent(
// @formatter:off
@JsonProperty("type") EventType type,
@JsonProperty("index") Integer index) implements StreamEvent {
}
// @formatter:on
@JsonInclude(Include.NON_NULL)
public record MessageStartEvent(// @formatter:off
@JsonProperty("type") EventType type,
@JsonProperty("message") ChatCompletionResponse message) implements StreamEvent {
}// @formatter:on
@JsonProperty("type") EventType type,
@JsonProperty("message") ChatCompletionResponse message) implements StreamEvent {
}
// @formatter:on
@JsonInclude(Include.NON_NULL)
public record MessageDeltaEvent(// @formatter:off
@JsonProperty("type") EventType type,
@JsonProperty("delta") MessageDelta delta,
@JsonProperty("usage") MessageDeltaUsage usage) implements StreamEvent {
public record MessageDeltaEvent(
// @formatter:off
@JsonProperty("type") EventType type,
@JsonProperty("delta") MessageDelta delta,
@JsonProperty("usage") MessageDeltaUsage usage) implements StreamEvent {
@JsonInclude(Include.NON_NULL)
public record MessageDelta(
@JsonProperty("stop_reason") String stopReason,
@JsonProperty("stop_sequence") String stopSequence) {
@JsonInclude(Include.NON_NULL)
public record MessageDelta(
@JsonProperty("stop_reason") String stopReason,
@JsonProperty("stop_sequence") String stopSequence) {
}
@JsonInclude(Include.NON_NULL)
public record MessageDeltaUsage(
@JsonProperty("output_tokens") Integer outputTokens) {
}
}// @formatter:on
@JsonProperty("output_tokens") Integer outputTokens) {
}
}
// @formatter:on
@JsonInclude(Include.NON_NULL)
public record MessageStopEvent(// @formatter:off
@JsonProperty("type") EventType type) implements StreamEvent {
}// @formatter:on
public record MessageStopEvent(
// @formatter:off
@JsonProperty("type") EventType type) implements StreamEvent {
}
// @formatter:on
///////////////////////////////////////
/// ERROR EVENT
///////////////////////////////////////
@JsonInclude(Include.NON_NULL)
public record ErrorEvent(// @formatter:off
@JsonProperty("type") EventType type,
@JsonProperty("error") Error error) implements StreamEvent {
public record ErrorEvent(
// @formatter:off
@JsonProperty("type") EventType type,
@JsonProperty("error") Error error) implements StreamEvent {
@JsonInclude(Include.NON_NULL)
public record Error(
@JsonProperty("type") String type,
@JsonProperty("message") String message) {
}
}// @formatter:on
@JsonInclude(Include.NON_NULL)
public record Error(
@JsonProperty("type") String type,
@JsonProperty("message") String message) {
}
}
// @formatter:on
///////////////////////////////////////
/// PING EVENT
///////////////////////////////////////
@JsonInclude(Include.NON_NULL)
public record PingEvent(// @formatter:off
@JsonProperty("type") EventType type) implements StreamEvent {
}// @formatter:on
public record PingEvent(
// @formatter:off
@JsonProperty("type") EventType type) implements StreamEvent {
}
// @formatter:on
}

View File

@@ -178,6 +178,7 @@ public class StreamHelper {
}
}
else if (event.type().equals(EventType.MESSAGE_STOP)) {
// pass through
}
else {
contentBlockReference.get().withType(event.type().name()).withContent(List.of());

View File

@@ -65,7 +65,7 @@ public class MockWeatherService implements Function<MockWeatherService.Request,
*/
public final String unitName;
private Unit(String text) {
Unit(String text) {
this.unitName = text;
}

View File

@@ -35,7 +35,7 @@ import org.springframework.util.StringUtils;
/**
* @author Christian Tzolov
*/
public class XmlHelper {
public final class XmlHelper {
// Regular expression to match XML block between <function_calls> and
// </function_calls> tags
@@ -46,6 +46,10 @@ public class XmlHelper {
private static final XmlMapper xmlMapper = new XmlMapper();
private XmlHelper() {
}
public static String extractFunctionCallsXmlBlock(String text) {
if (!StringUtils.hasText(text)) {
return "";
@@ -128,7 +132,8 @@ public class XmlHelper {
@JsonProperty("description") String description) {
}
}
} // @formatter:on
}
// @formatter:on
@JsonInclude(Include.NON_NULL) // @formatter:off
@JacksonXmlRootElement(localName = "function_calls")

View File

@@ -93,7 +93,7 @@ class AnthropicChatClientIT {
.user(u -> u.text("List five {subject}")
.param("subject", "ice cream flavors"))
.call()
.entity(new ParameterizedTypeReference<List<String>>() {});
.entity(new ParameterizedTypeReference<>() { });
// @formatter:on
logger.info(collection.toString());

View File

@@ -35,6 +35,10 @@
<developerConnection>git@github.com:spring-projects/spring-ai.git</developerConnection>
</scm>
<properties>
<disable.checks>false</disable.checks>
</properties>
<dependencies>
<!-- production dependencies -->

View File

@@ -45,7 +45,7 @@ public class AzureOpenAiAudioTranscriptionOptions implements AudioTranscriptionO
* The deployment name as defined in Azure Open AI Studio when creating a deployment
* backed by an Azure OpenAI base model.
*/
private @JsonProperty(value = "deployment_name") String deploymentName;
private @JsonProperty("deployment_name") String deploymentName;
/**
* The format of the transcript output, in one of these options: json, text, srt, verbose_json, or vtt.
@@ -138,41 +138,53 @@ public class AzureOpenAiAudioTranscriptionOptions implements AudioTranscriptionO
@Override
public boolean equals(Object obj) {
if (this == obj)
if (this == obj) {
return true;
if (obj == null)
}
if (obj == null) {
return false;
if (getClass() != obj.getClass())
}
if (getClass() != obj.getClass()) {
return false;
}
AzureOpenAiAudioTranscriptionOptions other = (AzureOpenAiAudioTranscriptionOptions) obj;
if (this.model == null) {
if (other.model != null)
if (other.model != null) {
return false;
}
}
else if (!this.model.equals(other.model))
else if (!this.model.equals(other.model)) {
return false;
}
if (this.prompt == null) {
if (other.prompt != null)
if (other.prompt != null) {
return false;
}
}
else if (!this.prompt.equals(other.prompt))
else if (!this.prompt.equals(other.prompt)) {
return false;
}
if (this.language == null) {
if (other.language != null)
if (other.language != null) {
return false;
}
}
else if (!this.language.equals(other.language))
else if (!this.language.equals(other.language)) {
return false;
if (this.responseFormat == null) {
return other.responseFormat==null;
}
else return this.responseFormat.equals(other.responseFormat);
if (this.responseFormat == null) {
return other.responseFormat == null;
}
else {
return this.responseFormat.equals(other.responseFormat);
}
}
public enum WhisperModel {
// @formatter:off
@JsonProperty("whisper") WHISPER("whisper");
@JsonProperty("whisper")
WHISPER("whisper");
// @formatter:on
public final String value;
@@ -190,11 +202,16 @@ public class AzureOpenAiAudioTranscriptionOptions implements AudioTranscriptionO
public enum TranscriptResponseFormat {
// @formatter:off
@JsonProperty("json") JSON(AudioTranscriptionFormat.JSON, StructuredResponse.class),
@JsonProperty("text") TEXT(AudioTranscriptionFormat.TEXT, String.class),
@JsonProperty("srt") SRT(AudioTranscriptionFormat.SRT, String.class),
@JsonProperty("verbose_json") VERBOSE_JSON(AudioTranscriptionFormat.VERBOSE_JSON, StructuredResponse.class),
@JsonProperty("vtt") VTT(AudioTranscriptionFormat.VTT, String.class);
@JsonProperty("json")
JSON(AudioTranscriptionFormat.JSON, StructuredResponse.class),
@JsonProperty("text")
TEXT(AudioTranscriptionFormat.TEXT, String.class),
@JsonProperty("srt")
SRT(AudioTranscriptionFormat.SRT, String.class),
@JsonProperty("verbose_json")
VERBOSE_JSON(AudioTranscriptionFormat.VERBOSE_JSON, StructuredResponse.class),
@JsonProperty("vtt")
VTT(AudioTranscriptionFormat.VTT, String.class);
public final AudioTranscriptionFormat value;
@@ -217,8 +234,10 @@ public class AzureOpenAiAudioTranscriptionOptions implements AudioTranscriptionO
public enum GranularityType {
// @formatter:off
@JsonProperty("word") WORD(AudioTranscriptionTimestampGranularity.WORD),
@JsonProperty("segment") SEGMENT(AudioTranscriptionTimestampGranularity.SEGMENT);
@JsonProperty("word")
WORD(AudioTranscriptionTimestampGranularity.WORD),
@JsonProperty("segment")
SEGMENT(AudioTranscriptionTimestampGranularity.SEGMENT);
// @formatter:on
public final AudioTranscriptionTimestampGranularity value;

View File

@@ -290,9 +290,10 @@ public class AzureOpenAiChatModel extends AbstractToolCallSupport implements Cha
return this.stream(new Prompt(toolCallConversation, prompt.getOptions()));
}
Flux<ChatResponse> flux = Flux.just(chatResponse).doOnError(observation::error).doFinally(s -> {
observation.stop();
}).contextWrite(ctx -> ctx.put(ObservationThreadLocalAccessor.KEY, observation));
Flux<ChatResponse> flux = Flux.just(chatResponse)
.doOnError(observation::error)
.doFinally(s -> observation.stop())
.contextWrite(ctx -> ctx.put(ObservationThreadLocalAccessor.KEY, observation));
return new MessageAggregator().aggregate(flux, observationContext::setResponse);
});
@@ -416,7 +417,7 @@ public class AzureOpenAiChatModel extends AbstractToolCallSupport implements Cha
return List.of(new ChatRequestUserMessage(items));
case SYSTEM:
return List.of(new ChatRequestSystemMessage(message.getContent()));
case ASSISTANT: {
case ASSISTANT:
AssistantMessage assistantMessage = (AssistantMessage) message;
List<ChatCompletionsToolCall> toolCalls = null;
if (!CollectionUtils.isEmpty(assistantMessage.getToolCalls())) {
@@ -430,20 +431,17 @@ public class AzureOpenAiChatModel extends AbstractToolCallSupport implements Cha
var azureAssistantMessage = new ChatRequestAssistantMessage(message.getContent());
azureAssistantMessage.setToolCalls(toolCalls);
return List.of(azureAssistantMessage);
}
case TOOL: {
case TOOL:
ToolResponseMessage toolMessage = (ToolResponseMessage) message;
toolMessage.getResponses().forEach(response -> {
Assert.isTrue(response.id() != null, "ToolResponseMessage must have an id");
});
toolMessage.getResponses()
.forEach(response -> Assert.isTrue(response.id() != null, "ToolResponseMessage must have an id"));
return toolMessage.getResponses()
.stream()
.map(tr -> new ChatRequestToolMessage(tr.responseData(), tr.id()))
.map(crtm -> ((ChatRequestMessage) crtm))
.toList();
}
default:
throw new IllegalArgumentException("Unknown message type " + message.getMessageType());
}

View File

@@ -22,17 +22,17 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.function.FunctionCallingOptions;
import org.springframework.util.Assert;
import com.azure.ai.openai.models.AzureChatEnhancementConfiguration;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.function.FunctionCallingOptions;
import org.springframework.util.Assert;
/**
* The configuration information for a chat completions request. Completions support a
* wide variety of tasks and generate text that continues from or "completes" provided
@@ -48,7 +48,7 @@ public class AzureOpenAiChatOptions implements FunctionCallingOptions, ChatOptio
/**
* The maximum number of tokens to generate.
*/
@JsonProperty(value = "max_tokens")
@JsonProperty("max_tokens")
private Integer maxTokens;
/**
@@ -58,7 +58,7 @@ public class AzureOpenAiChatOptions implements FunctionCallingOptions, ChatOptio
* temperature and top_p for the same completions request as the interaction of these
* two settings is difficult to predict.
*/
@JsonProperty(value = "temperature")
@JsonProperty("temperature")
private Double temperature;
/**
@@ -69,7 +69,7 @@ public class AzureOpenAiChatOptions implements FunctionCallingOptions, ChatOptio
* temperature and top_p for the same completions request as the interaction of these
* two settings is difficult to predict.
*/
@JsonProperty(value = "top_p")
@JsonProperty("top_p")
private Double topP;
/**
@@ -79,14 +79,14 @@ public class AzureOpenAiChatOptions implements FunctionCallingOptions, ChatOptio
* minimum and maximum values corresponding to a full ban or exclusive selection of a
* token, respectively. The exact behavior of a given bias score varies by model.
*/
@JsonProperty(value = "logit_bias")
@JsonProperty("logit_bias")
private Map<String, Integer> logitBias;
/**
* An identifier for the caller or end user of the operation. This may be used for
* tracking or rate-limiting purposes.
*/
@JsonProperty(value = "user")
@JsonProperty("user")
private String user;
/**
@@ -95,13 +95,13 @@ public class AzureOpenAiChatOptions implements FunctionCallingOptions, ChatOptio
* quickly consume your token quota. Use carefully and ensure reasonable settings for
* max_tokens and stop.
*/
@JsonProperty(value = "n")
@JsonProperty("n")
private Integer n;
/**
* A collection of textual sequences that will end completions generation.
*/
@JsonProperty(value = "stop")
@JsonProperty("stop")
private List<String> stop;
/**
@@ -110,7 +110,7 @@ public class AzureOpenAiChatOptions implements FunctionCallingOptions, ChatOptio
* likely to appear when they already exist and increase the model's likelihood to
* output new topics.
*/
@JsonProperty(value = "presence_penalty")
@JsonProperty("presence_penalty")
private Double presencePenalty;
/**
@@ -119,14 +119,14 @@ public class AzureOpenAiChatOptions implements FunctionCallingOptions, ChatOptio
* likely to appear as their frequency increases and decrease the likelihood of the
* model repeating the same statements verbatim.
*/
@JsonProperty(value = "frequency_penalty")
@JsonProperty("frequency_penalty")
private Double frequencyPenalty;
/**
* The deployment name as defined in Azure Open AI Studio when creating a deployment
* backed by an Azure OpenAI base model.
*/
@JsonProperty(value = "deployment_name")
@JsonProperty("deployment_name")
private String deploymentName;
/**
@@ -168,7 +168,7 @@ public class AzureOpenAiChatOptions implements FunctionCallingOptions, ChatOptio
* Seed value for deterministic sampling such that the same seed and parameters return
* the same result.
*/
@JsonProperty(value = "seed")
@JsonProperty("seed")
private Long seed;
/**
@@ -176,7 +176,7 @@ public class AzureOpenAiChatOptions implements FunctionCallingOptions, ChatOptio
* the log probabilities of each output token returned in the `content` of `message`.
* This option is currently not available on the `gpt-4-vision-preview` model.
*/
@JsonProperty(value = "log_probs")
@JsonProperty("log_probs")
private Boolean logprobs;
/*
@@ -184,7 +184,7 @@ public class AzureOpenAiChatOptions implements FunctionCallingOptions, ChatOptio
* each token position, each with an associated log probability. `logprobs` must be
* set to `true` if this parameter is used.
*/
@JsonProperty(value = "top_log_probs")
@JsonProperty("top_log_probs")
private Integer topLogProbs;
/*

View File

@@ -44,8 +44,6 @@ import org.springframework.ai.model.ModelOptionsUtils;
import org.springframework.ai.util.JacksonUtils;
import org.springframework.util.Assert;
import static java.lang.String.format;
/**
* {@link ImageModel} implementation for {@literal Microsoft Azure AI} backed by
* {@link OpenAIClient}.
@@ -92,15 +90,15 @@ public class AzureOpenAiImageModel implements ImageModel {
public ImageResponse call(ImagePrompt imagePrompt) {
ImageGenerationOptions imageGenerationOptions = toOpenAiImageOptions(imagePrompt);
String deploymentOrModelName = getDeploymentName(imagePrompt);
if (this.logger.isTraceEnabled()) {
this.logger.trace("Azure ImageGenerationOptions call {} with the following options : {} ",
deploymentOrModelName, toPrettyJson(imageGenerationOptions));
if (logger.isTraceEnabled()) {
logger.trace("Azure ImageGenerationOptions call {} with the following options : {} ", deploymentOrModelName,
toPrettyJson(imageGenerationOptions));
}
var images = this.openAIClient.getImageGenerations(deploymentOrModelName, imageGenerationOptions);
if (this.logger.isTraceEnabled()) {
this.logger.trace("Azure ImageGenerations: {}", toPrettyJson(images));
if (logger.isTraceEnabled()) {
logger.trace("Azure ImageGenerations: {}", toPrettyJson(images));
}
List<ImageGeneration> imageGenerations = images.getData().stream().map(entry -> {
@@ -154,8 +152,8 @@ public class AzureOpenAiImageModel implements ImageModel {
private ImageGenerationOptions toOpenAiImageOptions(ImagePrompt prompt) {
if (prompt.getInstructions().size() > 1) {
throw new RuntimeException(format("implementation support 1 image instruction only, found %s",
prompt.getInstructions().size()));
throw new RuntimeException(java.lang.String
.format("implementation support 1 image instruction only, found %s", prompt.getInstructions().size()));
}
if (prompt.getInstructions().isEmpty()) {
throw new RuntimeException("please provide image instruction, current is empty");

View File

@@ -45,14 +45,14 @@ public class AzureOpenAiImageOptions implements ImageOptions {
/**
* The model dall-e-3 or dall-e-2 By default dall-e-3
*/
@JsonProperty(value = "model")
@JsonProperty("model")
private String model = ImageModel.DALL_E_3.value;
/**
* The deployment name as defined in Azure Open AI Studio when creating a deployment
* backed by an Azure OpenAI base model.
*/
@JsonProperty(value = "deployment_name")
@JsonProperty("deployment_name")
private String deploymentName;
/**
@@ -255,7 +255,7 @@ public class AzureOpenAiImageOptions implements ImageOptions {
}
public static class Builder {
public final static class Builder {
private final AzureOpenAiImageOptions options;

View File

@@ -48,7 +48,7 @@ import org.springframework.util.CollectionUtils;
* @author Christian Tzolov
* @since 1.0.0
*/
public class MergeUtils {
public final class MergeUtils {
private static final Class<?>[] CHAT_COMPLETIONS_CONSTRUCTOR_ARG_TYPES = new Class<?>[] { String.class,
OffsetDateTime.class, List.class, CompletionsUsage.class };
@@ -59,6 +59,10 @@ public class MergeUtils {
private static final Class<?>[] chatResponseMessageConstructorArgumentTypes = new Class<?>[] { ChatRole.class,
String.class };
private MergeUtils() {
}
/**
* Create a new instance of the given class using the constructor at the given index.
* Can be used to create instances with private constructors.

View File

@@ -50,8 +50,9 @@ public class AzureOpenAiRuntimeHints implements RuntimeHintsRegistrar {
try {
var resolver = new PathMatchingResourcePatternResolver();
for (var resourceMatch : resolver.getResources("/azure-ai-openai.properties"))
for (var resourceMatch : resolver.getResources("/azure-ai-openai.properties")) {
hints.resources().registerResource(resourceMatch);
}
}
catch (Exception e) {
throw new RuntimeException(e);

View File

@@ -39,7 +39,6 @@ import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.Resource;
import static com.azure.core.http.policy.HttpLogDetailLevel.BODY_AND_HEADERS;
import static org.assertj.core.api.Assertions.assertThat;
/**
@@ -158,7 +157,8 @@ public class AzureOpenAiChatClientIT {
return new OpenAIClientBuilder().credential(new AzureKeyCredential(System.getenv("AZURE_OPENAI_API_KEY")))
.endpoint(System.getenv("AZURE_OPENAI_ENDPOINT"))
.serviceVersion(OpenAIServiceVersion.V2024_02_15_PREVIEW)
.httpLogOptions(new HttpLogOptions().setLogLevel(BODY_AND_HEADERS));
.httpLogOptions(new HttpLogOptions()
.setLogLevel(com.azure.core.http.policy.HttpLogDetailLevel.BODY_AND_HEADERS));
}
@Bean

View File

@@ -54,7 +54,6 @@ import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.util.MimeTypeUtils;
import static com.azure.core.http.policy.HttpLogDetailLevel.BODY_AND_HEADERS;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(classes = AzureOpenAiChatModelIT.TestConfiguration.class)
@@ -269,7 +268,8 @@ class AzureOpenAiChatModelIT {
return new OpenAIClientBuilder().credential(new AzureKeyCredential(System.getenv("AZURE_OPENAI_API_KEY")))
.endpoint(System.getenv("AZURE_OPENAI_ENDPOINT"))
.serviceVersion(OpenAIServiceVersion.V2024_02_15_PREVIEW)
.httpLogOptions(new HttpLogOptions().setLogLevel(BODY_AND_HEADERS));
.httpLogOptions(new HttpLogOptions()
.setLogLevel(com.azure.core.http.policy.HttpLogDetailLevel.BODY_AND_HEADERS));
}
@Bean

View File

@@ -42,7 +42,6 @@ import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import static com.azure.core.http.policy.HttpLogDetailLevel.BODY_AND_HEADERS;
import static org.assertj.core.api.Assertions.assertThat;
/**
@@ -190,7 +189,8 @@ class AzureOpenAiChatModelObservationIT {
return new OpenAIClientBuilder().credential(new AzureKeyCredential(System.getenv("AZURE_OPENAI_API_KEY")))
.endpoint(System.getenv("AZURE_OPENAI_ENDPOINT"))
.serviceVersion(OpenAIServiceVersion.V2024_02_15_PREVIEW)
.httpLogOptions(new HttpLogOptions().setLogLevel(BODY_AND_HEADERS));
.httpLogOptions(new HttpLogOptions()
.setLogLevel(com.azure.core.http.policy.HttpLogDetailLevel.BODY_AND_HEADERS));
}
@Bean

View File

@@ -223,7 +223,7 @@ public class MockAiTestConfiguration {
}
protected Logger getLogger() {
return this.logger;
return logger;
}
@Override

View File

@@ -73,7 +73,7 @@ class AzureOpenAiChatModelFunctionCallIT {
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("getCurrentWeather")
.withDescription("Get the current weather in a given location")
.withResponseConverter((response) -> "" + response.temp() + response.unit())
.withResponseConverter(response -> "" + response.temp() + response.unit())
.build()))
.build();
@@ -97,7 +97,7 @@ class AzureOpenAiChatModelFunctionCallIT {
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("getCurrentWeather")
.withDescription("Get the current weather in a given location")
.withResponseConverter((response) -> "" + response.temp() + response.unit())
.withResponseConverter(response -> "" + response.temp() + response.unit())
.build()))
.build();
@@ -119,7 +119,7 @@ class AzureOpenAiChatModelFunctionCallIT {
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("getCurrentWeather")
.withDescription("Get the current weather in a given location")
.withResponseConverter((response) -> "" + response.temp() + response.unit())
.withResponseConverter(response -> "" + response.temp() + response.unit())
.build()))
.build();
@@ -156,7 +156,7 @@ class AzureOpenAiChatModelFunctionCallIT {
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("getCurrentWeather")
.withDescription("Get the current weather in a given location")
.withResponseConverter((response) -> "" + response.temp() + response.unit())
.withResponseConverter(response -> "" + response.temp() + response.unit())
.build()))
.build();

View File

@@ -65,7 +65,7 @@ public class MockWeatherService implements Function<MockWeatherService.Request,
*/
public final String unitName;
private Unit(String text) {
Unit(String text) {
this.unitName = text;
}

View File

@@ -36,6 +36,10 @@
<developerConnection>git@github.com:spring-projects/spring-ai.git</developerConnection>
</scm>
<properties>
<disable.checks>false</disable.checks>
</properties>
<dependencies>
<!-- production dependencies -->

View File

@@ -28,7 +28,7 @@ import org.springframework.ai.chat.messages.MessageType;
* @author Christian Tzolov
* @since 1.0.0
*/
public class MessageToPromptConverter {
public final class MessageToPromptConverter {
private static final String HUMAN_PROMPT = "Human:";

View File

@@ -200,11 +200,11 @@ public class AnthropicChatBedrockApi extends
return new Builder(prompt);
}
public static class Builder {
public static final class Builder {
private final String prompt;
private Double temperature;// = 0.7;
private Integer maxTokensToSample;// = 500;
private Integer topK;// = 10;
private Double temperature; // = 0.7;
private Integer maxTokensToSample; // = 500;
private Integer topK; // = 10;
private Double topP;
private List<String> stopSequences;
private String anthropicVersion;
@@ -275,4 +275,4 @@ public class AnthropicChatBedrockApi extends
}
}
// @formatter:on
// @formatter:on

View File

@@ -85,10 +85,11 @@ public class BedrockAnthropic3ChatModel implements ChatModel, StreamingChatModel
AnthropicChatResponse response = this.anthropicChatApi.chatCompletion(request);
List<Generation> generations = response.content().stream().map(content -> {
return new Generation(new AssistantMessage(content.text()),
ChatGenerationMetadata.from(response.stopReason(), null));
}).toList();
List<Generation> generations = response.content()
.stream()
.map(content -> new Generation(new AssistantMessage(content.text()),
ChatGenerationMetadata.from(response.stopReason(), null)))
.toList();
ChatResponseMetadata metadata = ChatResponseMetadata.builder()
.withId(response.id())

View File

@@ -230,12 +230,12 @@ public class Anthropic3ChatBedrockApi extends
return new Builder(messages);
}
public static class Builder {
public static final class Builder {
private final List<ChatCompletionMessage> messages;
private String system;
private Double temperature;// = 0.7;
private Integer maxTokens;// = 500;
private Integer topK;// = 10;
private Double temperature; // = 0.7;
private Integer maxTokens; // = 500;
private Integer topK; // = 10;
private Double topP;
private List<String> stopSequences;
private String anthropicVersion;
@@ -301,7 +301,8 @@ public class Anthropic3ChatBedrockApi extends
* responses.
*/
@JsonInclude(Include.NON_NULL)
public record MediaContent( // @formatter:off
public record MediaContent(
// @formatter:off
@JsonProperty("type") Type type,
@JsonProperty("source") Source source,
@JsonProperty("text") String text,
@@ -349,7 +350,8 @@ public class Anthropic3ChatBedrockApi extends
* @param data The base64-encoded data of the content.
*/
@JsonInclude(Include.NON_NULL)
public record Source( // @formatter:off
public record Source(
// @formatter:off
@JsonProperty("type") String type,
@JsonProperty("media_type") String mediaType,
@JsonProperty("data") String data) {

View File

@@ -52,43 +52,59 @@ public class BedrockRuntimeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
var mcs = MemberCategory.values();
for (var tr : findJsonAnnotatedClassesInPackage(AbstractBedrockApi.class))
for (var tr : findJsonAnnotatedClassesInPackage(AbstractBedrockApi.class)) {
hints.reflection().registerType(tr, mcs);
for (var tr : findJsonAnnotatedClassesInPackage(Ai21Jurassic2ChatBedrockApi.class))
}
for (var tr : findJsonAnnotatedClassesInPackage(Ai21Jurassic2ChatBedrockApi.class)) {
hints.reflection().registerType(tr, mcs);
}
for (var tr : findJsonAnnotatedClassesInPackage(CohereChatBedrockApi.class))
for (var tr : findJsonAnnotatedClassesInPackage(CohereChatBedrockApi.class)) {
hints.reflection().registerType(tr, mcs);
for (var tr : findJsonAnnotatedClassesInPackage(BedrockCohereChatOptions.class))
}
for (var tr : findJsonAnnotatedClassesInPackage(BedrockCohereChatOptions.class)) {
hints.reflection().registerType(tr, mcs);
for (var tr : findJsonAnnotatedClassesInPackage(CohereEmbeddingBedrockApi.class))
}
for (var tr : findJsonAnnotatedClassesInPackage(CohereEmbeddingBedrockApi.class)) {
hints.reflection().registerType(tr, mcs);
for (var tr : findJsonAnnotatedClassesInPackage(BedrockCohereEmbeddingOptions.class))
}
for (var tr : findJsonAnnotatedClassesInPackage(BedrockCohereEmbeddingOptions.class)) {
hints.reflection().registerType(tr, mcs);
}
for (var tr : findJsonAnnotatedClassesInPackage(LlamaChatBedrockApi.class))
for (var tr : findJsonAnnotatedClassesInPackage(LlamaChatBedrockApi.class)) {
hints.reflection().registerType(tr, mcs);
for (var tr : findJsonAnnotatedClassesInPackage(BedrockLlamaChatOptions.class))
}
for (var tr : findJsonAnnotatedClassesInPackage(BedrockLlamaChatOptions.class)) {
hints.reflection().registerType(tr, mcs);
}
for (var tr : findJsonAnnotatedClassesInPackage(TitanChatBedrockApi.class))
for (var tr : findJsonAnnotatedClassesInPackage(TitanChatBedrockApi.class)) {
hints.reflection().registerType(tr, mcs);
for (var tr : findJsonAnnotatedClassesInPackage(BedrockTitanChatOptions.class))
}
for (var tr : findJsonAnnotatedClassesInPackage(BedrockTitanChatOptions.class)) {
hints.reflection().registerType(tr, mcs);
for (var tr : findJsonAnnotatedClassesInPackage(BedrockTitanEmbeddingOptions.class))
}
for (var tr : findJsonAnnotatedClassesInPackage(BedrockTitanEmbeddingOptions.class)) {
hints.reflection().registerType(tr, mcs);
for (var tr : findJsonAnnotatedClassesInPackage(TitanEmbeddingBedrockApi.class))
}
for (var tr : findJsonAnnotatedClassesInPackage(TitanEmbeddingBedrockApi.class)) {
hints.reflection().registerType(tr, mcs);
}
for (var tr : findJsonAnnotatedClassesInPackage(AnthropicChatBedrockApi.class))
for (var tr : findJsonAnnotatedClassesInPackage(AnthropicChatBedrockApi.class)) {
hints.reflection().registerType(tr, mcs);
for (var tr : findJsonAnnotatedClassesInPackage(AnthropicChatOptions.class))
}
for (var tr : findJsonAnnotatedClassesInPackage(AnthropicChatOptions.class)) {
hints.reflection().registerType(tr, mcs);
}
for (var tr : findJsonAnnotatedClassesInPackage(Anthropic3ChatBedrockApi.class))
for (var tr : findJsonAnnotatedClassesInPackage(Anthropic3ChatBedrockApi.class)) {
hints.reflection().registerType(tr, mcs);
for (var tr : findJsonAnnotatedClassesInPackage(Anthropic3ChatOptions.class))
}
for (var tr : findJsonAnnotatedClassesInPackage(Anthropic3ChatOptions.class)) {
hints.reflection().registerType(tr, mcs);
}
}
}

View File

@@ -13,9 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// @formatter:off
package org.springframework.ai.bedrock.api;
// @formatter:off
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
@@ -103,7 +105,7 @@ public abstract class AbstractBedrockApi<I, O, SO> {
* @param objectMapper The object mapper to use for JSON serialization and deserialization.
*/
public AbstractBedrockApi(String modelId, AwsCredentialsProvider credentialsProvider, String region,
ObjectMapper objectMapper) {
ObjectMapper objectMapper) {
this(modelId, credentialsProvider, region, objectMapper, Duration.ofMinutes(5));
}
@@ -273,7 +275,7 @@ public abstract class AbstractBedrockApi<I, O, SO> {
InvokeModelWithResponseStreamResponseHandler.Visitor visitor = InvokeModelWithResponseStreamResponseHandler.Visitor
.builder()
.onChunk((chunk) -> {
.onChunk(chunk -> {
try {
logger.debug("Received chunk: " + chunk.bytes().asString(StandardCharsets.UTF_8));
SO response = this.objectMapper.readValue(chunk.bytes().asByteArray(), clazz);
@@ -284,7 +286,7 @@ public abstract class AbstractBedrockApi<I, O, SO> {
eventSink.tryEmitError(e);
}
})
.onDefault((event) -> {
.onDefault(event -> {
logger.error("Unknown or unhandled event: " + event.toString());
eventSink.tryEmitError(new Throwable("Unknown or unhandled event: " + event.toString()));
})
@@ -295,24 +297,20 @@ public abstract class AbstractBedrockApi<I, O, SO> {
.onComplete(
() -> {
EmitResult emitResult = eventSink.tryEmitComplete();
while(!emitResult.isSuccess()){
while (!emitResult.isSuccess()) {
System.out.println("Emitting complete:" + emitResult);
emitResult = eventSink.tryEmitComplete();
};
}
eventSink.emitComplete(EmitFailureHandler.busyLooping(Duration.ofSeconds(3)));
// EmitResult emitResult = eventSink.tryEmitComplete();
logger.debug("\nCompleted streaming response.");
})
.onError((error) -> {
.onError(error -> {
logger.error("\n\nError streaming response: " + error.getMessage());
eventSink.tryEmitError(error);
})
.onEventStream((stream) -> {
stream.subscribe(
(ResponseStream e) -> {
e.accept(visitor);
});
})
.onEventStream(stream -> stream.subscribe(
(ResponseStream e) -> e.accept(visitor)))
.build();
this.clientStreaming.invokeModelWithResponseStream(invokeRequest, responseHandler);
@@ -338,4 +336,4 @@ public abstract class AbstractBedrockApi<I, O, SO> {
@JsonProperty("invocationLatency") Long invocationLatency) {
}
}
// @formatter:on
// @formatter:on

View File

@@ -61,9 +61,7 @@ public class BedrockCohereChatModel implements ChatModel, StreamingChatModel {
@Override
public ChatResponse call(Prompt prompt) {
CohereChatResponse response = this.chatApi.chatCompletion(this.createRequest(prompt, false));
List<Generation> generations = response.generations().stream().map(g -> {
return new Generation(g.text());
}).toList();
List<Generation> generations = response.generations().stream().map(g -> new Generation(g.text())).toList();
return new ChatResponse(generations);
}

View File

@@ -41,45 +41,54 @@ public class BedrockCohereChatOptions implements ChatOptions {
* (optional) Use a lower value to decrease randomness in the response. Defaults to
* 0.7.
*/
@JsonProperty("temperature") Double temperature;
@JsonProperty("temperature")
Double temperature;
/**
* (optional) The maximum cumulative probability of tokens to consider when sampling.
* The generative uses combined Top-k and nucleus sampling. Nucleus sampling considers
* the smallest set of tokens whose probability sum is at least topP.
*/
@JsonProperty("p") Double topP;
@JsonProperty("p")
Double topP;
/**
* (optional) Specify the number of token choices the generative uses to generate the
* next token.
*/
@JsonProperty("k") Integer topK;
@JsonProperty("k")
Integer topK;
/**
* (optional) Specify the maximum number of tokens to use in the generated response.
*/
@JsonProperty("max_tokens") Integer maxTokens;
@JsonProperty("max_tokens")
Integer maxTokens;
/**
* (optional) Configure up to four sequences that the generative recognizes. After a
* stop sequence, the generative stops generating further tokens. The returned text
* doesn't contain the stop sequence.
*/
@JsonProperty("stop_sequences") List<String> stopSequences;
@JsonProperty("stop_sequences")
List<String> stopSequences;
/**
* (optional) Specify how and if the token likelihoods are returned with the response.
*/
@JsonProperty("return_likelihoods") ReturnLikelihoods returnLikelihoods;
@JsonProperty("return_likelihoods")
ReturnLikelihoods returnLikelihoods;
/**
* (optional) The maximum number of generations that the generative should return.
*/
@JsonProperty("num_generations") Integer numGenerations;
@JsonProperty("num_generations")
Integer numGenerations;
/**
* Prevents the model from generating unwanted tokens or incentivize the model to include desired tokens.
*/
@JsonProperty("logit_bias") LogitBias logitBias;
@JsonProperty("logit_bias")
LogitBias logitBias;
/**
* (optional) Specifies how the API handles inputs longer than the maximum token
* length.
*/
@JsonProperty("truncate") Truncate truncate;
@JsonProperty("truncate")
Truncate truncate;
// @formatter:on
public static Builder builder() {

View File

@@ -13,9 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// @formatter:off
package org.springframework.ai.bedrock.cohere.api;
// @formatter:off
import java.time.Duration;
import java.util.List;
@@ -412,4 +414,4 @@ public class CohereChatBedrockApi extends
}
}
}
// @formatter:on
// @formatter:on

View File

@@ -13,9 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// @formatter:off
package org.springframework.ai.bedrock.cohere.api;
// @formatter:off
import java.time.Duration;
import java.util.List;
@@ -169,19 +171,23 @@ public class CohereEmbeddingBedrockApi extends
* In search use-cases, use search_document when you encode documents for embeddings that you store in a
* vector database.
*/
@JsonProperty("search_document") SEARCH_DOCUMENT,
@JsonProperty("search_document")
SEARCH_DOCUMENT,
/**
* Use search_query when querying your vector DB to find relevant documents.
*/
@JsonProperty("search_query") SEARCH_QUERY,
@JsonProperty("search_query")
SEARCH_QUERY,
/**
* Use classification when using embeddings as an input to a text classifier.
*/
@JsonProperty("classification") CLASSIFICATION,
@JsonProperty("classification")
CLASSIFICATION,
/**
* Use clustering to cluster the embeddings.
*/
@JsonProperty("clustering") CLUSTERING
@JsonProperty("clustering")
CLUSTERING
}
/**
@@ -224,4 +230,4 @@ public class CohereEmbeddingBedrockApi extends
}
}
// @formatter:on
// @formatter:on

View File

@@ -13,9 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// @formatter:off
package org.springframework.ai.bedrock.jurassic2.api;
// @formatter:off
import java.time.Duration;
import java.util.List;
@@ -409,4 +411,4 @@ public class Ai21Jurassic2ChatBedrockApi extends
}
// @formatter:on
// @formatter:on

View File

@@ -287,12 +287,14 @@ public class LlamaChatBedrockApi extends
/**
* The model has finished generating text for the input prompt.
*/
@JsonProperty("stop") STOP,
@JsonProperty("stop")
STOP,
/**
* The response was truncated because of the response length you set.
*/
@JsonProperty("length") LENGTH
@JsonProperty("length")
LENGTH
}
}
}
// @formatter:on
// @formatter:on

View File

@@ -60,9 +60,10 @@ public class BedrockTitanChatModel implements ChatModel, StreamingChatModel {
@Override
public ChatResponse call(Prompt prompt) {
TitanChatResponse response = this.chatApi.chatCompletion(this.createRequest(prompt));
List<Generation> generations = response.results().stream().map(result -> {
return new Generation(result.outputText());
}).toList();
List<Generation> generations = response.results()
.stream()
.map(result -> new Generation(result.outputText()))
.toList();
return new ChatResponse(generations);
}

View File

@@ -78,7 +78,7 @@ public class BedrockTitanEmbeddingModel extends AbstractEmbeddingModel {
public EmbeddingResponse call(EmbeddingRequest request) {
Assert.notEmpty(request.getInstructions(), "At least one text is required!");
if (request.getInstructions().size() != 1) {
this.logger.warn(
logger.warn(
"Titan Embedding does not support batch embedding. Will make multiple API calls to embed(Document)");
}

View File

@@ -236,7 +236,8 @@ public class TitanChatBedrockApi extends
if (this.temperature == null && this.topP == null && this.maxTokenCount == null
&& this.stopSequences == null) {
return new TitanChatRequest(this.inputText, null);
} else {
}
else {
return new TitanChatRequest(this.inputText,
new TextGenerationConfig(
this.temperature,

View File

@@ -102,7 +102,7 @@ public class TitanEmbeddingBedrockApi extends
/**
* amazon.titan-embed-text-v2
*/
TITAN_EMBED_TEXT_V2("amazon.titan-embed-text-v2:0");;
TITAN_EMBED_TEXT_V2("amazon.titan-embed-text-v2:0");
private final String id;
@@ -182,4 +182,4 @@ public class TitanEmbeddingBedrockApi extends
}
}
// @formatter:on
// @formatter:on

View File

@@ -35,8 +35,6 @@ import org.springframework.ai.bedrock.anthropic.api.AnthropicChatBedrockApi.Anth
import static org.assertj.core.api.Assertions.assertThat;
;
/**
* @author Christian Tzolov
*/
@@ -70,7 +68,7 @@ public class AnthropicChatBedrockApiIT {
assertThat(response.stop()).isEqualTo("\n\nHuman:");
assertThat(response.amazonBedrockInvocationMetrics()).isNull();
this.logger.info("" + response);
logger.info("" + response);
}
@Test

View File

@@ -38,7 +38,6 @@ import org.springframework.ai.bedrock.anthropic3.api.Anthropic3ChatBedrockApi.Ch
import org.springframework.ai.bedrock.anthropic3.api.Anthropic3ChatBedrockApi.MediaContent;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.ai.bedrock.anthropic3.api.Anthropic3ChatBedrockApi.DEFAULT_ANTHROPIC_VERSION;
/**
* @author Ben Middleton
@@ -62,12 +61,13 @@ public class Anthropic3ChatBedrockApiIT {
.withTemperature(0.8)
.withMaxTokens(300)
.withTopK(10)
.withAnthropicVersion(DEFAULT_ANTHROPIC_VERSION)
.withAnthropicVersion(
org.springframework.ai.bedrock.anthropic3.api.Anthropic3ChatBedrockApi.DEFAULT_ANTHROPIC_VERSION)
.build();
AnthropicChatResponse response = this.anthropicChatApi.chatCompletion(request);
this.logger.info("" + response.content());
logger.info("" + response.content());
assertThat(response).isNotNull();
assertThat(response.content().get(0).text()).isNotEmpty();
@@ -77,7 +77,7 @@ public class Anthropic3ChatBedrockApiIT {
assertThat(response.usage().inputTokens()).isGreaterThan(10);
assertThat(response.usage().outputTokens()).isGreaterThan(100);
this.logger.info("" + response);
logger.info("" + response);
}
@Test
@@ -102,12 +102,13 @@ public class Anthropic3ChatBedrockApiIT {
.withTemperature(0.8)
.withMaxTokens(400)
.withTopK(10)
.withAnthropicVersion(DEFAULT_ANTHROPIC_VERSION)
.withAnthropicVersion(
org.springframework.ai.bedrock.anthropic3.api.Anthropic3ChatBedrockApi.DEFAULT_ANTHROPIC_VERSION)
.build();
AnthropicChatResponse response = this.anthropicChatApi.chatCompletion(request);
this.logger.info("" + response.content());
logger.info("" + response.content());
assertThat(response).isNotNull();
assertThat(response.content().get(0).text()).isNotEmpty();
assertThat(response.content().get(0).text()).contains("Blackbeard");
@@ -116,7 +117,7 @@ public class Anthropic3ChatBedrockApiIT {
assertThat(response.usage().inputTokens()).isGreaterThan(30);
assertThat(response.usage().outputTokens()).isGreaterThan(200);
this.logger.info("" + response);
logger.info("" + response);
}
@Test
@@ -128,7 +129,8 @@ public class Anthropic3ChatBedrockApiIT {
.withTemperature(0.8)
.withMaxTokens(300)
.withTopK(10)
.withAnthropicVersion(DEFAULT_ANTHROPIC_VERSION)
.withAnthropicVersion(
org.springframework.ai.bedrock.anthropic3.api.Anthropic3ChatBedrockApi.DEFAULT_ANTHROPIC_VERSION)
.build();
Flux<Anthropic3ChatBedrockApi.AnthropicChatStreamingResponse> responseStream = this.anthropicChatApi

View File

@@ -57,4 +57,4 @@ class BedrockRuntimeHintsTests {
}
}
}

View File

@@ -35,8 +35,6 @@ import org.springframework.ai.model.ModelOptionsUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
;
/**
* @author Christian Tzolov
*/

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* 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
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://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,
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.bedrock.titan;
import java.time.Duration;
@@ -66,8 +67,8 @@ class BedrockTitanChatModelIT {
@Test
void multipleStreamAttempts() {
Flux<ChatResponse> joke1Stream = chatModel.stream(new Prompt(new UserMessage("Tell me a joke?")));
Flux<ChatResponse> joke2Stream = chatModel.stream(new Prompt(new UserMessage("Tell me a toy joke?")));
Flux<ChatResponse> joke1Stream = this.chatModel.stream(new Prompt(new UserMessage("Tell me a joke?")));
Flux<ChatResponse> joke2Stream = this.chatModel.stream(new Prompt(new UserMessage("Tell me a toy joke?")));
String joke1 = joke1Stream.collectList()
.block()
@@ -96,10 +97,10 @@ class BedrockTitanChatModelIT {
String name = "Bob";
String voice = "pirate";
UserMessage userMessage = new UserMessage(request);
SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(systemResource);
SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(this.systemResource);
Message systemMessage = systemPromptTemplate.createMessage(Map.of("name", name, "voice", voice));
Prompt prompt = new Prompt(List.of(userMessage, systemMessage));
ChatResponse response = chatModel.call(prompt);
ChatResponse response = this.chatModel.call(prompt);
assertThat(response.getResult().getOutput().getContent()).contains("Blackbeard");
}
@@ -136,16 +137,13 @@ class BedrockTitanChatModelIT {
Map.of("subject", "an array of numbers from 1 to 9 under they key name 'numbers'", "format", format));
Prompt prompt = new Prompt(promptTemplate.createMessage());
Generation generation = chatModel.call(prompt).getResult();
Generation generation = this.chatModel.call(prompt).getResult();
Map<String, Object> result = outputConverter.convert(generation.getOutput().getContent());
assertThat(result.get("numbers")).isEqualTo(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9));
}
record ActorsFilmsRecord(String actor, List<String> movies) {
}
@Disabled("TODO: Fix the converter instructions to return the correct format")
@Test
void beanOutputConverterRecords() {
@@ -160,7 +158,7 @@ class BedrockTitanChatModelIT {
""";
PromptTemplate promptTemplate = new PromptTemplate(template, Map.of("format", format));
Prompt prompt = new Prompt(promptTemplate.createMessage());
Generation generation = chatModel.call(prompt).getResult();
Generation generation = this.chatModel.call(prompt).getResult();
ActorsFilmsRecord actorsFilms = outputConverter.convert(generation.getOutput().getContent());
assertThat(actorsFilms.actor()).isEqualTo("Tom Hanks");
@@ -182,7 +180,7 @@ class BedrockTitanChatModelIT {
PromptTemplate promptTemplate = new PromptTemplate(template, Map.of("format", format));
Prompt prompt = new Prompt(promptTemplate.createMessage());
String generationTextFromStream = chatModel.stream(prompt)
String generationTextFromStream = this.chatModel.stream(prompt)
.collectList()
.block()
.stream()
@@ -215,4 +213,7 @@ class BedrockTitanChatModelIT {
}
record ActorsFilmsRecord(String actor, List<String> movies) {
}
}

View File

@@ -35,6 +35,10 @@
<developerConnection>git@github.com:spring-projects/spring-ai.git</developerConnection>
</scm>
<properties>
<disable.checks>false</disable.checks>
</properties>
<dependencies>
<!-- production dependencies -->

View File

@@ -44,15 +44,15 @@ public class ClientIT {
address: #1 Samuel St.
Just generate the JSON object without explanations:
[/INST]
""";
""";
Prompt prompt = new Prompt(mistral7bInstruct);
ChatResponse chatResponse = this.huggingfaceChatModel.call(prompt);
assertThat(chatResponse.getResult().getOutput().getContent()).isNotEmpty();
String expectedResponse = """
{
"name": "John",
"lastname": "Smith",
"address": "#1 Samuel St."
"name": "John",
"lastname": "Smith",
"address": "#1 Samuel St."
}""";
assertThat(chatResponse.getResult().getOutput().getContent()).isEqualTo(expectedResponse);
assertThat(chatResponse.getResult().getOutput().getMetadata()).containsKey("generated_tokens");

View File

@@ -36,6 +36,10 @@
<developerConnection>git@github.com:spring-projects/spring-ai.git</developerConnection>
</scm>
<properties>
<disable.checks>false</disable.checks>
</properties>
<dependencies>
<!-- production dependencies -->

View File

@@ -73,8 +73,6 @@ import org.springframework.retry.support.RetryTemplate;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import static org.springframework.ai.minimax.api.MiniMaxApiConstants.TOOL_CALL_FUNCTION_TYPE;
/**
* {@link ChatModel} and {@link StreamingChatModel} implementation for {@literal MiniMax}
* backed by {@link MiniMaxApi}.
@@ -246,9 +244,10 @@ public class MiniMaxChatModel extends AbstractToolCallSupport implements ChatMod
// @formatter:off
// if the choice is a web search tool call, return last message of choice.messages
ChatCompletionMessage message = null;
if(choice.message() != null) {
if (choice.message() != null) {
message = choice.message();
} else if(!CollectionUtils.isEmpty(choice.messages())){
}
else if (!CollectionUtils.isEmpty(choice.messages())) {
// the MiniMax web search messages result is ['user message','assistant tool call', 'tool call', 'assistant message']
// so the last message is the assistant message
message = choice.messages().get(choice.messages().size() - 1);
@@ -328,7 +327,8 @@ public class MiniMaxChatModel extends AbstractToolCallSupport implements ChatMod
return buildGeneration(choice, metadata);
}).toList();
return new ChatResponse(generations, from(chatCompletion2));
} catch (Exception e) {
}
catch (Exception e) {
logger.error("Error processing chat completion", e);
return new ChatResponse(List.of());
}
@@ -368,7 +368,8 @@ public class MiniMaxChatModel extends AbstractToolCallSupport implements ChatMod
return generation.getOutput()
.getToolCalls()
.stream()
.anyMatch(toolCall -> TOOL_CALL_FUNCTION_TYPE.equals(toolCall.type()));
.anyMatch(toolCall -> org.springframework.ai.minimax.api.MiniMaxApiConstants.TOOL_CALL_FUNCTION_TYPE
.equals(toolCall.type()));
}
private ChatOptions buildRequestOptions(ChatCompletionRequest request) {
@@ -456,9 +457,8 @@ public class MiniMaxChatModel extends AbstractToolCallSupport implements ChatMod
else if (message.getMessageType() == MessageType.TOOL) {
ToolResponseMessage toolMessage = (ToolResponseMessage) message;
toolMessage.getResponses().forEach(response -> {
Assert.isTrue(response.id() != null, "ToolResponseMessage must have an id");
});
toolMessage.getResponses()
.forEach(response -> Assert.isTrue(response.id() != null, "ToolResponseMessage must have an id"));
return toolMessage.getResponses()
.stream()

View File

@@ -37,8 +37,9 @@ public class MiniMaxRuntimeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(@NonNull RuntimeHints hints, @Nullable ClassLoader classLoader) {
var mcs = MemberCategory.values();
for (var tr : findJsonAnnotatedClassesInPackage(MiniMaxApi.class))
for (var tr : findJsonAnnotatedClassesInPackage(MiniMaxApi.class)) {
hints.reflection().registerType(tr, mcs);
}
}
}

View File

@@ -103,7 +103,7 @@ public class MiniMaxApi {
*/
public MiniMaxApi(String baseUrl, String miniMaxToken, RestClient.Builder restClientBuilder, ResponseErrorHandler responseErrorHandler) {
Consumer<HttpHeaders> authHeaders = (headers) -> {
Consumer<HttpHeaders> authHeaders = headers -> {
headers.setBearerAuth(miniMaxToken);
headers.setContentType(MediaType.APPLICATION_JSON);
};
@@ -251,27 +251,33 @@ public class MiniMaxApi {
/**
* The model hit a natural stop point or a provided stop sequence.
*/
@JsonProperty("stop") STOP,
@JsonProperty("stop")
STOP,
/**
* The maximum number of tokens specified in the request was reached.
*/
@JsonProperty("length") LENGTH,
@JsonProperty("length")
LENGTH,
/**
* The content was omitted due to a flag from our content filters.
*/
@JsonProperty("content_filter") CONTENT_FILTER,
@JsonProperty("content_filter")
CONTENT_FILTER,
/**
* The model called a tool.
*/
@JsonProperty("tool_calls") TOOL_CALLS,
@JsonProperty("tool_calls")
TOOL_CALLS,
/**
* (deprecated) The model called a function.
*/
@JsonProperty("function_call") FUNCTION_CALL,
@JsonProperty("function_call")
FUNCTION_CALL,
/**
* Only for compatibility with Mistral AI API.
*/
@JsonProperty("tool_call") TOOL_CALL
@JsonProperty("tool_call")
TOOL_CALL
}
/**
@@ -353,8 +359,10 @@ public class MiniMaxApi {
/**
* Function tool type.
*/
@JsonProperty("function") FUNCTION,
@JsonProperty("web_search") WEB_SEARCH
@JsonProperty("function")
FUNCTION,
@JsonProperty("web_search")
WEB_SEARCH
}
/**
@@ -385,7 +393,7 @@ public class MiniMaxApi {
}
}
/**
/**
* Creates a model response for the given chat conversation.
*
* @param messages A list of messages comprising the conversation so far.
@@ -425,7 +433,7 @@ public class MiniMaxApi {
* functions are present. Use the {@link ToolChoiceBuilder} to create the tool choice value.
*/
@JsonInclude(Include.NON_NULL)
public record ChatCompletionRequest (
public record ChatCompletionRequest(
@JsonProperty("messages") List<ChatCompletionMessage> messages,
@JsonProperty("model") String model,
@JsonProperty("frequency_penalty") Double frequencyPenalty,
@@ -451,7 +459,7 @@ public class MiniMaxApi {
*/
public ChatCompletionRequest(List<ChatCompletionMessage> messages, String model, Double temperature) {
this(messages, model, null, null, null, null,
null, null, null, false, temperature, null,null,
null, null, null, false, temperature, null, null,
null, null);
}
@@ -466,7 +474,7 @@ public class MiniMaxApi {
*/
public ChatCompletionRequest(List<ChatCompletionMessage> messages, String model, Double temperature, boolean stream) {
this(messages, model, null, null, null, null,
null, null, null, stream, temperature, null,null,
null, null, null, stream, temperature, null, null,
null, null);
}
@@ -482,11 +490,11 @@ public class MiniMaxApi {
public ChatCompletionRequest(List<ChatCompletionMessage> messages, String model,
List<FunctionTool> tools, Object toolChoice) {
this(messages, model, null, null, null, null,
null, null, null, false, 0.8, null,null,
null, null, null, false, 0.8, null, null,
tools, toolChoice);
}
/**
/**
* Shortcut constructor for a chat completion request with the given messages, model, tools and tool choice.
* Streaming is set to false, temperature to 0.8 and all other parameters are null.
*
@@ -496,7 +504,7 @@ public class MiniMaxApi {
*/
public ChatCompletionRequest(List<ChatCompletionMessage> messages, Boolean stream) {
this(messages, null, null, null, null, null,
null, null, null, stream, null, null,null,
null, null, null, stream, null, null, null,
null, null);
}
@@ -582,19 +590,23 @@ public class MiniMaxApi {
/**
* System message.
*/
@JsonProperty("system") SYSTEM,
@JsonProperty("system")
SYSTEM,
/**
* User message.
*/
@JsonProperty("user") USER,
@JsonProperty("user")
USER,
/**
* Assistant message.
*/
@JsonProperty("assistant") ASSISTANT,
@JsonProperty("assistant")
ASSISTANT,
/**
* Tool message.
*/
@JsonProperty("tool") TOOL
@JsonProperty("tool")
TOOL
}
/**
@@ -714,11 +726,10 @@ public class MiniMaxApi {
@JsonProperty("logprobs") LogProbs logprobs) {
}
public record BaseResponse(
@JsonProperty("status_code") Long statusCode,
@JsonProperty("status_msg") String message
){}
) { }
}
/**

View File

@@ -32,4 +32,8 @@ public final class MiniMaxApiConstants {
public static final String PROVIDER_NAME = AiProvider.MINIMAX.value();
private MiniMaxApiConstants() {
}
}

View File

@@ -70,7 +70,7 @@ public class ChatCompletionRequestTests {
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName(TOOL_FUNCTION_NAME)
.withDescription("Get the weather in location")
.withResponseConverter((response) -> "" + response.temp() + response.unit())
.withResponseConverter(response -> "" + response.temp() + response.unit())
.build()))
.build()),
false);
@@ -97,7 +97,7 @@ public class ChatCompletionRequestTests {
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName(TOOL_FUNCTION_NAME)
.withDescription("Get the weather in location")
.withResponseConverter((response) -> "" + response.temp() + response.unit())
.withResponseConverter(response -> "" + response.temp() + response.unit())
.build()))
.build());

View File

@@ -131,7 +131,7 @@ public class MiniMaxApiToolFunctionCallIT {
ResponseEntity<ChatCompletion> chatCompletion2 = this.miniMaxApi.chatCompletionEntity(functionResponseRequest);
this.logger.info("Final response: " + chatCompletion2.getBody());
logger.info("Final response: " + chatCompletion2.getBody());
assertThat(Objects.requireNonNull(chatCompletion2.getBody()).choices()).isNotEmpty();

View File

@@ -51,7 +51,7 @@ import org.springframework.retry.support.RetryTemplate;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.Mockito.when;
import static org.mockito.BDDMockito.given;
/**
* @author Geng Rong
@@ -87,13 +87,13 @@ public class MiniMaxRetryTests {
var choice = new ChatCompletion.Choice(ChatCompletionFinishReason.STOP, 0,
new ChatCompletionMessage("Response", Role.ASSISTANT), null, null);
ChatCompletion expectedChatCompletion = new ChatCompletion("id", List.of(choice), 666l, "model", null, null,
ChatCompletion expectedChatCompletion = new ChatCompletion("id", List.of(choice), 666L, "model", null, null,
null, new MiniMaxApi.Usage(10, 10, 10));
when(this.miniMaxApi.chatCompletionEntity(isA(ChatCompletionRequest.class)))
.thenThrow(new TransientAiException("Transient Error 1"))
.thenThrow(new TransientAiException("Transient Error 2"))
.thenReturn(ResponseEntity.of(Optional.of(expectedChatCompletion)));
given(this.miniMaxApi.chatCompletionEntity(isA(ChatCompletionRequest.class)))
.willThrow(new TransientAiException("Transient Error 1"))
.willThrow(new TransientAiException("Transient Error 2"))
.willReturn(ResponseEntity.of(Optional.of(expectedChatCompletion)));
var result = this.chatModel.call(new Prompt("text"));
@@ -105,8 +105,8 @@ public class MiniMaxRetryTests {
@Test
public void miniMaxChatNonTransientError() {
when(this.miniMaxApi.chatCompletionEntity(isA(ChatCompletionRequest.class)))
.thenThrow(new RuntimeException("Non Transient Error"));
given(this.miniMaxApi.chatCompletionEntity(isA(ChatCompletionRequest.class)))
.willThrow(new RuntimeException("Non Transient Error"));
assertThrows(RuntimeException.class, () -> this.chatModel.call(new Prompt("text")));
}
@@ -115,13 +115,13 @@ public class MiniMaxRetryTests {
var choice = new ChatCompletionChunk.ChunkChoice(ChatCompletionFinishReason.STOP, 0,
new ChatCompletionMessage("Response", Role.ASSISTANT), null);
ChatCompletionChunk expectedChatCompletion = new ChatCompletionChunk("id", List.of(choice), 666l, "model", null,
ChatCompletionChunk expectedChatCompletion = new ChatCompletionChunk("id", List.of(choice), 666L, "model", null,
null);
when(this.miniMaxApi.chatCompletionStream(isA(ChatCompletionRequest.class)))
.thenThrow(new TransientAiException("Transient Error 1"))
.thenThrow(new TransientAiException("Transient Error 2"))
.thenReturn(Flux.just(expectedChatCompletion));
given(this.miniMaxApi.chatCompletionStream(isA(ChatCompletionRequest.class)))
.willThrow(new TransientAiException("Transient Error 1"))
.willThrow(new TransientAiException("Transient Error 2"))
.willReturn(Flux.just(expectedChatCompletion));
var result = this.chatModel.stream(new Prompt("text"));
@@ -133,8 +133,8 @@ public class MiniMaxRetryTests {
@Test
public void miniMaxChatStreamNonTransientError() {
when(this.miniMaxApi.chatCompletionStream(isA(ChatCompletionRequest.class)))
.thenThrow(new RuntimeException("Non Transient Error"));
given(this.miniMaxApi.chatCompletionStream(isA(ChatCompletionRequest.class)))
.willThrow(new RuntimeException("Non Transient Error"));
assertThrows(RuntimeException.class, () -> this.chatModel.stream(new Prompt("text")).collectList().block());
}
@@ -143,10 +143,10 @@ public class MiniMaxRetryTests {
EmbeddingList expectedEmbeddings = new EmbeddingList(List.of(new float[] { 9.9f, 8.8f }), "model", 10);
when(this.miniMaxApi.embeddings(isA(EmbeddingRequest.class)))
.thenThrow(new TransientAiException("Transient Error 1"))
.thenThrow(new TransientAiException("Transient Error 2"))
.thenReturn(ResponseEntity.of(Optional.of(expectedEmbeddings)));
given(this.miniMaxApi.embeddings(isA(EmbeddingRequest.class)))
.willThrow(new TransientAiException("Transient Error 1"))
.willThrow(new TransientAiException("Transient Error 2"))
.willReturn(ResponseEntity.of(Optional.of(expectedEmbeddings)));
var result = this.embeddingModel
.call(new org.springframework.ai.embedding.EmbeddingRequest(List.of("text1", "text2"), null));
@@ -159,8 +159,8 @@ public class MiniMaxRetryTests {
@Test
public void miniMaxEmbeddingNonTransientError() {
when(this.miniMaxApi.embeddings(isA(EmbeddingRequest.class)))
.thenThrow(new RuntimeException("Non Transient Error"));
given(this.miniMaxApi.embeddings(isA(EmbeddingRequest.class)))
.willThrow(new RuntimeException("Non Transient Error"));
assertThrows(RuntimeException.class, () -> this.embeddingModel
.call(new org.springframework.ai.embedding.EmbeddingRequest(List.of("text1", "text2"), null)));
}

View File

@@ -65,7 +65,7 @@ public class MockWeatherService implements Function<MockWeatherService.Request,
*/
public final String unitName;
private Unit(String text) {
Unit(String text) {
this.unitName = text;
}

View File

@@ -38,7 +38,6 @@ import org.springframework.ai.minimax.MiniMaxChatOptions;
import org.springframework.ai.minimax.api.MiniMaxApi;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.ai.minimax.api.MiniMaxApi.ChatModel.ABAB_6_5_S_Chat;
/**
* @author Geng Rong
@@ -93,7 +92,7 @@ public class MiniMaxChatOptionsTests {
List<MiniMaxApi.FunctionTool> functionTool = List.of(MiniMaxApi.FunctionTool.webSearchFunctionTool());
MiniMaxChatOptions options = MiniMaxChatOptions.builder()
.withModel(ABAB_6_5_S_Chat.value)
.withModel(org.springframework.ai.minimax.api.MiniMaxApi.ChatModel.ABAB_6_5_S_Chat.value)
.withTools(functionTool)
.build();
@@ -123,7 +122,7 @@ public class MiniMaxChatOptionsTests {
List<MiniMaxApi.FunctionTool> functionTool = List.of(MiniMaxApi.FunctionTool.webSearchFunctionTool());
MiniMaxChatOptions options = MiniMaxChatOptions.builder()
.withModel(ABAB_6_5_S_Chat.value)
.withModel(org.springframework.ai.minimax.api.MiniMaxApi.ChatModel.ABAB_6_5_S_Chat.value)
.withTools(functionTool)
.build();

View File

@@ -37,6 +37,10 @@
<developerConnection>git@github.com:spring-projects/spring-ai.git</developerConnection>
</scm>
<properties>
<disable.checks>false</disable.checks>
</properties>
<dependencies>
<!-- production dependencies -->

View File

@@ -178,7 +178,7 @@ public class MistralAiChatModel extends AbstractToolCallSupport implements ChatM
ChatCompletion chatCompletion = completionEntity.getBody();
if (chatCompletion == null) {
this.logger.warn("No chat completion returned for prompt: {}", prompt);
logger.warn("No chat completion returned for prompt: {}", prompt);
return new ChatResponse(List.of());
}
@@ -266,7 +266,7 @@ public class MistralAiChatModel extends AbstractToolCallSupport implements ChatM
}
}
catch (Exception e) {
this.logger.error("Error processing chat completion", e);
logger.error("Error processing chat completion", e);
return new ChatResponse(List.of());
}
}));
@@ -284,9 +284,7 @@ public class MistralAiChatModel extends AbstractToolCallSupport implements ChatM
}
})
.doOnError(observation::error)
.doFinally(s -> {
observation.stop();
})
.doFinally(s -> observation.stop())
.contextWrite(ctx -> ctx.put(ObservationThreadLocalAccessor.KEY, observation));
// @formatter:on;
@@ -349,9 +347,8 @@ public class MistralAiChatModel extends AbstractToolCallSupport implements ChatM
}
else if (message instanceof ToolResponseMessage toolResponseMessage) {
toolResponseMessage.getResponses().forEach(response -> {
Assert.isTrue(response.id() != null, "ToolResponseMessage must have an id");
});
toolResponseMessage.getResponses()
.forEach(response -> Assert.isTrue(response.id() != null, "ToolResponseMessage must have an id"));
return toolResponseMessage.getResponses()
.stream()

View File

@@ -23,8 +23,6 @@ import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import static org.springframework.ai.aot.AiRuntimeHints.findJsonAnnotatedClassesInPackage;
/**
* The MistralAiRuntimeHints class is responsible for registering runtime hints for
* Mistral AI API classes.
@@ -37,8 +35,9 @@ public class MistralAiRuntimeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(@NonNull RuntimeHints hints, @Nullable ClassLoader classLoader) {
var mcs = MemberCategory.values();
for (var tr : findJsonAnnotatedClassesInPackage(MistralAiApi.class))
for (var tr : org.springframework.ai.aot.AiRuntimeHints.findJsonAnnotatedClassesInPackage(MistralAiApi.class)) {
hints.reflection().registerType(tr, mcs);
}
}
}

View File

@@ -221,26 +221,32 @@ public class MistralAiApi {
public enum ChatCompletionFinishReason {
// @formatter:off
/**
* The model hit a natural stop point or a provided stop sequence.
*/
@JsonProperty("stop") STOP,
/**
* The maximum number of tokens specified in the request was reached.
*/
@JsonProperty("length") LENGTH,
/**
* The content was omitted due to a flag from our content filters.
*/
@JsonProperty("model_length") MODEL_LENGTH,
/**
*
*/
@JsonProperty("error") ERROR,
/**
* The model requested a tool call.
*/
@JsonProperty("tool_calls") TOOL_CALLS
/**
* The model hit a natural stop point or a provided stop sequence.
*/
@JsonProperty("stop")
STOP,
/**
* The maximum number of tokens specified in the request was reached.
*/
@JsonProperty("length")
LENGTH,
/**
* The content was omitted due to a flag from our content filters.
*/
@JsonProperty("model_length")
MODEL_LENGTH,
@JsonProperty("error")
ERROR,
/**
* The model requested a tool call.
*/
@JsonProperty("tool_calls")
TOOL_CALLS
// @formatter:on
}
@@ -257,18 +263,18 @@ public class MistralAiApi {
public enum ChatModel implements ChatModelDescription {
// @formatter:off
@Deprecated(since = "1.0.0-M1", forRemoval = true) // Replaced by OPEN_MISTRAL_7B
TINY("open-mistral-7b"),
@Deprecated(since = "1.0.0-M1", forRemoval = true) // Replaced by OPEN_MIXTRAL_7B
MIXTRAL("open-mixtral-8x7b"),
OPEN_MISTRAL_7B("open-mistral-7b"),
OPEN_MIXTRAL_7B("open-mixtral-8x7b"),
OPEN_MIXTRAL_22B("open-mixtral-8x22b"),
SMALL("mistral-small-latest"),
@Deprecated(since = "1.0.0-M1", forRemoval = true) // Mistral is removing this model
MEDIUM("mistral-medium-latest"),
LARGE("mistral-large-latest");
// @formatter:on
@Deprecated(since = "1.0.0-M1", forRemoval = true) // Replaced by OPEN_MISTRAL_7B
TINY("open-mistral-7b"),
@Deprecated(since = "1.0.0-M1", forRemoval = true) // Replaced by OPEN_MIXTRAL_7B
MIXTRAL("open-mixtral-8x7b"),
OPEN_MISTRAL_7B("open-mistral-7b"),
OPEN_MIXTRAL_7B("open-mixtral-8x7b"),
OPEN_MIXTRAL_22B("open-mixtral-8x22b"),
SMALL("mistral-small-latest"),
@Deprecated(since = "1.0.0-M1", forRemoval = true) // Mistral is removing this model
MEDIUM("mistral-medium-latest"),
LARGE("mistral-large-latest");
// @formatter:on
private final String value;
@@ -294,7 +300,7 @@ public class MistralAiApi {
public enum EmbeddingModel {
// @formatter:off
EMBED("mistral-embed");
EMBED("mistral-embed");
// @formatter:on
private final String value;
@@ -380,9 +386,9 @@ public class MistralAiApi {
@JsonInclude(Include.NON_NULL)
public record Usage(
// @formatter:off
@JsonProperty("prompt_tokens") Integer promptTokens,
@JsonProperty("total_tokens") Integer totalTokens,
@JsonProperty("completion_tokens") Integer completionTokens) {
@JsonProperty("prompt_tokens") Integer promptTokens,
@JsonProperty("total_tokens") Integer totalTokens,
@JsonProperty("completion_tokens") Integer completionTokens) {
// @formatter:on
}
@@ -397,9 +403,9 @@ public class MistralAiApi {
@JsonInclude(Include.NON_NULL)
public record Embedding(
// @formatter:off
@JsonProperty("index") Integer index,
@JsonProperty("embedding") float[] embedding,
@JsonProperty("object") String object) {
@JsonProperty("index") Integer index,
@JsonProperty("embedding") float[] embedding,
@JsonProperty("object") String object) {
// @formatter:on
/**
@@ -451,9 +457,9 @@ public class MistralAiApi {
@JsonInclude(Include.NON_NULL)
public record EmbeddingRequest<T>(
// @formatter:off
@JsonProperty("input") T input,
@JsonProperty("model") String model,
@JsonProperty("encoding_format") String encodingFormat) {
@JsonProperty("input") T input,
@JsonProperty("model") String model,
@JsonProperty("encoding_format") String encodingFormat) {
// @formatter:on
/**
@@ -489,10 +495,10 @@ public class MistralAiApi {
@JsonInclude(Include.NON_NULL)
public record EmbeddingList<T>(
// @formatter:off
@JsonProperty("object") String object,
@JsonProperty("data") List<T> data,
@JsonProperty("model") String model,
@JsonProperty("usage") Usage usage) {
@JsonProperty("object") String object,
@JsonProperty("data") List<T> data,
@JsonProperty("model") String model,
@JsonProperty("usage") Usage usage) {
// @formatter:on
}
@@ -535,18 +541,18 @@ public class MistralAiApi {
@JsonInclude(Include.NON_NULL)
public record ChatCompletionRequest(
// @formatter:off
@JsonProperty("model") String model,
@JsonProperty("messages") List<ChatCompletionMessage> messages,
@JsonProperty("tools") List<FunctionTool> tools,
@JsonProperty("tool_choice") ToolChoice toolChoice,
@JsonProperty("temperature") Double temperature,
@JsonProperty("top_p") Double topP,
@JsonProperty("max_tokens") Integer maxTokens,
@JsonProperty("stream") Boolean stream,
@JsonProperty("safe_prompt") Boolean safePrompt,
@JsonProperty("stop") List<String> stop,
@JsonProperty("random_seed") Integer randomSeed,
@JsonProperty("response_format") ResponseFormat responseFormat) {
@JsonProperty("model") String model,
@JsonProperty("messages") List<ChatCompletionMessage> messages,
@JsonProperty("tools") List<FunctionTool> tools,
@JsonProperty("tool_choice") ToolChoice toolChoice,
@JsonProperty("temperature") Double temperature,
@JsonProperty("top_p") Double topP,
@JsonProperty("max_tokens") Integer maxTokens,
@JsonProperty("stream") Boolean stream,
@JsonProperty("safe_prompt") Boolean safePrompt,
@JsonProperty("stop") List<String> stop,
@JsonProperty("random_seed") Integer randomSeed,
@JsonProperty("response_format") ResponseFormat responseFormat) {
// @formatter:on
/**
@@ -619,9 +625,12 @@ public class MistralAiApi {
public enum ToolChoice {
// @formatter:off
@JsonProperty("auto") AUTO,
@JsonProperty("any") ANY,
@JsonProperty("none") NONE
@JsonProperty("auto")
AUTO,
@JsonProperty("any")
ANY,
@JsonProperty("none")
NONE
// @formatter:on
}
@@ -652,12 +661,17 @@ public class MistralAiApi {
@JsonInclude(Include.NON_NULL)
public record ChatCompletionMessage(
// @formatter:off
@JsonProperty("content") String content,
@JsonProperty("role") Role role,
@JsonProperty("name") String name,
@JsonProperty("tool_calls") List<ToolCall> toolCalls,
@JsonProperty("tool_call_id") String toolCallId) {
// @formatter:on
@JsonProperty("content")
String content,
@JsonProperty("role")
Role role,
@JsonProperty("name")
String name,
@JsonProperty("tool_calls")
List<ToolCall> toolCalls,
@JsonProperty("tool_call_id")
String toolCallId) {
// @formatter:on
/**
* Message comprising the conversation.
@@ -690,10 +704,14 @@ public class MistralAiApi {
public enum Role {
// @formatter:off
@JsonProperty("system") SYSTEM,
@JsonProperty("user") USER,
@JsonProperty("assistant") ASSISTANT,
@JsonProperty("tool") TOOL
@JsonProperty("system")
SYSTEM,
@JsonProperty("user")
USER,
@JsonProperty("assistant")
ASSISTANT,
@JsonProperty("tool")
TOOL
// @formatter:on
}
@@ -743,12 +761,12 @@ public class MistralAiApi {
@JsonInclude(Include.NON_NULL)
public record ChatCompletion(
// @formatter:off
@JsonProperty("id") String id,
@JsonProperty("object") String object,
@JsonProperty("created") Long created,
@JsonProperty("model") String model,
@JsonProperty("choices") List<Choice> choices,
@JsonProperty("usage") Usage usage) {
@JsonProperty("id") String id,
@JsonProperty("object") String object,
@JsonProperty("created") Long created,
@JsonProperty("model") String model,
@JsonProperty("choices") List<Choice> choices,
@JsonProperty("usage") Usage usage) {
// @formatter:on
/**
@@ -762,9 +780,9 @@ public class MistralAiApi {
@JsonInclude(Include.NON_NULL)
public record Choice(
// @formatter:off
@JsonProperty("index") Integer index,
@JsonProperty("message") ChatCompletionMessage message,
@JsonProperty("finish_reason") ChatCompletionFinishReason finishReason,
@JsonProperty("index") Integer index,
@JsonProperty("message") ChatCompletionMessage message,
@JsonProperty("finish_reason") ChatCompletionFinishReason finishReason,
@JsonProperty("logprobs") LogProbs logprobs) {
// @formatter:on
}
@@ -835,11 +853,11 @@ public class MistralAiApi {
@JsonInclude(Include.NON_NULL)
public record ChatCompletionChunk(
// @formatter:off
@JsonProperty("id") String id,
@JsonProperty("object") String object,
@JsonProperty("created") Long created,
@JsonProperty("model") String model,
@JsonProperty("choices") List<ChunkChoice> choices) {
@JsonProperty("id") String id,
@JsonProperty("object") String object,
@JsonProperty("created") Long created,
@JsonProperty("model") String model,
@JsonProperty("choices") List<ChunkChoice> choices) {
// @formatter:on
/**
@@ -853,9 +871,9 @@ public class MistralAiApi {
@JsonInclude(Include.NON_NULL)
public record ChunkChoice(
// @formatter:off
@JsonProperty("index") Integer index,
@JsonProperty("delta") ChatCompletionMessage delta,
@JsonProperty("finish_reason") ChatCompletionFinishReason finishReason,
@JsonProperty("index") Integer index,
@JsonProperty("delta") ChatCompletionMessage delta,
@JsonProperty("finish_reason") ChatCompletionFinishReason finishReason,
@JsonProperty("logprobs") LogProbs logprobs) {
// @formatter:on
}

View File

@@ -106,7 +106,7 @@ class MistralAiChatClientIT {
.user(u -> u.text("List five {subject}")
.param("subject", "ice cream flavors"))
.call()
.entity(new ParameterizedTypeReference<List<String>>() {});
.entity(new ParameterizedTypeReference<>() { });
// @formatter:on
logger.info(collection.toString());
@@ -298,4 +298,4 @@ class MistralAiChatClientIT {
}
}
}

View File

@@ -196,7 +196,7 @@ class MistralAiChatModelIT {
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("getCurrentWeather")
.withDescription("Get the weather in location")
.withResponseConverter((response) -> "" + response.temp() + response.unit())
.withResponseConverter(response -> "" + response.temp() + response.unit())
.build()))
.build();
@@ -219,7 +219,7 @@ class MistralAiChatModelIT {
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("getCurrentWeather")
.withDescription("Get the weather in location")
.withResponseConverter((response) -> "" + response.temp() + response.unit())
.withResponseConverter(response -> "" + response.temp() + response.unit())
.build()))
.build();

View File

@@ -50,7 +50,7 @@ import org.springframework.retry.support.RetryTemplate;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.Mockito.when;
import static org.mockito.BDDMockito.given;
/**
* @author Christian Tzolov
@@ -97,10 +97,10 @@ public class MistralAiRetryTests {
ChatCompletion expectedChatCompletion = new ChatCompletion("id", "chat.completion", 789L, "model",
List.of(choice), new MistralAiApi.Usage(10, 10, 10));
when(this.mistralAiApi.chatCompletionEntity(isA(ChatCompletionRequest.class)))
.thenThrow(new TransientAiException("Transient Error 1"))
.thenThrow(new TransientAiException("Transient Error 2"))
.thenReturn(ResponseEntity.of(Optional.of(expectedChatCompletion)));
given(this.mistralAiApi.chatCompletionEntity(isA(ChatCompletionRequest.class)))
.willThrow(new TransientAiException("Transient Error 1"))
.willThrow(new TransientAiException("Transient Error 2"))
.willReturn(ResponseEntity.of(Optional.of(expectedChatCompletion)));
var result = this.chatModel.call(new Prompt("text"));
@@ -112,8 +112,8 @@ public class MistralAiRetryTests {
@Test
public void mistralAiChatNonTransientError() {
when(this.mistralAiApi.chatCompletionEntity(isA(ChatCompletionRequest.class)))
.thenThrow(new RuntimeException("Non Transient Error"));
given(this.mistralAiApi.chatCompletionEntity(isA(ChatCompletionRequest.class)))
.willThrow(new RuntimeException("Non Transient Error"));
assertThrows(RuntimeException.class, () -> this.chatModel.call(new Prompt("text")));
}
@@ -126,10 +126,10 @@ public class MistralAiRetryTests {
ChatCompletionChunk expectedChatCompletion = new ChatCompletionChunk("id", "chat.completion.chunk", 789L,
"model", List.of(choice));
when(this.mistralAiApi.chatCompletionStream(isA(ChatCompletionRequest.class)))
.thenThrow(new TransientAiException("Transient Error 1"))
.thenThrow(new TransientAiException("Transient Error 2"))
.thenReturn(Flux.just(expectedChatCompletion));
given(this.mistralAiApi.chatCompletionStream(isA(ChatCompletionRequest.class)))
.willThrow(new TransientAiException("Transient Error 1"))
.willThrow(new TransientAiException("Transient Error 2"))
.willReturn(Flux.just(expectedChatCompletion));
var result = this.chatModel.stream(new Prompt("text"));
@@ -142,8 +142,8 @@ public class MistralAiRetryTests {
@Test
@Disabled("Currently stream() does not implement retry")
public void mistralAiChatStreamNonTransientError() {
when(this.mistralAiApi.chatCompletionStream(isA(ChatCompletionRequest.class)))
.thenThrow(new RuntimeException("Non Transient Error"));
given(this.mistralAiApi.chatCompletionStream(isA(ChatCompletionRequest.class)))
.willThrow(new RuntimeException("Non Transient Error"));
assertThrows(RuntimeException.class, () -> this.chatModel.stream(new Prompt("text")));
}
@@ -153,10 +153,10 @@ public class MistralAiRetryTests {
EmbeddingList<Embedding> expectedEmbeddings = new EmbeddingList<>("list",
List.of(new Embedding(0, new float[] { 9.9f, 8.8f })), "model", new MistralAiApi.Usage(10, 10, 10));
when(this.mistralAiApi.embeddings(isA(EmbeddingRequest.class)))
.thenThrow(new TransientAiException("Transient Error 1"))
.thenThrow(new TransientAiException("Transient Error 2"))
.thenReturn(ResponseEntity.of(Optional.of(expectedEmbeddings)));
given(this.mistralAiApi.embeddings(isA(EmbeddingRequest.class)))
.willThrow(new TransientAiException("Transient Error 1"))
.willThrow(new TransientAiException("Transient Error 2"))
.willReturn(ResponseEntity.of(Optional.of(expectedEmbeddings)));
var result = this.embeddingModel
.call(new org.springframework.ai.embedding.EmbeddingRequest(List.of("text1", "text2"), null));
@@ -169,8 +169,8 @@ public class MistralAiRetryTests {
@Test
public void mistralAiEmbeddingNonTransientError() {
when(this.mistralAiApi.embeddings(isA(EmbeddingRequest.class)))
.thenThrow(new RuntimeException("Non Transient Error"));
given(this.mistralAiApi.embeddings(isA(EmbeddingRequest.class)))
.willThrow(new RuntimeException("Non Transient Error"));
assertThrows(RuntimeException.class, () -> this.embeddingModel
.call(new org.springframework.ai.embedding.EmbeddingRequest(List.of("text1", "text2"), null)));
}

View File

@@ -25,7 +25,6 @@ import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.TypeReference;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.springframework.ai.aot.AiRuntimeHints.findJsonAnnotatedClassesInPackage;
import static org.springframework.aot.hint.predicate.RuntimeHintsPredicates.reflection;
class MistralAiRuntimeHintsTests {
@@ -36,7 +35,8 @@ class MistralAiRuntimeHintsTests {
MistralAiRuntimeHints mistralAiRuntimeHints = new MistralAiRuntimeHints();
mistralAiRuntimeHints.registerHints(runtimeHints, null);
Set<TypeReference> jsonAnnotatedClasses = findJsonAnnotatedClassesInPackage(MistralAiApi.class);
Set<TypeReference> jsonAnnotatedClasses = org.springframework.ai.aot.AiRuntimeHints
.findJsonAnnotatedClassesInPackage(MistralAiApi.class);
for (TypeReference jsonAnnotatedClass : jsonAnnotatedClasses) {
assertThat(runtimeHints).matches(reflection().onType(jsonAnnotatedClass));
}

View File

@@ -146,7 +146,7 @@ public class MistralAiApiToolFunctionCallIT {
ResponseEntity<ChatCompletion> chatCompletion2 = this.completionApi
.chatCompletionEntity(functionResponseRequest);
this.logger.info("Final response: " + chatCompletion2.getBody());
logger.info("Final response: " + chatCompletion2.getBody());
assertThat(chatCompletion2.getBody().choices()).isNotEmpty();

View File

@@ -136,7 +136,7 @@ public class PaymentStatusFunctionCallingIT {
.chatCompletionEntity(new ChatCompletionRequest(messages, MistralAiApi.ChatModel.LARGE.getValue()));
var responseContent = response.getBody().choices().get(0).message().content();
this.logger.info("Final response: " + responseContent);
logger.info("Final response: " + responseContent);
assertThat(responseContent).containsIgnoringCase("T1001");
assertThat(responseContent).containsIgnoringCase("Paid");

View File

@@ -36,6 +36,11 @@
<developerConnection>git@github.com:spring-projects/spring-ai.git</developerConnection>
</scm>
<properties>
<disable.checks>false</disable.checks>
</properties>
<dependencies>
<!-- production dependencies -->

View File

@@ -365,9 +365,8 @@ public class MoonshotChatModel extends AbstractToolCallSupport implements ChatMo
else if (message.getMessageType() == MessageType.TOOL) {
ToolResponseMessage toolMessage = (ToolResponseMessage) message;
toolMessage.getResponses().forEach(response -> {
Assert.isTrue(response.id() != null, "ToolResponseMessage must have an id");
});
toolMessage.getResponses()
.forEach(response -> Assert.isTrue(response.id() != null, "ToolResponseMessage must have an id"));
return toolMessage.getResponses()
.stream()

View File

@@ -36,8 +36,9 @@ public class MoonshotRuntimeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(@NonNull RuntimeHints hints, @Nullable ClassLoader classLoader) {
var mcs = MemberCategory.values();
for (var tr : findJsonAnnotatedClassesInPackage(MoonshotApi.class))
for (var tr : findJsonAnnotatedClassesInPackage(MoonshotApi.class)) {
hints.reflection().registerType(tr, mcs);
}
}
}

View File

@@ -39,8 +39,6 @@ import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.RestClient;
import org.springframework.web.reactive.function.client.WebClient;
import static org.springframework.ai.moonshot.api.MoonshotConstants.DEFAULT_BASE_URL;
/**
* Single-class, Java Client library for Moonshot platform. Provides implementation for
* the <a href="https://platform.moonshot.cn/docs/api-reference">Chat Completion</a> APIs.
@@ -68,7 +66,7 @@ public class MoonshotApi {
* @param moonshotApiKey Moonshot api Key.
*/
public MoonshotApi(String moonshotApiKey) {
this(DEFAULT_BASE_URL, moonshotApiKey);
this(org.springframework.ai.moonshot.api.MoonshotConstants.DEFAULT_BASE_URL, moonshotApiKey);
}
/**
@@ -222,9 +220,9 @@ public class MoonshotApi {
public enum ChatModel implements ChatModelDescription {
// @formatter:off
MOONSHOT_V1_8K("moonshot-v1-8k"),
MOONSHOT_V1_32K("moonshot-v1-32k"),
MOONSHOT_V1_128K("moonshot-v1-128k");
MOONSHOT_V1_8K("moonshot-v1-8k"),
MOONSHOT_V1_32K("moonshot-v1-32k"),
MOONSHOT_V1_128K("moonshot-v1-128k");
// @formatter:on
private final String value;
@@ -256,10 +254,10 @@ public class MoonshotApi {
@JsonInclude(Include.NON_NULL)
public record Usage(
// @formatter:off
@JsonProperty("prompt_tokens") Integer promptTokens,
@JsonProperty("total_tokens") Integer totalTokens,
@JsonProperty("completion_tokens") Integer completionTokens) {
// @formatter:on
@JsonProperty("prompt_tokens") Integer promptTokens,
@JsonProperty("total_tokens") Integer totalTokens,
@JsonProperty("completion_tokens") Integer completionTokens) {
// @formatter:on
}
/**
@@ -295,16 +293,16 @@ public class MoonshotApi {
@JsonInclude(Include.NON_NULL)
public record ChatCompletionRequest(
// @formatter:off
@JsonProperty("messages") List<ChatCompletionMessage> messages,
@JsonProperty("model") String model,
@JsonProperty("max_tokens") Integer maxTokens,
@JsonProperty("temperature") Double temperature,
@JsonProperty("top_p") Double topP,
@JsonProperty("n") Integer n,
@JsonProperty("frequency_penalty") Double frequencyPenalty,
@JsonProperty("presence_penalty") Double presencePenalty,
@JsonProperty("stop") List<String> stop,
@JsonProperty("stream") Boolean stream,
@JsonProperty("messages") List<ChatCompletionMessage> messages,
@JsonProperty("model") String model,
@JsonProperty("max_tokens") Integer maxTokens,
@JsonProperty("temperature") Double temperature,
@JsonProperty("top_p") Double topP,
@JsonProperty("n") Integer n,
@JsonProperty("frequency_penalty") Double frequencyPenalty,
@JsonProperty("presence_penalty") Double presencePenalty,
@JsonProperty("stop") List<String> stop,
@JsonProperty("stream") Boolean stream,
@JsonProperty("tools") List<FunctionTool> tools,
@JsonProperty("tool_choice") Object toolChoice) {
// @formatter:on
@@ -516,12 +514,12 @@ public class MoonshotApi {
@JsonInclude(Include.NON_NULL)
public record ChatCompletion(
// @formatter:off
@JsonProperty("id") String id,
@JsonProperty("object") String object,
@JsonProperty("created") Long created,
@JsonProperty("model") String model,
@JsonProperty("choices") List<Choice> choices,
@JsonProperty("usage") Usage usage) {
@JsonProperty("id") String id,
@JsonProperty("object") String object,
@JsonProperty("created") Long created,
@JsonProperty("model") String model,
@JsonProperty("choices") List<Choice> choices,
@JsonProperty("usage") Usage usage) {
// @formatter:on
/**
@@ -534,9 +532,9 @@ public class MoonshotApi {
@JsonInclude(Include.NON_NULL)
public record Choice(
// @formatter:off
@JsonProperty("index") Integer index,
@JsonProperty("message") ChatCompletionMessage message,
@JsonProperty("finish_reason") ChatCompletionFinishReason finishReason) {
@JsonProperty("index") Integer index,
@JsonProperty("message") ChatCompletionMessage message,
@JsonProperty("finish_reason") ChatCompletionFinishReason finishReason) {
// @formatter:on
}
@@ -557,11 +555,11 @@ public class MoonshotApi {
@JsonInclude(Include.NON_NULL)
public record ChatCompletionChunk(
// @formatter:off
@JsonProperty("id") String id,
@JsonProperty("object") String object,
@JsonProperty("created") Long created,
@JsonProperty("model") String model,
@JsonProperty("choices") List<ChunkChoice> choices) {
@JsonProperty("id") String id,
@JsonProperty("object") String object,
@JsonProperty("created") Long created,
@JsonProperty("model") String model,
@JsonProperty("choices") List<ChunkChoice> choices) {
// @formatter:on
/**

View File

@@ -27,4 +27,8 @@ public final class MoonshotConstants {
public static final String PROVIDER_NAME = AiProvider.MOONSHOT.value();
private MoonshotConstants() {
}
}

View File

@@ -45,7 +45,7 @@ import org.springframework.retry.support.RetryTemplate;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.Mockito.when;
import static org.mockito.BDDMockito.given;
/**
* @author Geng Rong
@@ -80,13 +80,13 @@ public class MoonshotRetryTests {
var choice = new ChatCompletion.Choice(0, new ChatCompletionMessage("Response", Role.ASSISTANT),
ChatCompletionFinishReason.STOP);
ChatCompletion expectedChatCompletion = new ChatCompletion("id", "chat.completion", 789l, "model",
ChatCompletion expectedChatCompletion = new ChatCompletion("id", "chat.completion", 789L, "model",
List.of(choice), new MoonshotApi.Usage(10, 10, 10));
when(this.moonshotApi.chatCompletionEntity(isA(ChatCompletionRequest.class)))
.thenThrow(new TransientAiException("Transient Error 1"))
.thenThrow(new TransientAiException("Transient Error 2"))
.thenReturn(ResponseEntity.of(Optional.of(expectedChatCompletion)));
given(this.moonshotApi.chatCompletionEntity(isA(ChatCompletionRequest.class)))
.willThrow(new TransientAiException("Transient Error 1"))
.willThrow(new TransientAiException("Transient Error 2"))
.willReturn(ResponseEntity.of(Optional.of(expectedChatCompletion)));
var result = this.chatModel.call(new Prompt("text"));
@@ -98,8 +98,8 @@ public class MoonshotRetryTests {
@Test
public void moonshotChatNonTransientError() {
when(this.moonshotApi.chatCompletionEntity(isA(ChatCompletionRequest.class)))
.thenThrow(new RuntimeException("Non Transient Error"));
given(this.moonshotApi.chatCompletionEntity(isA(ChatCompletionRequest.class)))
.willThrow(new RuntimeException("Non Transient Error"));
assertThrows(RuntimeException.class, () -> this.chatModel.call(new Prompt("text")));
}
@@ -108,13 +108,13 @@ public class MoonshotRetryTests {
var choice = new ChatCompletionChunk.ChunkChoice(0, new ChatCompletionMessage("Response", Role.ASSISTANT),
ChatCompletionFinishReason.STOP, null);
ChatCompletionChunk expectedChatCompletion = new ChatCompletionChunk("id", "chat.completion.chunk", 789l,
ChatCompletionChunk expectedChatCompletion = new ChatCompletionChunk("id", "chat.completion.chunk", 789L,
"model", List.of(choice));
when(this.moonshotApi.chatCompletionStream(isA(ChatCompletionRequest.class)))
.thenThrow(new TransientAiException("Transient Error 1"))
.thenThrow(new TransientAiException("Transient Error 2"))
.thenReturn(Flux.just(expectedChatCompletion));
given(this.moonshotApi.chatCompletionStream(isA(ChatCompletionRequest.class)))
.willThrow(new TransientAiException("Transient Error 1"))
.willThrow(new TransientAiException("Transient Error 2"))
.willReturn(Flux.just(expectedChatCompletion));
var result = this.chatModel.stream(new Prompt("text"));
@@ -126,8 +126,8 @@ public class MoonshotRetryTests {
@Test
public void moonshotChatStreamNonTransientError() {
when(this.moonshotApi.chatCompletionStream(isA(ChatCompletionRequest.class)))
.thenThrow(new RuntimeException("Non Transient Error"));
given(this.moonshotApi.chatCompletionStream(isA(ChatCompletionRequest.class)))
.willThrow(new RuntimeException("Non Transient Error"));
assertThrows(RuntimeException.class, () -> this.chatModel.stream(new Prompt("text")).collectList().block());
}

View File

@@ -65,7 +65,7 @@ public class MockWeatherService implements Function<MockWeatherService.Request,
*/
public final String unitName;
private Unit(String text) {
Unit(String text) {
this.unitName = text;
}
@@ -78,8 +78,8 @@ public class MockWeatherService implements Function<MockWeatherService.Request,
@JsonClassDescription("Weather API request")
public record Request(@JsonProperty(required = true,
value = "location") @JsonPropertyDescription("The city and state e.g. San Francisco, CA") String location,
@JsonProperty(value = "lat") @JsonPropertyDescription("The city latitude") double lat,
@JsonProperty(value = "lon") @JsonPropertyDescription("The city longitude") double lon,
@JsonProperty("lat") @JsonPropertyDescription("The city latitude") double lat,
@JsonProperty("lon") @JsonPropertyDescription("The city longitude") double lon,
@JsonProperty(required = true, value = "unit") @JsonPropertyDescription("Temperature unit") Unit unit) {
}

View File

@@ -139,7 +139,7 @@ public class MoonshotApiToolFunctionCallIT {
ResponseEntity<ChatCompletion> chatCompletion2 = this.moonshotApi.chatCompletionEntity(functionResponseRequest);
this.logger.info("Final response: " + chatCompletion2.getBody());
logger.info("Final response: " + chatCompletion2.getBody());
assertThat(Objects.requireNonNull(chatCompletion2.getBody()).choices()).isNotEmpty();

View File

@@ -66,7 +66,7 @@ class MoonshotChatModelFunctionCallingIT {
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("getCurrentWeather")
.withDescription("Get the weather in location")
.withResponseConverter((response) -> "" + response.temp() + response.unit())
.withResponseConverter(response -> "" + response.temp() + response.unit())
.build()))
.build();
@@ -90,7 +90,7 @@ class MoonshotChatModelFunctionCallingIT {
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("getCurrentWeather")
.withDescription("Get the weather in location")
.withResponseConverter((response) -> "" + response.temp() + response.unit())
.withResponseConverter(response -> "" + response.temp() + response.unit())
.build()))
.build();

View File

@@ -36,6 +36,10 @@
<developerConnection>git@github.com:spring-projects/spring-ai.git</developerConnection>
</scm>
<properties>
<disable.checks>false</disable.checks>
</properties>
<dependencies>
<!-- production dependencies -->

View File

@@ -43,7 +43,7 @@ public class BaseEmbeddingModelTest {
* Create an OCIEmbeddingModel instance using a config file authentication provider.
* @return OCIEmbeddingModel instance
*/
public static OCIEmbeddingModel get() {
public OCIEmbeddingModel get() {
try {
ConfigFileAuthenticationDetailsProvider authProvider = new ConfigFileAuthenticationDetailsProvider(
CONFIG_FILE, PROFILE);

View File

@@ -30,10 +30,11 @@
<name>Spring AI Model - Ollama</name>
<description>Ollama models support</description>
<properties>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<disable.checks>true</disable.checks>
</properties>
<dependencies>

View File

@@ -236,9 +236,9 @@ public class OllamaChatModel extends AbstractToolCallSupport implements ChatMode
}
})
.doOnError(observation::error)
.doFinally(s -> {
observation.stop();
})
.doFinally(s ->
observation.stop()
)
.contextWrite(ctx -> ctx.put(ObservationThreadLocalAccessor.KEY, observation));
// @formatter:on
@@ -392,7 +392,7 @@ public class OllamaChatModel extends AbstractToolCallSupport implements ChatMode
this.observationConvention = observationConvention;
}
public static class Builder {
public static final class Builder {
private OllamaApi ollamaApi;

View File

@@ -211,7 +211,7 @@ public class OllamaEmbeddingModel extends AbstractEmbeddingModel {
}
public static class Builder {
public static final class Builder {
private OllamaApi ollamaApi;

View File

@@ -37,10 +37,12 @@ public class OllamaRuntimeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
var mcs = MemberCategory.values();
for (var tr : findJsonAnnotatedClassesInPackage(OllamaApi.class))
for (var tr : findJsonAnnotatedClassesInPackage(OllamaApi.class)) {
hints.reflection().registerType(tr, mcs);
for (var tr : findJsonAnnotatedClassesInPackage(OllamaOptions.class))
}
for (var tr : findJsonAnnotatedClassesInPackage(OllamaOptions.class)) {
hints.reflection().registerType(tr, mcs);
}
}
}

View File

@@ -113,7 +113,7 @@ public class OllamaApi {
@Deprecated(since = "1.0.0-M2", forRemoval = true)
public GenerateResponse generate(GenerateRequest completionRequest) {
Assert.notNull(completionRequest, REQUEST_BODY_NULL_ERROR);
Assert.isTrue(completionRequest.stream() == false, "Stream mode must be disabled.");
Assert.isTrue(!completionRequest.stream(), "Stream mode must be disabled.");
return this.restClient.post()
.uri("/api/generate")
@@ -534,19 +534,23 @@ public class OllamaApi {
/**
* System message type used as instructions to the model.
*/
@JsonProperty("system") SYSTEM,
@JsonProperty("system")
SYSTEM,
/**
* User message type.
*/
@JsonProperty("user") USER,
@JsonProperty("user")
USER,
/**
* Assistant message type. Usually the response from the model.
*/
@JsonProperty("assistant") ASSISTANT,
@JsonProperty("assistant")
ASSISTANT,
/**
* Tool message.
*/
@JsonProperty("tool") TOOL
@JsonProperty("tool")
TOOL
}
@@ -664,7 +668,8 @@ public class OllamaApi {
/**
* Function tool type.
*/
@JsonProperty("function") FUNCTION
@JsonProperty("function")
FUNCTION
}
/**
@@ -897,13 +902,13 @@ public class OllamaApi {
@JsonProperty("families") List<String> families,
@JsonProperty("parameter_size") String parameterSize,
@JsonProperty("quantization_level") String quantizationLevel
) {}
) { }
}
@JsonInclude(Include.NON_NULL)
public record ListModelResponse(
@JsonProperty("models") List<Model> models
) {}
) { }
@JsonInclude(Include.NON_NULL)
public record ShowModelRequest(
@@ -929,18 +934,18 @@ public class OllamaApi {
@JsonProperty("model_info") Map<String, Object> modelInfo,
@JsonProperty("projector_info") Map<String, Object> projectorInfo,
@JsonProperty("modified_at") Instant modifiedAt
) {}
) { }
@JsonInclude(Include.NON_NULL)
public record CopyModelRequest(
@JsonProperty("source") String source,
@JsonProperty("destination") String destination
) {}
) { }
@JsonInclude(Include.NON_NULL)
public record DeleteModelRequest(
@JsonProperty("model") String model
) {}
) { }
@JsonInclude(Include.NON_NULL)
public record PullModelRequest(
@@ -968,7 +973,7 @@ public class OllamaApi {
@JsonProperty("digest") String digest,
@JsonProperty("total") Long total,
@JsonProperty("completed") Long completed
) {}
) { }
}
// @formatter:on
// @formatter:on

View File

@@ -61,24 +61,28 @@ public class OllamaOptions implements FunctionCallingOptions, ChatOptions, Embed
/**
* Whether to use NUMA. (Default: false)
*/
@JsonProperty("numa") private Boolean useNUMA;
@JsonProperty("numa")
private Boolean useNUMA;
/**
* Sets the size of the context window used to generate the next token. (Default: 2048)
*/
@JsonProperty("num_ctx") private Integer numCtx;
@JsonProperty("num_ctx")
private Integer numCtx;
/**
* Prompt processing maximum batch size. (Default: 512)
*/
@JsonProperty("num_batch") private Integer numBatch;
@JsonProperty("num_batch")
private Integer numBatch;
/**
* The number of layers to send to the GPU(s). On macOS, it defaults to 1
* to enable metal support, 0 to disable.
* (Default: -1, which indicates that numGPU should be set dynamically)
*/
@JsonProperty("num_gpu") private Integer numGPU;
@JsonProperty("num_gpu")
private Integer numGPU;
/**
* When using multiple GPUs this option controls which GPU is used
@@ -87,28 +91,33 @@ public class OllamaOptions implements FunctionCallingOptions, ChatOptions, Embed
* more VRAM to store a scratch buffer for temporary results.
* By default, GPU 0 is used.
*/
@JsonProperty("main_gpu")private Integer mainGPU;
@JsonProperty("main_gpu")
private Integer mainGPU;
/**
* (Default: false)
*/
@JsonProperty("low_vram") private Boolean lowVRAM;
@JsonProperty("low_vram")
private Boolean lowVRAM;
/**
* (Default: true)
*/
@JsonProperty("f16_kv") private Boolean f16KV;
@JsonProperty("f16_kv")
private Boolean f16KV;
/**
* Return logits for all the tokens, not just the last one.
* To enable completions to return logprobs, this must be true.
*/
@JsonProperty("logits_all") private Boolean logitsAll;
@JsonProperty("logits_all")
private Boolean logitsAll;
/**
* Load only the vocabulary, not the weights.
*/
@JsonProperty("vocab_only") private Boolean vocabOnly;
@JsonProperty("vocab_only")
private Boolean vocabOnly;
/**
* By default, models are mapped into memory, which allows the system to load only the necessary parts
@@ -119,7 +128,8 @@ public class OllamaOptions implements FunctionCallingOptions, ChatOptions, Embed
* the model from loading at all.
* (Default: null)
*/
@JsonProperty("use_mmap") private Boolean useMMap;
@JsonProperty("use_mmap")
private Boolean useMMap;
/**
* Lock the model in memory, preventing it from being swapped out when memory-mapped.
@@ -127,7 +137,8 @@ public class OllamaOptions implements FunctionCallingOptions, ChatOptions, Embed
* by requiring more RAM to run and potentially slowing down load times as the model loads into RAM.
* (Default: false)
*/
@JsonProperty("use_mlock") private Boolean useMLock;
@JsonProperty("use_mlock")
private Boolean useMLock;
/**
* Set the number of threads to use during generation. For optimal performance, it is recommended to set this value
@@ -135,113 +146,131 @@ public class OllamaOptions implements FunctionCallingOptions, ChatOptions, Embed
* Using the correct number of threads can greatly improve performance.
* By default, Ollama will detect this value for optimal performance.
*/
@JsonProperty("num_thread") private Integer numThread;
@JsonProperty("num_thread")
private Integer numThread;
// Following fields are predict options used at runtime.
/**
* (Default: 4)
*/
@JsonProperty("num_keep") private Integer numKeep;
@JsonProperty("num_keep")
private Integer numKeep;
/**
* Sets the random number seed to use for generation. Setting this to a
* specific number will make the model generate the same text for the same prompt.
* (Default: -1)
*/
@JsonProperty("seed") private Integer seed;
@JsonProperty("seed")
private Integer seed;
/**
* Maximum number of tokens to predict when generating text.
* (Default: 128, -1 = infinite generation, -2 = fill context)
*/
@JsonProperty("num_predict") private Integer numPredict;
@JsonProperty("num_predict")
private Integer numPredict;
/**
* Reduces the probability of generating nonsense. A higher value (e.g.
* 100) will give more diverse answers, while a lower value (e.g. 10) will be more
* conservative. (Default: 40)
*/
@JsonProperty("top_k") private Integer topK;
@JsonProperty("top_k")
private Integer topK;
/**
* Works together with top-k. A higher value (e.g., 0.95) will lead to
* more diverse text, while a lower value (e.g., 0.5) will generate more focused and
* conservative text. (Default: 0.9)
*/
@JsonProperty("top_p") private Double topP;
@JsonProperty("top_p")
private Double topP;
/**
* Tail free sampling is used to reduce the impact of less probable tokens
* from the output. A higher value (e.g., 2.0) will reduce the impact more, while a
* value of 1.0 disables this setting. (default: 1)
*/
@JsonProperty("tfs_z") private Float tfsZ;
@JsonProperty("tfs_z")
private Float tfsZ;
/**
* (Default: 1.0)
*/
@JsonProperty("typical_p") private Float typicalP;
@JsonProperty("typical_p")
private Float typicalP;
/**
* Sets how far back for the model to look back to prevent
* repetition. (Default: 64, 0 = disabled, -1 = num_ctx)
*/
@JsonProperty("repeat_last_n") private Integer repeatLastN;
@JsonProperty("repeat_last_n")
private Integer repeatLastN;
/**
* The temperature of the model. Increasing the temperature will
* make the model answer more creatively. (Default: 0.8)
*/
@JsonProperty("temperature") private Double temperature;
@JsonProperty("temperature")
private Double temperature;
/**
* Sets how strongly to penalize repetitions. A higher value
* (e.g., 1.5) will penalize repetitions more strongly, while a lower value (e.g.,
* 0.9) will be more lenient. (Default: 1.1)
*/
@JsonProperty("repeat_penalty") private Double repeatPenalty;
@JsonProperty("repeat_penalty")
private Double repeatPenalty;
/**
* (Default: 0.0)
*/
@JsonProperty("presence_penalty") private Double presencePenalty;
@JsonProperty("presence_penalty")
private Double presencePenalty;
/**
* (Default: 0.0)
*/
@JsonProperty("frequency_penalty") private Double frequencyPenalty;
@JsonProperty("frequency_penalty")
private Double frequencyPenalty;
/**
* Enable Mirostat sampling for controlling perplexity. (default: 0, 0
* = disabled, 1 = Mirostat, 2 = Mirostat 2.0)
*/
@JsonProperty("mirostat") private Integer mirostat;
@JsonProperty("mirostat")
private Integer mirostat;
/**
* Controls the balance between coherence and diversity of the output.
* A lower value will result in more focused and coherent text. (Default: 5.0)
*/
@JsonProperty("mirostat_tau") private Float mirostatTau;
@JsonProperty("mirostat_tau")
private Float mirostatTau;
/**
* Influences how quickly the algorithm responds to feedback from the generated text.
* A lower learning rate will result in slower adjustments, while a higher learning rate
* will make the algorithm more responsive. (Default: 0.1)
*/
@JsonProperty("mirostat_eta") private Float mirostatEta;
@JsonProperty("mirostat_eta")
private Float mirostatEta;
/**
* (Default: true)
*/
@JsonProperty("penalize_newline") private Boolean penalizeNewline;
@JsonProperty("penalize_newline")
private Boolean penalizeNewline;
/**
* Sets the stop sequences to use. When this pattern is encountered the
* LLM will stop generating text and return. Multiple stop patterns may be set by
* specifying multiple separate stop parameters in a modelfile.
*/
@JsonProperty("stop") private List<String> stop;
@JsonProperty("stop")
private List<String> stop;
// Following fields are not part of the Ollama Options API but part of the Request.
@@ -251,27 +280,30 @@ public class OllamaOptions implements FunctionCallingOptions, ChatOptions, Embed
* Used to allow overriding the model name with prompt options.
* Part of Chat completion <a href="https://github.com/ollama/ollama/blob/main/docs/api.md#parameters-1">parameters</a>.
*/
@JsonProperty("model") private String model;
@JsonProperty("model")
private String model;
/**
* Sets the desired format of output from the LLM. The only valid values are null or "json".
* Part of Chat completion <a href="https://github.com/ollama/ollama/blob/main/docs/api.md#parameters-1">advanced parameters</a>.
*/
@JsonProperty("format") private String format;
@JsonProperty("format")
private String format;
/**
* Sets the length of time for Ollama to keep the model loaded. Valid values for this
* setting are parsed by <a href="https://pkg.go.dev/time#ParseDuration">ParseDuration in Go</a>.
* Part of Chat completion <a href="https://github.com/ollama/ollama/blob/main/docs/api.md#parameters-1">advanced parameters</a>.
*/
@JsonProperty("keep_alive") private String keepAlive;
@JsonProperty("keep_alive")
private String keepAlive;
/**
* Truncates the end of each input to fit within context length. Returns error if false and context length is exceeded.
* Defaults to true.
*/
@JsonProperty("truncate") private Boolean truncate;
@JsonProperty("truncate")
private Boolean truncate;
/**
* Tool Function Callbacks to register with the ChatModel.
@@ -310,7 +342,7 @@ public class OllamaOptions implements FunctionCallingOptions, ChatOptions, Embed
public static OllamaOptions create() {
return new OllamaOptions();
}
/**
* Filter out the non-supported fields from the options.
* @param options The options to filter.
@@ -714,8 +746,8 @@ public class OllamaOptions implements FunctionCallingOptions, ChatOptions, Embed
@Override
@JsonIgnore
public Integer getMaxTokens() {
return getNumPredict();
}
return getNumPredict();
}
@JsonIgnore
public void setMaxTokens(Integer maxTokens) {

View File

@@ -81,13 +81,13 @@ public class OllamaModelManager {
}
public void deleteModel(String modelName) {
this.logger.info("Start deletion of model: {}", modelName);
logger.info("Start deletion of model: {}", modelName);
if (!isModelAvailable(modelName)) {
this.logger.info("Model {} not found", modelName);
logger.info("Model {} not found", modelName);
return;
}
this.ollamaApi.deleteModel(new DeleteModelRequest(modelName));
this.logger.info("Completed deletion of model: {}", modelName);
logger.info("Completed deletion of model: {}", modelName);
}
public void pullModel(String modelName) {
@@ -101,27 +101,27 @@ public class OllamaModelManager {
if (PullModelStrategy.WHEN_MISSING.equals(pullModelStrategy)) {
if (isModelAvailable(modelName)) {
this.logger.debug("Model '{}' already available. Skipping pull operation.", modelName);
logger.debug("Model '{}' already available. Skipping pull operation.", modelName);
return;
}
}
// @formatter:off
this.logger.info("Start pulling model: {}", modelName);
logger.info("Start pulling model: {}", modelName);
this.ollamaApi.pullModel(new PullModelRequest(modelName))
.bufferUntilChanged(OllamaApi.ProgressResponse::status)
.doOnEach(signal -> {
var progressResponses = signal.get();
if (!CollectionUtils.isEmpty(progressResponses) && progressResponses.get(progressResponses.size() - 1) != null) {
this.logger.info("Pulling the '{}' model - Status: {}", modelName, progressResponses.get(progressResponses.size() - 1).status());
logger.info("Pulling the '{}' model - Status: {}", modelName, progressResponses.get(progressResponses.size() - 1).status());
}
})
.takeUntil(progressResponses -> progressResponses.get(0) != null && progressResponses.get(0).status().equals("success"))
.timeout(this.options.timeout())
.retryWhen(Retry.backoff(this.options.maxRetries(), Duration.ofSeconds(5)))
.blockLast();
this.logger.info("Completed pulling the '{}' model", modelName);
logger.info("Completed pulling the '{}' model", modelName);
// @formatter:on
}

View File

@@ -39,6 +39,6 @@ public enum PullModelStrategy {
/**
* Never pull the model.
*/
NEVER;
NEVER
}

View File

@@ -1,3 +1,19 @@
/*
* Copyright 2023-2024 the original author or authors.
*
* 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
*
* https://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 org.springframework.ai.ollama;
import java.time.Duration;

View File

@@ -71,7 +71,7 @@ class OllamaChatModelFunctionCallingIT extends BaseOllamaIT {
.withName("getCurrentWeather")
.withDescription(
"Find the weather conditions, forecasts, and temperatures for a location, like a city or state.")
.withResponseConverter((response) -> "" + response.temp() + response.unit())
.withResponseConverter(response -> "" + response.temp() + response.unit())
.build()))
.build();
@@ -96,7 +96,7 @@ class OllamaChatModelFunctionCallingIT extends BaseOllamaIT {
.withName("getCurrentWeather")
.withDescription(
"Find the weather conditions, forecasts, and temperatures for a location, like a city or state.")
.withResponseConverter((response) -> "" + response.temp() + response.unit())
.withResponseConverter(response -> "" + response.temp() + response.unit())
.build()))
.build();

View File

@@ -37,7 +37,7 @@ import org.springframework.core.io.ClassPathResource;
import org.springframework.util.MimeTypeUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertThrows;
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
@SpringBootTest
@Testcontainers
@@ -58,7 +58,8 @@ class OllamaChatModelMultimodalIT extends BaseOllamaIT {
var userMessage = new UserMessage("Explain what do you see in this picture?",
List.of(new Media(MimeTypeUtils.IMAGE_PNG, imageData)));
assertThrows(RuntimeException.class, () -> this.chatModel.call(new Prompt(List.of(userMessage))));
assertThatThrownBy(() -> this.chatModel.call(new Prompt(List.of(userMessage))))
.isInstanceOf(RuntimeException.class);
}
@Test

View File

@@ -37,7 +37,7 @@ import org.springframework.ai.ollama.api.OllamaApi.EmbeddingsResponse;
import org.springframework.ai.ollama.api.OllamaOptions;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
import static org.mockito.BDDMockito.given;
/**
* @author Christian Tzolov
@@ -56,10 +56,10 @@ public class OllamaEmbeddingModelTests {
@Test
public void options() {
when(this.ollamaApi.embed(this.embeddingsRequestCaptor.capture()))
.thenReturn(new EmbeddingsResponse("RESPONSE_MODEL_NAME",
given(this.ollamaApi.embed(this.embeddingsRequestCaptor.capture()))
.willReturn(new EmbeddingsResponse("RESPONSE_MODEL_NAME",
List.of(new float[] { 1f, 2f, 3f }, new float[] { 4f, 5f, 6f }), 0L, 0L, 0))
.thenReturn(new EmbeddingsResponse("RESPONSE_MODEL_NAME2",
.willReturn(new EmbeddingsResponse("RESPONSE_MODEL_NAME2",
List.of(new float[] { 7f, 8f, 9f }, new float[] { 10f, 11f, 12f }), 0L, 0L, 0));
// Tests default options

View File

@@ -21,8 +21,12 @@ import org.testcontainers.utility.DockerImageName;
/**
* @author Thomas Vitale
*/
public class OllamaImage {
public final class OllamaImage {
public static final DockerImageName DEFAULT_IMAGE = DockerImageName.parse("ollama/ollama:0.3.14");
private OllamaImage() {
}
}

Some files were not shown because too many files have changed in this diff Show More