Addressing more checkstyle violations
- Enable checkstyle on more modules and adressing violations review
This commit is contained in:
committed by
Mark Pollack
parent
cdc1cecb57
commit
e72ab6ba25
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.");
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/*
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -223,7 +223,7 @@ public class MockAiTestConfiguration {
|
||||
}
|
||||
|
||||
protected Logger getLogger() {
|
||||
return this.logger;
|
||||
return logger;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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:";
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)");
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -57,4 +57,4 @@ class BedrockRuntimeHintsTests {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
){}
|
||||
) { }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -32,4 +32,8 @@ public final class MiniMaxApiConstants {
|
||||
|
||||
public static final String PROVIDER_NAME = AiProvider.MINIMAX.value();
|
||||
|
||||
private MiniMaxApiConstants() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
/**
|
||||
|
||||
@@ -27,4 +27,8 @@ public final class MoonshotConstants {
|
||||
|
||||
public static final String PROVIDER_NAME = AiProvider.MOONSHOT.value();
|
||||
|
||||
private MoonshotConstants() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -211,7 +211,7 @@ public class OllamaEmbeddingModel extends AbstractEmbeddingModel {
|
||||
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
public static final class Builder {
|
||||
|
||||
private OllamaApi ollamaApi;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -39,6 +39,6 @@ public enum PullModelStrategy {
|
||||
/**
|
||||
* Never pull the model.
|
||||
*/
|
||||
NEVER;
|
||||
NEVER
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user