Introduce checkstyle plugin

- Based on https://github.com/spring-io/spring-javaformat
- In this iteration, checkstyles are only enabled for spring-ai-core
This commit is contained in:
Soby Chacko
2024-10-24 10:39:48 -04:00
committed by Mark Pollack
parent 33a72417e1
commit 8e758dbd00
1412 changed files with 26997 additions and 21963 deletions

View File

@@ -1,5 +1,21 @@
#!/bin/bash
#
# 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.
#
set -x
az extension add --name spring

View File

@@ -8,3 +8,5 @@ indent_style = tab
indent_size = 4
continuation_indent_size = 8
end_of_line = lf
insert_final_newline = true

View File

@@ -1,4 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ 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.
-->
<extensions>
<extension>
<groupId>fr.jcgay.maven</groupId>

View File

@@ -1,18 +1,17 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you 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
# 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.
#
# 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.
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar

View File

@@ -1,4 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ 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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

View File

@@ -1,18 +1,45 @@
package org.springframework.ai.reader.markdown;
/*
* 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.
*/
import org.commonmark.node.*;
import org.commonmark.parser.Parser;
import org.springframework.ai.document.Document;
import org.springframework.ai.document.DocumentReader;
import org.springframework.ai.reader.markdown.config.MarkdownDocumentReaderConfig;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
package org.springframework.ai.reader.markdown;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import org.commonmark.node.AbstractVisitor;
import org.commonmark.node.BlockQuote;
import org.commonmark.node.Code;
import org.commonmark.node.FencedCodeBlock;
import org.commonmark.node.HardLineBreak;
import org.commonmark.node.Heading;
import org.commonmark.node.ListItem;
import org.commonmark.node.Node;
import org.commonmark.node.SoftLineBreak;
import org.commonmark.node.Text;
import org.commonmark.node.ThematicBreak;
import org.commonmark.parser.Parser;
import org.springframework.ai.document.Document;
import org.springframework.ai.document.DocumentReader;
import org.springframework.ai.reader.markdown.config.MarkdownDocumentReaderConfig;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
/**
* Reads the given Markdown resource and groups headers, paragraphs, or text divided by
* horizontal lines (depending on the
@@ -58,10 +85,10 @@ public class MarkdownDocumentReader implements DocumentReader {
*/
@Override
public List<Document> get() {
try (var input = markdownResource.getInputStream()) {
Node node = parser.parseReader(new InputStreamReader(input));
try (var input = this.markdownResource.getInputStream()) {
Node node = this.parser.parseReader(new InputStreamReader(input));
DocumentVisitor documentVisitor = new DocumentVisitor(config);
DocumentVisitor documentVisitor = new DocumentVisitor(this.config);
node.accept(documentVisitor);
return documentVisitor.getDocuments();
@@ -90,7 +117,7 @@ public class MarkdownDocumentReader implements DocumentReader {
@Override
public void visit(org.commonmark.node.Document document) {
currentDocumentBuilder = Document.builder();
this.currentDocumentBuilder = Document.builder();
super.visit(document);
}
@@ -102,7 +129,7 @@ public class MarkdownDocumentReader implements DocumentReader {
@Override
public void visit(ThematicBreak thematicBreak) {
if (config.horizontalRuleCreateDocument) {
if (this.config.horizontalRuleCreateDocument) {
buildAndFlush();
}
super.visit(thematicBreak);
@@ -128,32 +155,32 @@ public class MarkdownDocumentReader implements DocumentReader {
@Override
public void visit(BlockQuote blockQuote) {
if (!config.includeBlockquote) {
if (!this.config.includeBlockquote) {
buildAndFlush();
}
translateLineBreakToSpace();
currentDocumentBuilder.withMetadata("category", "blockquote");
this.currentDocumentBuilder.withMetadata("category", "blockquote");
super.visit(blockQuote);
}
@Override
public void visit(Code code) {
currentParagraphs.add(code.getLiteral());
currentDocumentBuilder.withMetadata("category", "code_inline");
this.currentParagraphs.add(code.getLiteral());
this.currentDocumentBuilder.withMetadata("category", "code_inline");
super.visit(code);
}
@Override
public void visit(FencedCodeBlock fencedCodeBlock) {
if (!config.includeCodeBlock) {
if (!this.config.includeCodeBlock) {
buildAndFlush();
}
translateLineBreakToSpace();
currentParagraphs.add(fencedCodeBlock.getLiteral());
currentDocumentBuilder.withMetadata("category", "code_block");
currentDocumentBuilder.withMetadata("lang", fencedCodeBlock.getInfo());
this.currentParagraphs.add(fencedCodeBlock.getLiteral());
this.currentDocumentBuilder.withMetadata("category", "code_block");
this.currentDocumentBuilder.withMetadata("lang", fencedCodeBlock.getInfo());
buildAndFlush();
@@ -163,11 +190,11 @@ public class MarkdownDocumentReader implements DocumentReader {
@Override
public void visit(Text text) {
if (text.getParent() instanceof Heading heading) {
currentDocumentBuilder.withMetadata("category", "header_%d".formatted(heading.getLevel()))
this.currentDocumentBuilder.withMetadata("category", "header_%d".formatted(heading.getLevel()))
.withMetadata("title", text.getLiteral());
}
else {
currentParagraphs.add(text.getLiteral());
this.currentParagraphs.add(text.getLiteral());
}
super.visit(text);
@@ -176,29 +203,29 @@ public class MarkdownDocumentReader implements DocumentReader {
public List<Document> getDocuments() {
buildAndFlush();
return documents;
return this.documents;
}
private void buildAndFlush() {
if (!currentParagraphs.isEmpty()) {
String content = String.join("", currentParagraphs);
if (!this.currentParagraphs.isEmpty()) {
String content = String.join("", this.currentParagraphs);
Document.Builder builder = currentDocumentBuilder.withContent(content);
Document.Builder builder = this.currentDocumentBuilder.withContent(content);
config.additionalMetadata.forEach(builder::withMetadata);
this.config.additionalMetadata.forEach(builder::withMetadata);
Document document = builder.build();
documents.add(document);
this.documents.add(document);
currentParagraphs.clear();
this.currentParagraphs.clear();
}
currentDocumentBuilder = Document.builder();
this.currentDocumentBuilder = Document.builder();
}
private void translateLineBreakToSpace() {
if (!currentParagraphs.isEmpty()) {
currentParagraphs.add(" ");
if (!this.currentParagraphs.isEmpty()) {
this.currentParagraphs.add(" ");
}
}

View File

@@ -1,12 +1,28 @@
/*
* 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.markdown.config;
import java.util.HashMap;
import java.util.Map;
import org.springframework.ai.document.Document;
import org.springframework.ai.reader.markdown.MarkdownDocumentReader;
import org.springframework.util.Assert;
import java.util.HashMap;
import java.util.Map;
/**
* Common configuration for the {@link MarkdownDocumentReader}.
*
@@ -23,10 +39,10 @@ public class MarkdownDocumentReaderConfig {
public final Map<String, Object> additionalMetadata;
public MarkdownDocumentReaderConfig(Builder builder) {
horizontalRuleCreateDocument = builder.horizontalRuleCreateDocument;
includeCodeBlock = builder.includeCodeBlock;
includeBlockquote = builder.includeBlockquote;
additionalMetadata = builder.additionalMetadata;
this.horizontalRuleCreateDocument = builder.horizontalRuleCreateDocument;
this.includeCodeBlock = builder.includeCodeBlock;
this.includeBlockquote = builder.includeBlockquote;
this.additionalMetadata = builder.additionalMetadata;
}
/**

View File

@@ -1,12 +1,29 @@
package org.springframework.ai.reader.markdown;
/*
* 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.
*/
import org.junit.jupiter.api.Test;
import org.springframework.ai.document.Document;
import org.springframework.ai.reader.markdown.config.MarkdownDocumentReaderConfig;
package org.springframework.ai.reader.markdown;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.springframework.ai.document.Document;
import org.springframework.ai.reader.markdown.config.MarkdownDocumentReaderConfig;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.groups.Tuple.tuple;

View File

@@ -1,4 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ 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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,9 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.reader.pdf;
import java.awt.Rectangle;
import java.awt.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@@ -24,9 +25,9 @@ import java.util.stream.Collectors;
import org.apache.pdfbox.pdfparser.PDFParser;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.document.Document;
import org.springframework.ai.document.DocumentReader;
import org.springframework.ai.reader.pdf.config.PdfDocumentReaderConfig;
@@ -46,22 +47,22 @@ import org.springframework.util.StringUtils;
*/
public class PagePdfDocumentReader implements DocumentReader {
private final Logger logger = LoggerFactory.getLogger(getClass());
private static final String PDF_PAGE_REGION = "pdfPageRegion";
public static final String METADATA_START_PAGE_NUMBER = "page_number";
public static final String METADATA_END_PAGE_NUMBER = "end_page_number";
public static final String METADATA_FILE_NAME = "file_name";
private static final String PDF_PAGE_REGION = "pdfPageRegion";
protected final PDDocument document;
private PdfDocumentReaderConfig config;
private final Logger logger = LoggerFactory.getLogger(getClass());
protected String resourceFileName;
private PdfDocumentReaderConfig config;
public PagePdfDocumentReader(String resourceUrl) {
this(new DefaultResourceLoader().getResource(resourceUrl));
}
@@ -103,15 +104,15 @@ public class PagePdfDocumentReader implements DocumentReader {
int totalPages = this.document.getDocumentCatalog().getPages().getCount();
int logFrequency = totalPages > 10 ? totalPages / 10 : 1; // if less than 10
// pages, print
// each iteration
// pages, print
// each iteration
int counter = 0;
PDPage lastPage = this.document.getDocumentCatalog().getPages().iterator().next();
for (PDPage page : this.document.getDocumentCatalog().getPages()) {
lastPage = page;
if (counter % logFrequency == 0 && counter / logFrequency < 10) {
logger.info("Processing PDF page: {}", (counter + 1));
this.logger.info("Processing PDF page: {}", (counter + 1));
}
counter++;
@@ -153,7 +154,7 @@ public class PagePdfDocumentReader implements DocumentReader {
readDocuments.add(toDocument(lastPage, pageTextGroupList.stream().collect(Collectors.joining()),
startPageNumber, pageNumber));
}
logger.info("Processing {} pages", totalPages);
this.logger.info("Processing {} pages", totalPages);
return readDocuments;
}

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,18 +13,19 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.reader.pdf;
import java.awt.Rectangle;
import java.awt.*;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.apache.pdfbox.pdfparser.PDFParser;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.document.Document;
import org.springframework.ai.document.DocumentReader;
import org.springframework.ai.reader.pdf.config.ParagraphManager;
@@ -48,8 +49,6 @@ import org.springframework.util.StringUtils;
*/
public class ParagraphPdfDocumentReader implements DocumentReader {
private final Logger logger = LoggerFactory.getLogger(getClass());
// Constants for metadata keys
private static final String METADATA_START_PAGE = "page_number";
@@ -61,14 +60,16 @@ public class ParagraphPdfDocumentReader implements DocumentReader {
private static final String METADATA_FILE_NAME = "file_name";
private final ParagraphManager paragraphTextExtractor;
protected final PDDocument document;
private PdfDocumentReaderConfig config;
private final Logger logger = LoggerFactory.getLogger(getClass());
private final ParagraphManager paragraphTextExtractor;
protected String resourceFileName;
private PdfDocumentReaderConfig config;
/**
* Constructs a ParagraphPdfDocumentReader using a resource URL.
* @param resourceUrl The URL of the PDF resource.
@@ -132,7 +133,7 @@ public class ParagraphPdfDocumentReader implements DocumentReader {
List<Document> documents = new ArrayList<>(paragraphs.size());
if (!CollectionUtils.isEmpty(paragraphs)) {
logger.info("Start processing paragraphs from PDF");
this.logger.info("Start processing paragraphs from PDF");
Iterator<Paragraph> itr = paragraphs.iterator();
var current = itr.next();
@@ -151,7 +152,7 @@ public class ParagraphPdfDocumentReader implements DocumentReader {
}
}
}
logger.info("End processing paragraphs from PDF");
this.logger.info("End processing paragraphs from PDF");
return documents;
}

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,15 +13,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.reader.pdf.aot;
import java.io.IOException;
import java.util.Set;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import java.io.IOException;
import java.util.Set;
/**
* The PdfReaderRuntimeHints class is responsible for registering runtime hints for PDFBox
* resources.

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.reader.pdf.config;
import java.io.IOException;
@@ -39,34 +40,6 @@ import org.springframework.util.CollectionUtils;
*/
public class ParagraphManager {
/**
* Represents a document paragraph metadata and hierarchy.
*
* @param parent Parent paragraph that will contain a children paragraphs.
* @param title Paragraph title as it appears in the PDF document.
* @param level The TOC deepness level for this paragraph. The root is at level 0.
* @param startPageNumber The page number in the PDF where this paragraph begins.
* @param endPageNumber The page number in the PDF where this paragraph ends.
* @param children Sub-paragraphs for this paragraph.
*/
public record Paragraph(Paragraph parent, String title, int level, int startPageNumber, int endPageNumber,
int position, List<Paragraph> children) {
public Paragraph(Paragraph parent, String title, int level, int startPageNumber, int endPageNumber,
int position) {
this(parent, title, level, startPageNumber, endPageNumber, position, new ArrayList<>());
}
@Override
public String toString() {
String indent = (level < 0) ? "" : new String(new char[level * 2]).replace('\0', ' ');
return indent + " " + level + ") " + title + " [" + startPageNumber + "," + endPageNumber + "], children = "
+ children.size() + ", pos = " + position;
}
}
/**
* Root of the paragraphs tree.
*/
@@ -90,7 +63,7 @@ public class ParagraphManager {
new Paragraph(null, "root", -1, 1, this.document.getNumberOfPages(), 0),
this.document.getDocumentCatalog().getDocumentOutline(), 0);
printParagraph(rootParagraph, System.out);
printParagraph(this.rootParagraph, System.out);
}
catch (Exception e) {
throw new RuntimeException(e);
@@ -203,4 +176,32 @@ public class ParagraphManager {
return resultList;
}
/**
* Represents a document paragraph metadata and hierarchy.
*
* @param parent Parent paragraph that will contain a children paragraphs.
* @param title Paragraph title as it appears in the PDF document.
* @param level The TOC deepness level for this paragraph. The root is at level 0.
* @param startPageNumber The page number in the PDF where this paragraph begins.
* @param endPageNumber The page number in the PDF where this paragraph ends.
* @param children Sub-paragraphs for this paragraph.
*/
public record Paragraph(Paragraph parent, String title, int level, int startPageNumber, int endPageNumber,
int position, List<Paragraph> children) {
public Paragraph(Paragraph parent, String title, int level, int startPageNumber, int endPageNumber,
int position) {
this(parent, title, level, startPageNumber, endPageNumber, position, new ArrayList<>());
}
@Override
public String toString() {
String indent = (this.level < 0) ? "" : new String(new char[this.level * 2]).replace('\0', ' ');
return indent + " " + this.level + ") " + this.title + " [" + this.startPageNumber + ","
+ this.endPageNumber + "], children = " + this.children.size() + ", pos = " + this.position;
}
}
}

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.reader.pdf.config;
import org.springframework.ai.reader.ExtractedTextFormatter;
@@ -40,6 +41,14 @@ public class PdfDocumentReaderConfig {
public final ExtractedTextFormatter pageExtractedTextFormatter;
private PdfDocumentReaderConfig(PdfDocumentReaderConfig.Builder builder) {
this.pagesPerDocument = builder.pagesPerDocument;
this.pageBottomMargin = builder.pageBottomMargin;
this.pageTopMargin = builder.pageTopMargin;
this.pageExtractedTextFormatter = builder.pageExtractedTextFormatter;
this.reversedParagraphPosition = builder.reversedParagraphPosition;
}
/**
* Start building a new configuration.
* @return The entry point for creating a new configuration.
@@ -56,14 +65,6 @@ public class PdfDocumentReaderConfig {
return builder().build();
}
private PdfDocumentReaderConfig(PdfDocumentReaderConfig.Builder builder) {
this.pagesPerDocument = builder.pagesPerDocument;
this.pageBottomMargin = builder.pageBottomMargin;
this.pageTopMargin = builder.pageTopMargin;
this.pageExtractedTextFormatter = builder.pageExtractedTextFormatter;
this.reversedParagraphPosition = builder.reversedParagraphPosition;
}
public static class Builder {
private int pagesPerDocument = 1;

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -180,8 +180,9 @@ public class ForkPDFLayoutTextStripper extends PDFTextStripper {
double height = textPosition.getHeight();
int numberOfLines = (int) (Math.floor(textYPosition - previousTextYPosition) / height);
numberOfLines = Math.max(1, numberOfLines - 1); // exclude current new line
if (DEBUG)
if (DEBUG) {
System.out.println(height + " " + numberOfLines);
}
return numberOfLines;
}
else {
@@ -191,7 +192,7 @@ public class ForkPDFLayoutTextStripper extends PDFTextStripper {
private TextLine addNewLine() {
TextLine textLine = new TextLine(this.getCurrentPageWidth());
textLineList.add(textLine);
this.textLineList.add(textLine);
return textLine;
}
@@ -248,7 +249,7 @@ class TextLine {
}
public String getLine() {
return line;
return this.line;
}
private int computeIndexForCharacter(final Character character) {
@@ -313,7 +314,7 @@ class TextLine {
private void completeLineWithSpaces() {
for (int i = 0; i < this.getLineLength(); ++i) {
line += SPACE_CHARACTER;
this.line += SPACE_CHARACTER;
}
}
@@ -350,8 +351,9 @@ class Character {
this.isFirstCharacterOfAWord = isFirstCharacterOfAWord;
this.isCharacterAtTheBeginningOfNewLine = isCharacterAtTheBeginningOfNewLine;
this.isCharacterCloseToPreviousWord = isCharacterPartOfASentence;
if (ForkPDFLayoutTextStripper.DEBUG)
if (ForkPDFLayoutTextStripper.DEBUG) {
System.out.println(this.toString());
}
}
public char getCharacterValue() {
@@ -384,14 +386,14 @@ class Character {
public String toString() {
String toString = "";
toString += index;
toString += this.index;
toString += " ";
toString += characterValue;
toString += " isCharacterPartOfPreviousWord=" + isCharacterPartOfPreviousWord;
toString += " isFirstCharacterOfAWord=" + isFirstCharacterOfAWord;
toString += " isCharacterAtTheBeginningOfNewLine=" + isCharacterAtTheBeginningOfNewLine;
toString += " isCharacterPartOfASentence=" + isCharacterCloseToPreviousWord;
toString += " isCharacterCloseToPreviousWord=" + isCharacterCloseToPreviousWord;
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;
}
@@ -424,12 +426,12 @@ class CharacterFactory {
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, isCharacterPartOfPreviousWord, isFirstCharacterOfAWord,
isCharacterAtTheBeginningOfNewLine, isCharacterCloseToPreviousWord);
return new Character(character, index, this.isCharacterPartOfPreviousWord, this.isFirstCharacterOfAWord,
this.isCharacterAtTheBeginningOfNewLine, this.isCharacterCloseToPreviousWord);
}
private boolean isCharacterAtTheBeginningOfNewLine(final TextPosition textPosition) {
if (!firstCharacterOfLineFound) {
if (!this.firstCharacterOfLineFound) {
return true;
}
TextPosition previousTextPosition = this.getPreviousTextPosition();
@@ -438,18 +440,18 @@ class CharacterFactory {
}
private boolean isFirstCharacterOfAWord(final TextPosition textPosition) {
if (!firstCharacterOfLineFound) {
if (!this.firstCharacterOfLineFound) {
return true;
}
double numberOfSpaces = this.numberOfSpacesBetweenTwoCharacters(previousTextPosition, textPosition);
double numberOfSpaces = this.numberOfSpacesBetweenTwoCharacters(this.previousTextPosition, textPosition);
return (numberOfSpaces > 1) || this.isCharacterAtTheBeginningOfNewLine(textPosition);
}
private boolean isCharacterCloseToPreviousWord(final TextPosition textPosition) {
if (!firstCharacterOfLineFound) {
if (!this.firstCharacterOfLineFound) {
return false;
}
double numberOfSpaces = this.numberOfSpacesBetweenTwoCharacters(previousTextPosition, textPosition);
double numberOfSpaces = this.numberOfSpacesBetweenTwoCharacters(this.previousTextPosition, textPosition);
return (numberOfSpaces > 1 && numberOfSpaces <= ForkPDFLayoutTextStripper.OUTPUT_SPACE_CHARACTER_WIDTH_IN_PT);
}
@@ -485,4 +487,4 @@ class CharacterFactory {
this.previousTextPosition = previousTextPosition;
}
}
}

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.reader.pdf.layout;
import java.awt.geom.Rectangle2D;
@@ -70,8 +71,8 @@ public class PDFLayoutTextStripperByArea extends ForkPDFLayoutTextStripper {
* java coordinates (y == 0 is top), not PDF coordinates (y == 0 is bottom).
*/
public void addRegion(String regionName, Rectangle2D rect) {
regions.add(regionName);
regionArea.put(regionName, rect);
this.regions.add(regionName);
this.regionArea.put(regionName, rect);
}
/**
@@ -80,8 +81,8 @@ public class PDFLayoutTextStripperByArea extends ForkPDFLayoutTextStripper {
* @param regionName The name of the region to delete.
*/
public void removeRegion(String regionName) {
regions.remove(regionName);
regionArea.remove(regionName);
this.regions.remove(regionName);
this.regionArea.remove(regionName);
}
/**
@@ -89,7 +90,7 @@ public class PDFLayoutTextStripperByArea extends ForkPDFLayoutTextStripper {
* @return A list of java.lang.String objects to identify the region names.
*/
public List<String> getRegions() {
return regions;
return this.regions;
}
/**
@@ -98,7 +99,7 @@ public class PDFLayoutTextStripperByArea extends ForkPDFLayoutTextStripper {
* @return The text that was identified in that region.
*/
public String getTextForRegion(String regionName) {
StringWriter text = regionText.get(regionName);
StringWriter text = this.regionText.get(regionName);
return text.toString();
}
@@ -108,14 +109,14 @@ public class PDFLayoutTextStripperByArea extends ForkPDFLayoutTextStripper {
* @throws IOException If there is an error while extracting text.
*/
public void extractRegions(PDPage page) throws IOException {
for (String regionName : regions) {
for (String regionName : this.regions) {
setStartPage(getCurrentPageNo());
setEndPage(getCurrentPageNo());
// reset the stored text for the region so this class can be reused.
ArrayList<List<TextPosition>> regionCharactersByArticle = new ArrayList<List<TextPosition>>();
regionCharactersByArticle.add(new ArrayList<TextPosition>());
regionCharacterList.put(regionName, regionCharactersByArticle);
regionText.put(regionName, new StringWriter());
this.regionCharacterList.put(regionName, regionCharactersByArticle);
this.regionText.put(regionName, new StringWriter());
}
if (page.hasContents()) {
@@ -128,10 +129,10 @@ public class PDFLayoutTextStripperByArea extends ForkPDFLayoutTextStripper {
*/
@Override
protected void processTextPosition(TextPosition text) {
for (Map.Entry<String, Rectangle2D> regionAreaEntry : regionArea.entrySet()) {
for (Map.Entry<String, Rectangle2D> regionAreaEntry : this.regionArea.entrySet()) {
Rectangle2D rect = regionAreaEntry.getValue();
if (rect.contains(text.getX(), text.getY())) {
charactersByArticle = regionCharacterList.get(regionAreaEntry.getKey());
this.charactersByArticle = this.regionCharacterList.get(regionAreaEntry.getKey());
super.processTextPosition(text);
}
}
@@ -143,9 +144,9 @@ public class PDFLayoutTextStripperByArea extends ForkPDFLayoutTextStripper {
*/
@Override
protected void writePage() throws IOException {
for (String region : regionArea.keySet()) {
charactersByArticle = regionCharacterList.get(region);
output = regionText.get(region);
for (String region : this.regionArea.keySet()) {
this.charactersByArticle = this.regionCharacterList.get(region);
this.output = this.regionText.get(region);
super.writePage();
}
}

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.reader.pdf;
import java.util.List;

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.reader.pdf;
import org.junit.jupiter.api.Test;

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,10 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.reader.pdf.aot;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.aot.hint.RuntimeHints;
import static org.springframework.aot.hint.predicate.RuntimeHintsPredicates.resource;

View File

@@ -1,4 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ 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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.reader.tika;
import java.io.IOException;

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.reader.tika;
import org.junit.jupiter.params.ParameterizedTest;

View File

@@ -1,4 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ 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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.anthropic;
import java.util.ArrayList;
@@ -28,6 +29,9 @@ import io.micrometer.observation.ObservationRegistry;
import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.ai.anthropic.api.AnthropicApi;
import org.springframework.ai.anthropic.api.AnthropicApi.AnthropicMessage;
import org.springframework.ai.anthropic.api.AnthropicApi.ChatCompletionRequest;
@@ -42,7 +46,11 @@ import org.springframework.ai.chat.messages.ToolResponseMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.metadata.ChatGenerationMetadata;
import org.springframework.ai.chat.metadata.ChatResponseMetadata;
import org.springframework.ai.chat.model.*;
import org.springframework.ai.chat.model.AbstractToolCallSupport;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.model.MessageAggregator;
import org.springframework.ai.chat.observation.ChatModelObservationContext;
import org.springframework.ai.chat.observation.ChatModelObservationConvention;
import org.springframework.ai.chat.observation.ChatModelObservationDocumentation;
@@ -61,9 +69,6 @@ import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* The {@link ChatModel} implementation for the Anthropic service.
*
@@ -76,16 +81,21 @@ import reactor.core.publisher.Mono;
*/
public class AnthropicChatModel extends AbstractToolCallSupport implements ChatModel {
private static final Logger logger = LoggerFactory.getLogger(AnthropicChatModel.class);
private static final ChatModelObservationConvention DEFAULT_OBSERVATION_CONVENTION = new DefaultChatModelObservationConvention();
public static final String DEFAULT_MODEL_NAME = AnthropicApi.ChatModel.CLAUDE_3_5_SONNET.getValue();
public static final Integer DEFAULT_MAX_TOKENS = 500;
public static final Double DEFAULT_TEMPERATURE = 0.8;
private static final Logger logger = LoggerFactory.getLogger(AnthropicChatModel.class);
private static final ChatModelObservationConvention DEFAULT_OBSERVATION_CONVENTION = new DefaultChatModelObservationConvention();
/**
* The retry template used to retry the OpenAI API calls.
*/
public final RetryTemplate retryTemplate;
/**
* The lower-level API for the Anthropic service.
*/
@@ -96,11 +106,6 @@ public class AnthropicChatModel extends AbstractToolCallSupport implements ChatM
*/
private final AnthropicChatOptions defaultOptions;
/**
* The retry template used to retry the OpenAI API calls.
*/
public final RetryTemplate retryTemplate;
/**
* Observation registry used for instrumentation.
*/

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.anthropic;
import java.util.ArrayList;
@@ -91,91 +92,24 @@ public class AnthropicChatOptions implements ChatOptions, FunctionCallingOptions
return new Builder();
}
public static class Builder {
private final AnthropicChatOptions options = new AnthropicChatOptions();
public Builder withModel(String model) {
this.options.model = model;
return this;
}
public Builder withModel(AnthropicApi.ChatModel model) {
this.options.model = model.getValue();
return this;
}
public Builder withMaxTokens(Integer maxTokens) {
this.options.maxTokens = maxTokens;
return this;
}
public Builder withMetadata(ChatCompletionRequest.Metadata metadata) {
this.options.metadata = metadata;
return this;
}
public Builder withStopSequences(List<String> stopSequences) {
this.options.stopSequences = stopSequences;
return this;
}
public Builder withTemperature(Double temperature) {
this.options.temperature = temperature;
return this;
}
public Builder withTopP(Double topP) {
this.options.topP = topP;
return this;
}
public Builder withTopK(Integer topK) {
this.options.topK = topK;
return this;
}
public Builder withFunctionCallbacks(List<FunctionCallback> functionCallbacks) {
this.options.functionCallbacks = functionCallbacks;
return this;
}
public Builder withFunctions(Set<String> functionNames) {
Assert.notNull(functionNames, "Function names must not be null");
this.options.functions = functionNames;
return this;
}
public Builder withFunction(String functionName) {
Assert.hasText(functionName, "Function name must not be empty");
this.options.functions.add(functionName);
return this;
}
public Builder withProxyToolCalls(Boolean proxyToolCalls) {
this.options.proxyToolCalls = proxyToolCalls;
return this;
}
public Builder withToolContext(Map<String, Object> toolContext) {
if (this.options.toolContext == null) {
this.options.toolContext = toolContext;
}
else {
this.options.toolContext.putAll(toolContext);
}
return this;
}
public AnthropicChatOptions build() {
return this.options;
}
public static AnthropicChatOptions fromOptions(AnthropicChatOptions fromOptions) {
return builder().withModel(fromOptions.getModel())
.withMaxTokens(fromOptions.getMaxTokens())
.withMetadata(fromOptions.getMetadata())
.withStopSequences(fromOptions.getStopSequences())
.withTemperature(fromOptions.getTemperature())
.withTopP(fromOptions.getTopP())
.withTopK(fromOptions.getTopK())
.withFunctionCallbacks(fromOptions.getFunctionCallbacks())
.withFunctions(fromOptions.getFunctions())
.withProxyToolCalls(fromOptions.getProxyToolCalls())
.withToolContext(fromOptions.getToolContext())
.build();
}
@Override
public String getModel() {
return model;
return this.model;
}
public void setModel(String model) {
@@ -293,19 +227,86 @@ public class AnthropicChatOptions implements ChatOptions, FunctionCallingOptions
return fromOptions(this);
}
public static AnthropicChatOptions fromOptions(AnthropicChatOptions fromOptions) {
return builder().withModel(fromOptions.getModel())
.withMaxTokens(fromOptions.getMaxTokens())
.withMetadata(fromOptions.getMetadata())
.withStopSequences(fromOptions.getStopSequences())
.withTemperature(fromOptions.getTemperature())
.withTopP(fromOptions.getTopP())
.withTopK(fromOptions.getTopK())
.withFunctionCallbacks(fromOptions.getFunctionCallbacks())
.withFunctions(fromOptions.getFunctions())
.withProxyToolCalls(fromOptions.getProxyToolCalls())
.withToolContext(fromOptions.getToolContext())
.build();
public static class Builder {
private final AnthropicChatOptions options = new AnthropicChatOptions();
public Builder withModel(String model) {
this.options.model = model;
return this;
}
public Builder withModel(AnthropicApi.ChatModel model) {
this.options.model = model.getValue();
return this;
}
public Builder withMaxTokens(Integer maxTokens) {
this.options.maxTokens = maxTokens;
return this;
}
public Builder withMetadata(ChatCompletionRequest.Metadata metadata) {
this.options.metadata = metadata;
return this;
}
public Builder withStopSequences(List<String> stopSequences) {
this.options.stopSequences = stopSequences;
return this;
}
public Builder withTemperature(Double temperature) {
this.options.temperature = temperature;
return this;
}
public Builder withTopP(Double topP) {
this.options.topP = topP;
return this;
}
public Builder withTopK(Integer topK) {
this.options.topK = topK;
return this;
}
public Builder withFunctionCallbacks(List<FunctionCallback> functionCallbacks) {
this.options.functionCallbacks = functionCallbacks;
return this;
}
public Builder withFunctions(Set<String> functionNames) {
Assert.notNull(functionNames, "Function names must not be null");
this.options.functions = functionNames;
return this;
}
public Builder withFunction(String functionName) {
Assert.hasText(functionName, "Function name must not be empty");
this.options.functions.add(functionName);
return this;
}
public Builder withProxyToolCalls(Boolean proxyToolCalls) {
this.options.proxyToolCalls = proxyToolCalls;
return this;
}
public Builder withToolContext(Map<String, Object> toolContext) {
if (this.options.toolContext == null) {
this.options.toolContext = toolContext;
}
else {
this.options.toolContext.putAll(toolContext);
}
return this;
}
public AnthropicChatOptions build() {
return this.options;
}
}
}

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.anthropic.aot;
import org.springframework.ai.anthropic.api.AnthropicApi;

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.anthropic.api;
import java.util.ArrayList;
@@ -23,6 +24,14 @@ import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Predicate;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.ai.anthropic.api.StreamHelper.ChatCompletionResponseBuilder;
import org.springframework.ai.model.ChatModelDescription;
import org.springframework.ai.model.ModelOptionsUtils;
@@ -38,15 +47,6 @@ import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.RestClient;
import org.springframework.web.reactive.function.client.WebClient;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* @author Christian Tzolov
* @author Mariusz Bernacki
@@ -57,12 +57,6 @@ public class AnthropicApi {
public static final String PROVIDER_NAME = AiProvider.ANTHROPIC.value();
private static final String HEADER_X_API_KEY = "x-api-key";
private static final String HEADER_ANTHROPIC_VERSION = "anthropic-version";
private static final String HEADER_ANTHROPIC_BETA = "anthropic-beta";
public static final String DEFAULT_BASE_URL = "https://api.anthropic.com";
public static final String DEFAULT_ANTHROPIC_VERSION = "2023-06-01";
@@ -71,10 +65,18 @@ public class AnthropicApi {
public static final String BETA_MAX_TOKENS = "max-tokens-3-5-sonnet-2024-07-15";
private static final String HEADER_X_API_KEY = "x-api-key";
private static final String HEADER_ANTHROPIC_VERSION = "anthropic-version";
private static final String HEADER_ANTHROPIC_BETA = "anthropic-beta";
private static final Predicate<String> SSE_DONE_PREDICATE = "[DONE]"::equals;
private final RestClient restClient;
private final StreamHelper streamHelper = new StreamHelper();
private WebClient webClient;
/**
@@ -141,6 +143,74 @@ public class AnthropicApi {
.build();
}
/**
* Creates a model response for the given chat conversation.
* @param chatRequest The chat completion request.
* @return Entity response with {@link ChatCompletionResponse} as a body and HTTP
* status code and headers.
*/
public ResponseEntity<ChatCompletionResponse> chatCompletionEntity(ChatCompletionRequest chatRequest) {
Assert.notNull(chatRequest, "The request body can not be null.");
Assert.isTrue(!chatRequest.stream(), "Request must set the stream property to false.");
return this.restClient.post()
.uri("/v1/messages")
.body(chatRequest)
.retrieve()
.toEntity(ChatCompletionResponse.class);
}
/**
* Creates a streaming chat response for the given chat conversation.
* @param chatRequest The chat completion request. Must have the stream property set
* to true.
* @return Returns a {@link Flux} stream from chat completion chunks.
*/
public Flux<ChatCompletionResponse> chatCompletionStream(ChatCompletionRequest chatRequest) {
Assert.notNull(chatRequest, "The request body can not be null.");
Assert.isTrue(chatRequest.stream(), "Request must set the stream property to true.");
AtomicBoolean isInsideTool = new AtomicBoolean(false);
AtomicReference<ChatCompletionResponseBuilder> chatCompletionReference = new AtomicReference<>();
return this.webClient.post()
.uri("/v1/messages")
.body(Mono.just(chatRequest), ChatCompletionRequest.class)
.retrieve()
.bodyToFlux(String.class)
.takeUntil(SSE_DONE_PREDICATE)
.filter(SSE_DONE_PREDICATE.negate())
.map(content -> ModelOptionsUtils.jsonToObject(content, StreamEvent.class))
.filter(event -> event.type() != EventType.PING)
// Detect if the chunk is part of a streaming function call.
.map(event -> {
if (this.streamHelper.isToolUseStart(event)) {
isInsideTool.set(true);
}
return event;
})
// Group all chunks belonging to the same function call.
.windowUntil(event -> {
if (isInsideTool.get() && this.streamHelper.isToolUseFinish(event)) {
isInsideTool.set(false);
return true;
}
return !isInsideTool.get();
})
// Merging the window chunks into a single chunk.
.concatMapIterable(window -> {
Mono<StreamEvent> monoChunk = window.reduce(new ToolUseAggregationEvent(),
this.streamHelper::mergeToolUseEvents);
return List.of(monoChunk);
})
.flatMap(mono -> mono)
.map(event -> this.streamHelper.eventToChatCompletionResponse(event, chatCompletionReference))
.filter(chatCompletionResponse -> chatCompletionResponse.type() != null);
}
/**
* Check the <a href="https://docs.anthropic.com/claude/docs/models-overview">Models
* overview</a> and <a href=
@@ -185,13 +255,93 @@ public class AnthropicApi {
*/
public enum Role {
// @formatter:off
// @formatter:off
@JsonProperty("user") USER,
@JsonProperty("assistant") ASSISTANT
// @formatter:on
}
/**
* The evnt type of the streamed chunk.
*/
public enum EventType {
/**
* Message start event. Contains a Message object with empty content.
*/
@JsonProperty("message_start")
MESSAGE_START,
/**
* Message delta event, indicating top-level changes to the final Message object.
*/
@JsonProperty("message_delta")
MESSAGE_DELTA,
/**
* A final message stop event.
*/
@JsonProperty("message_stop")
MESSAGE_STOP,
/**
*
*/
@JsonProperty("content_block_start")
CONTENT_BLOCK_START,
/**
*
*/
@JsonProperty("content_block_delta")
CONTENT_BLOCK_DELTA,
/**
*
*/
@JsonProperty("content_block_stop")
CONTENT_BLOCK_STOP,
/**
*
*/
@JsonProperty("error")
ERROR,
/**
*
*/
@JsonProperty("ping")
PING,
/**
* Artifically created event to aggregate tool use events.
*/
TOOL_USE_AGGREATE;
}
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type",
visible = true)
@JsonSubTypes({ @JsonSubTypes.Type(value = ContentBlockStartEvent.class, name = "content_block_start"),
@JsonSubTypes.Type(value = ContentBlockDeltaEvent.class, name = "content_block_delta"),
@JsonSubTypes.Type(value = ContentBlockStopEvent.class, name = "content_block_stop"),
@JsonSubTypes.Type(value = PingEvent.class, name = "ping"),
@JsonSubTypes.Type(value = ErrorEvent.class, name = "error"),
@JsonSubTypes.Type(value = MessageStartEvent.class, name = "message_start"),
@JsonSubTypes.Type(value = MessageDeltaEvent.class, name = "message_delta"),
@JsonSubTypes.Type(value = MessageStopEvent.class, name = "message_stop") })
public interface StreamEvent {
@JsonProperty("type")
EventType type();
}
/**
* @param model The model that will complete your prompt. See the list of
* <a href="https://docs.anthropic.com/claude/docs/models-overview">models</a> for
@@ -257,6 +407,14 @@ public class AnthropicApi {
this(model, messages, system, maxTokens, null, stopSequences, stream, temperature, null, null, null);
}
public static ChatCompletionRequestBuilder builder() {
return new ChatCompletionRequestBuilder();
}
public static ChatCompletionRequestBuilder from(ChatCompletionRequest request) {
return new ChatCompletionRequestBuilder(request);
}
/**
* @param userId An external identifier for the user who is associated with the
* request. This should be a uuid, hash value, or other opaque identifier.
@@ -265,15 +423,9 @@ public class AnthropicApi {
*/
@JsonInclude(Include.NON_NULL)
public record Metadata(@JsonProperty("user_id") String userId) {
}
public static ChatCompletionRequestBuilder builder() {
return new ChatCompletionRequestBuilder();
}
public static ChatCompletionRequestBuilder from(ChatCompletionRequest request) {
return new ChatCompletionRequestBuilder(request);
}
}
public static class ChatCompletionRequestBuilder {
@@ -378,12 +530,16 @@ public class AnthropicApi {
}
public ChatCompletionRequest build() {
return new ChatCompletionRequest(model, messages, system, maxTokens, metadata, stopSequences, stream,
temperature, topP, topK, tools);
return new ChatCompletionRequest(this.model, this.messages, this.system, this.maxTokens, this.metadata,
this.stopSequences, this.stream, this.temperature, this.topP, this.topK, this.tools);
}
}
///////////////////////////////////////
/// ERROR EVENT
///////////////////////////////////////
/**
* Input messages.
*
@@ -535,9 +691,15 @@ public class AnthropicApi {
public Source(String mediaType, String data) {
this("base64", mediaType, data);
}
}
}
///////////////////////////////////////
/// CONTENT_BLOCK EVENTS
///////////////////////////////////////
@JsonInclude(Include.NON_NULL)
public record Tool(// @formatter:off
@JsonProperty("name") String name,
@@ -546,6 +708,8 @@ public class AnthropicApi {
// @formatter:on
}
// CB START EVENT
/**
* @param id Unique object identifier. The format and length of IDs may change over
* time.
@@ -572,6 +736,8 @@ public class AnthropicApi {
// @formatter:on
}
// CB DELTA EVENT
/**
* Usage statistics.
*
@@ -585,94 +751,7 @@ public class AnthropicApi {
// @formatter:off
}
///////////////////////////////////////
/// ERROR EVENT
///////////////////////////////////////
/**
* The evnt type of the streamed chunk.
*/
public enum EventType {
/**
* Message start event. Contains a Message object with empty content.
*/
@JsonProperty("message_start")
MESSAGE_START,
/**
* Message delta event, indicating top-level changes to the final Message object.
*/
@JsonProperty("message_delta")
MESSAGE_DELTA,
/**
* A final message stop event.
*/
@JsonProperty("message_stop")
MESSAGE_STOP,
/**
*
*/
@JsonProperty("content_block_start")
CONTENT_BLOCK_START,
/**
*
*/
@JsonProperty("content_block_delta")
CONTENT_BLOCK_DELTA,
/**
*
*/
@JsonProperty("content_block_stop")
CONTENT_BLOCK_STOP,
/**
*
*/
@JsonProperty("error")
ERROR,
/**
*
*/
@JsonProperty("ping")
PING,
/**
* Artifically created event to aggregate tool use events.
*/
TOOL_USE_AGGREATE;
}
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type",
visible = true)
@JsonSubTypes({ @JsonSubTypes.Type(value = ContentBlockStartEvent.class, name = "content_block_start"),
@JsonSubTypes.Type(value = ContentBlockDeltaEvent.class, name = "content_block_delta"),
@JsonSubTypes.Type(value = ContentBlockStopEvent.class, name = "content_block_stop"),
@JsonSubTypes.Type(value = PingEvent.class, name = "ping"),
@JsonSubTypes.Type(value = ErrorEvent.class, name = "error"),
@JsonSubTypes.Type(value = MessageStartEvent.class, name = "message_start"),
@JsonSubTypes.Type(value = MessageDeltaEvent.class, name = "message_delta"),
@JsonSubTypes.Type(value = MessageStopEvent.class, name = "message_stop") })
public interface StreamEvent {
@JsonProperty("type")
EventType type();
}
///////////////////////////////////////
/// CONTENT_BLOCK EVENTS
///////////////////////////////////////
/// ECB STOP
/**
* Special event used to aggregate multiple tool use events into a single event with
@@ -736,13 +815,17 @@ public class AnthropicApi {
@Override
public String toString() {
return "EventToolUseBuilder [index=" + index + ", id=" + id + ", name=" + name + ", partialJson="
+ partialJson + ", toolUseMap=" + toolContentBlocks + "]";
return "EventToolUseBuilder [index=" + this.index + ", id=" + this.id + ", name=" + this.name + ", partialJson="
+ this.partialJson + ", toolUseMap=" + this.toolContentBlocks + "]";
}
}
// CB START EVENT
///////////////////////////////////////
/// MESSAGE EVENTS
///////////////////////////////////////
// MESSAGE START EVENT
@JsonInclude(Include.NON_NULL)
public record ContentBlockStartEvent(// @formatter:off
@@ -773,7 +856,7 @@ public class AnthropicApi {
}
}// @formatter:on
// CB DELTA EVENT
// MESSAGE DELTA EVENT
@JsonInclude(Include.NON_NULL)
public record ContentBlockDeltaEvent(// @formatter:off
@@ -803,7 +886,7 @@ public class AnthropicApi {
}
}// @formatter:on
/// ECB STOP
// MESSAGE STOP EVENT
@JsonInclude(Include.NON_NULL)
public record ContentBlockStopEvent(// @formatter:off
@@ -811,20 +894,12 @@ public class AnthropicApi {
@JsonProperty("index") Integer index) implements StreamEvent {
}// @formatter:on
///////////////////////////////////////
/// MESSAGE EVENTS
///////////////////////////////////////
// MESSAGE START EVENT
@JsonInclude(Include.NON_NULL)
public record MessageStartEvent(// @formatter:off
@JsonProperty("type") EventType type,
@JsonProperty("message") ChatCompletionResponse message) implements StreamEvent {
}// @formatter:on
// MESSAGE DELTA EVENT
@JsonInclude(Include.NON_NULL)
public record MessageDeltaEvent(// @formatter:off
@JsonProperty("type") EventType type,
@@ -843,8 +918,6 @@ public class AnthropicApi {
}
}// @formatter:on
// MESSAGE STOP EVENT
@JsonInclude(Include.NON_NULL)
public record MessageStopEvent(// @formatter:off
@JsonProperty("type") EventType type) implements StreamEvent {
@@ -873,74 +946,4 @@ public class AnthropicApi {
@JsonProperty("type") EventType type) implements StreamEvent {
}// @formatter:on
/**
* Creates a model response for the given chat conversation.
* @param chatRequest The chat completion request.
* @return Entity response with {@link ChatCompletionResponse} as a body and HTTP
* status code and headers.
*/
public ResponseEntity<ChatCompletionResponse> chatCompletionEntity(ChatCompletionRequest chatRequest) {
Assert.notNull(chatRequest, "The request body can not be null.");
Assert.isTrue(!chatRequest.stream(), "Request must set the stream property to false.");
return this.restClient.post()
.uri("/v1/messages")
.body(chatRequest)
.retrieve()
.toEntity(ChatCompletionResponse.class);
}
private final StreamHelper streamHelper = new StreamHelper();
/**
* Creates a streaming chat response for the given chat conversation.
* @param chatRequest The chat completion request. Must have the stream property set
* to true.
* @return Returns a {@link Flux} stream from chat completion chunks.
*/
public Flux<ChatCompletionResponse> chatCompletionStream(ChatCompletionRequest chatRequest) {
Assert.notNull(chatRequest, "The request body can not be null.");
Assert.isTrue(chatRequest.stream(), "Request must set the stream property to true.");
AtomicBoolean isInsideTool = new AtomicBoolean(false);
AtomicReference<ChatCompletionResponseBuilder> chatCompletionReference = new AtomicReference<>();
return this.webClient.post()
.uri("/v1/messages")
.body(Mono.just(chatRequest), ChatCompletionRequest.class)
.retrieve()
.bodyToFlux(String.class)
.takeUntil(SSE_DONE_PREDICATE)
.filter(SSE_DONE_PREDICATE.negate())
.map(content -> ModelOptionsUtils.jsonToObject(content, StreamEvent.class))
.filter(event -> event.type() != EventType.PING)
// Detect if the chunk is part of a streaming function call.
.map(event -> {
if (this.streamHelper.isToolUseStart(event)) {
isInsideTool.set(true);
}
return event;
})
// Group all chunks belonging to the same function call.
.windowUntil(event -> {
if (isInsideTool.get() && this.streamHelper.isToolUseFinish(event)) {
isInsideTool.set(false);
return true;
}
return !isInsideTool.get();
})
// Merging the window chunks into a single chunk.
.concatMapIterable(window -> {
Mono<StreamEvent> monoChunk = window.reduce(new ToolUseAggregationEvent(),
this.streamHelper::mergeToolUseEvents);
return List.of(monoChunk);
})
.flatMap(mono -> mono)
.map(event -> streamHelper.eventToChatCompletionResponse(event, chatCompletionReference))
.filter(chatCompletionResponse -> chatCompletionResponse.type() != null);
}
}

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.anthropic.api;
import java.util.ArrayList;
@@ -22,22 +23,22 @@ import java.util.concurrent.atomic.AtomicReference;
import org.springframework.ai.anthropic.api.AnthropicApi.ChatCompletionResponse;
import org.springframework.ai.anthropic.api.AnthropicApi.ContentBlock;
import org.springframework.ai.anthropic.api.AnthropicApi.ContentBlock.Type;
import org.springframework.ai.anthropic.api.AnthropicApi.Role;
import org.springframework.ai.anthropic.api.AnthropicApi.Usage;
import org.springframework.ai.anthropic.api.AnthropicApi.ContentBlockDeltaEvent;
import org.springframework.ai.anthropic.api.AnthropicApi.ContentBlockStartEvent;
import org.springframework.ai.anthropic.api.AnthropicApi.ToolUseAggregationEvent;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.ai.anthropic.api.AnthropicApi.MessageDeltaEvent;
import org.springframework.ai.anthropic.api.AnthropicApi.MessageStartEvent;
import org.springframework.ai.anthropic.api.AnthropicApi.StreamEvent;
import org.springframework.ai.anthropic.api.AnthropicApi.ContentBlockDeltaEvent.ContentBlockDeltaJson;
import org.springframework.ai.anthropic.api.AnthropicApi.ContentBlockDeltaEvent.ContentBlockDeltaText;
import org.springframework.ai.anthropic.api.AnthropicApi.ContentBlockStartEvent;
import org.springframework.ai.anthropic.api.AnthropicApi.ContentBlockStartEvent.ContentBlockText;
import org.springframework.ai.anthropic.api.AnthropicApi.ContentBlockStartEvent.ContentBlockToolUse;
import org.springframework.ai.anthropic.api.AnthropicApi.EventType;
import org.springframework.ai.anthropic.api.AnthropicApi.MessageDeltaEvent;
import org.springframework.ai.anthropic.api.AnthropicApi.MessageStartEvent;
import org.springframework.ai.anthropic.api.AnthropicApi.Role;
import org.springframework.ai.anthropic.api.AnthropicApi.StreamEvent;
import org.springframework.ai.anthropic.api.AnthropicApi.ToolUseAggregationEvent;
import org.springframework.ai.anthropic.api.AnthropicApi.Usage;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
/**
* Helper class to support streaming function calling.

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.anthropic.metadata;
import java.time.Duration;

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.anthropic.metadata;
import org.springframework.ai.anthropic.api.AnthropicApi;
@@ -27,10 +28,6 @@ import org.springframework.util.Assert;
*/
public class AnthropicUsage implements Usage {
public static AnthropicUsage from(AnthropicApi.Usage usage) {
return new AnthropicUsage(usage);
}
private final AnthropicApi.Usage usage;
protected AnthropicUsage(AnthropicApi.Usage usage) {
@@ -38,6 +35,10 @@ public class AnthropicUsage implements Usage {
this.usage = usage;
}
public static AnthropicUsage from(AnthropicApi.Usage usage) {
return new AnthropicUsage(usage);
}
protected AnthropicApi.Usage getUsage() {
return this.usage;
}

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,9 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.anthropic;
import static org.assertj.core.api.Assertions.assertThat;
package org.springframework.ai.anthropic;
import java.io.IOException;
import java.util.ArrayList;
@@ -30,11 +29,12 @@ import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
import org.springframework.ai.anthropic.api.AnthropicApi;
import org.springframework.ai.anthropic.api.tool.MockWeatherService;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.model.Media;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatModel;
@@ -47,6 +47,7 @@ import org.springframework.ai.chat.prompt.SystemPromptTemplate;
import org.springframework.ai.converter.BeanOutputConverter;
import org.springframework.ai.converter.ListOutputConverter;
import org.springframework.ai.converter.MapOutputConverter;
import org.springframework.ai.model.Media;
import org.springframework.ai.model.function.FunctionCallbackWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
@@ -59,7 +60,7 @@ import org.springframework.core.io.Resource;
import org.springframework.util.MimeTypeUtils;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Flux;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(classes = AnthropicChatModelIT.Config.class, properties = "spring.ai.retry.on-http-codes=429")
@EnabledIfEnvironmentVariable(named = "ANTHROPIC_API_KEY", matches = ".+")
@@ -76,17 +77,25 @@ class AnthropicChatModelIT {
@Value("classpath:/prompts/system-message.st")
private Resource systemResource;
private static void validateChatResponseMetadata(ChatResponse response, String model) {
assertThat(response.getMetadata().getId()).isNotEmpty();
assertThat(response.getMetadata().getModel()).containsIgnoringCase(model);
assertThat(response.getMetadata().getUsage().getPromptTokens()).isPositive();
assertThat(response.getMetadata().getUsage().getGenerationTokens()).isPositive();
assertThat(response.getMetadata().getUsage().getTotalTokens()).isPositive();
}
@ParameterizedTest(name = "{0} : {displayName} ")
@ValueSource(strings = { "claude-3-opus-20240229", "claude-3-sonnet-20240229", "claude-3-haiku-20240307",
"claude-3-5-sonnet-20241022" })
void roleTest(String modelName) {
UserMessage userMessage = new UserMessage(
"Tell me about 3 famous pirates from the Golden Age of Piracy and why they did.");
SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(systemResource);
SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(this.systemResource);
Message systemMessage = systemPromptTemplate.createMessage(Map.of("name", "Bob", "voice", "pirate"));
Prompt prompt = new Prompt(List.of(userMessage, systemMessage),
AnthropicChatOptions.builder().withModel(modelName).build());
ChatResponse response = chatModel.call(prompt);
ChatResponse response = this.chatModel.call(prompt);
assertThat(response.getResults()).hasSize(1);
assertThat(response.getMetadata().getUsage().getGenerationTokens()).isGreaterThan(0);
assertThat(response.getMetadata().getUsage().getPromptTokens()).isGreaterThan(0);
@@ -103,17 +112,17 @@ class AnthropicChatModelIT {
void testMessageHistory() {
UserMessage userMessage = new UserMessage(
"Tell me about 3 famous pirates from the Golden Age of Piracy and why they did.");
SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(systemResource);
SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(this.systemResource);
Message systemMessage = systemPromptTemplate.createMessage(Map.of("name", "Bob", "voice", "pirate"));
Prompt prompt = new Prompt(List.of(userMessage, systemMessage),
AnthropicChatOptions.builder().withModel("claude-3-sonnet-20240229").build());
ChatResponse response = chatModel.call(prompt);
ChatResponse response = this.chatModel.call(prompt);
assertThat(response.getResult().getOutput().getContent()).containsAnyOf("Blackbeard", "Bartholomew");
var promptWithMessageHistory = new Prompt(List.of(new UserMessage("Dummy"), response.getResult().getOutput(),
new UserMessage("Repeat the last assistant message.")));
response = chatModel.call(promptWithMessageHistory);
response = this.chatModel.call(promptWithMessageHistory);
assertThat(response.getResult().getOutput().getContent()).containsAnyOf("Blackbeard", "Bartholomew");
}
@@ -167,16 +176,13 @@ class AnthropicChatModelIT {
PromptTemplate promptTemplate = new PromptTemplate(template,
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 = mapOutputConverter.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) {
}
@Test
void beanOutputConverterRecords() {
@@ -189,7 +195,7 @@ class AnthropicChatModelIT {
""";
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 = beanOutputConverter.convert(generation.getOutput().getContent());
logger.info("" + actorsFilms);
@@ -210,7 +216,7 @@ class AnthropicChatModelIT {
PromptTemplate promptTemplate = new PromptTemplate(template, Map.of("format", format));
Prompt prompt = new Prompt(promptTemplate.createMessage());
String generationTextFromStream = streamingChatModel.stream(prompt)
String generationTextFromStream = this.streamingChatModel.stream(prompt)
.collectList()
.block()
.stream()
@@ -234,7 +240,7 @@ class AnthropicChatModelIT {
var userMessage = new UserMessage("Explain what do you see on this picture?",
List.of(new Media(MimeTypeUtils.IMAGE_PNG, imageData)));
var response = chatModel.call(new Prompt(List.of(userMessage)));
var response = this.chatModel.call(new Prompt(List.of(userMessage)));
logger.info(response.getResult().getOutput().getContent());
assertThat(response.getResult().getOutput().getContent()).contains("banan", "apple", "basket");
@@ -257,7 +263,7 @@ class AnthropicChatModelIT {
.build()))
.build();
ChatResponse response = chatModel.call(new Prompt(messages, promptOptions));
ChatResponse response = this.chatModel.call(new Prompt(messages, promptOptions));
logger.info("Response: {}", response);
@@ -284,7 +290,7 @@ class AnthropicChatModelIT {
.build()))
.build();
Flux<ChatResponse> response = chatModel.stream(new Prompt(messages, promptOptions));
Flux<ChatResponse> response = this.chatModel.stream(new Prompt(messages, promptOptions));
String content = response.collectList()
.block()
@@ -301,7 +307,7 @@ class AnthropicChatModelIT {
void validateCallResponseMetadata() {
String model = AnthropicApi.ChatModel.CLAUDE_2_1.getName();
// @formatter:off
ChatResponse response = ChatClient.create(chatModel).prompt()
ChatResponse response = ChatClient.create(this.chatModel).prompt()
.options(AnthropicChatOptions.builder().withModel(model).build())
.user("Tell me about 3 famous pirates from the Golden Age of Piracy and what they did")
.call()
@@ -316,7 +322,7 @@ class AnthropicChatModelIT {
void validateStreamCallResponseMetadata() {
String model = AnthropicApi.ChatModel.CLAUDE_3_5_SONNET.getName();
// @formatter:off
ChatResponse response = ChatClient.create(chatModel).prompt()
ChatResponse response = ChatClient.create(this.chatModel).prompt()
.options(AnthropicChatOptions.builder().withModel(model).build())
.user("Tell me about 3 famous pirates from the Golden Age of Piracy and what they did")
.stream()
@@ -328,12 +334,8 @@ class AnthropicChatModelIT {
validateChatResponseMetadata(response, model);
}
private static void validateChatResponseMetadata(ChatResponse response, String model) {
assertThat(response.getMetadata().getId()).isNotEmpty();
assertThat(response.getMetadata().getModel()).containsIgnoringCase(model);
assertThat(response.getMetadata().getUsage().getPromptTokens()).isPositive();
assertThat(response.getMetadata().getUsage().getGenerationTokens()).isPositive();
assertThat(response.getMetadata().getUsage().getTotalTokens()).isPositive();
record ActorsFilmsRecord(String actor, List<String> movies) {
}
@SpringBootConfiguration
@@ -360,4 +362,4 @@ class AnthropicChatModelIT {
}
}
}

View File

@@ -1,11 +1,11 @@
/*
* Copyright 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,16 +13,19 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.anthropic;
import static org.assertj.core.api.Assertions.assertThat;
package org.springframework.ai.anthropic;
import java.util.List;
import java.util.stream.Collectors;
import io.micrometer.observation.tck.TestObservationRegistry;
import io.micrometer.observation.tck.TestObservationRegistryAssert;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import reactor.core.publisher.Flux;
import org.springframework.ai.anthropic.api.AnthropicApi;
import org.springframework.ai.chat.metadata.ChatResponseMetadata;
import org.springframework.ai.chat.model.ChatResponse;
@@ -39,9 +42,7 @@ import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.retry.support.RetryTemplate;
import io.micrometer.observation.tck.TestObservationRegistry;
import io.micrometer.observation.tck.TestObservationRegistryAssert;
import reactor.core.publisher.Flux;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for observation instrumentation in {@link AnthropicChatModel}.
@@ -61,7 +62,7 @@ public class AnthropicChatModelObservationIT {
@BeforeEach
void beforeEach() {
observationRegistry.clear();
this.observationRegistry.clear();
}
@Test
@@ -77,7 +78,7 @@ public class AnthropicChatModelObservationIT {
Prompt prompt = new Prompt("Why does a raven look like a desk?", options);
ChatResponse chatResponse = chatModel.call(prompt);
ChatResponse chatResponse = this.chatModel.call(prompt);
assertThat(chatResponse.getResult().getOutput().getContent()).isNotEmpty();
ChatResponseMetadata responseMetadata = chatResponse.getMetadata();
@@ -99,7 +100,7 @@ public class AnthropicChatModelObservationIT {
Prompt prompt = new Prompt("Why does a raven look like a desk?", options);
Flux<ChatResponse> chatResponseFlux = chatModel.stream(prompt);
Flux<ChatResponse> chatResponseFlux = this.chatModel.stream(prompt);
List<ChatResponse> responses = chatResponseFlux.collectList().block();
assertThat(responses).isNotEmpty();
@@ -121,7 +122,7 @@ public class AnthropicChatModelObservationIT {
}
private void validate(ChatResponseMetadata responseMetadata, String finishReasons) {
TestObservationRegistryAssert.assertThat(observationRegistry)
TestObservationRegistryAssert.assertThat(this.observationRegistry)
.doesNotHaveAnyRemainingCurrentObservation()
.hasObservationWithNameEqualTo(DefaultChatModelObservationConvention.DEFAULT_NAME)
.that()

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.anthropic;
import org.springframework.ai.anthropic.api.AnthropicApi;

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.anthropic;
import org.junit.jupiter.api.Test;

View File

@@ -1,34 +1,35 @@
/*
* Copyright 2024 - 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.anthropic;
* 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.
*/
import static org.assertj.core.api.Assertions.assertThat;
package org.springframework.ai.anthropic;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.List;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.anthropic.api.AnthropicApi.StreamEvent;
import org.springframework.core.io.DefaultResourceLoader;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Christian Tzolov
@@ -44,6 +45,7 @@ public class EventParsingTests {
.getContentAsString(Charset.defaultCharset());
List<StreamEvent> events = new ObjectMapper().readerFor(new TypeReference<List<StreamEvent>>() {
}).readValue(json);
logger.info(events.toString());

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,16 +13,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.anthropic.aot;
import java.util.Set;
import org.junit.jupiter.api.Test;
import org.springframework.ai.anthropic.api.AnthropicApi;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.TypeReference;
import java.util.Set;
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;

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,14 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.anthropic.api;
import static org.assertj.core.api.Assertions.assertThat;
package org.springframework.ai.anthropic.api;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import reactor.core.publisher.Flux;
import org.springframework.ai.anthropic.api.AnthropicApi.AnthropicMessage;
import org.springframework.ai.anthropic.api.AnthropicApi.ChatCompletionRequest;
import org.springframework.ai.anthropic.api.AnthropicApi.ChatCompletionResponse;
@@ -28,7 +29,7 @@ import org.springframework.ai.anthropic.api.AnthropicApi.ContentBlock;
import org.springframework.ai.anthropic.api.AnthropicApi.Role;
import org.springframework.http.ResponseEntity;
import reactor.core.publisher.Flux;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Christian Tzolov
@@ -43,7 +44,7 @@ public class AnthropicApiIT {
AnthropicMessage chatCompletionMessage = new AnthropicMessage(List.of(new ContentBlock("Tell me a Joke?")),
Role.USER);
ResponseEntity<ChatCompletionResponse> response = anthropicApi
ResponseEntity<ChatCompletionResponse> response = this.anthropicApi
.chatCompletionEntity(new ChatCompletionRequest(AnthropicApi.ChatModel.CLAUDE_3_OPUS.getValue(),
List.of(chatCompletionMessage), null, 100, 0.8, false));
@@ -58,7 +59,7 @@ public class AnthropicApiIT {
AnthropicMessage chatCompletionMessage = new AnthropicMessage(List.of(new ContentBlock("Tell me a Joke?")),
Role.USER);
Flux<ChatCompletionResponse> response = anthropicApi.chatCompletionStream(new ChatCompletionRequest(
Flux<ChatCompletionResponse> response = this.anthropicApi.chatCompletionStream(new ChatCompletionRequest(
AnthropicApi.ChatModel.CLAUDE_3_OPUS.getValue(), List.of(chatCompletionMessage), null, 100, 0.8, true));
assertThat(response).isNotNull();

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.anthropic.api.tool;
import java.util.List;
@@ -25,10 +26,10 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.anthropic.api.AnthropicApi;
import org.springframework.ai.anthropic.api.AnthropicApi.ChatCompletionResponse;
import org.springframework.ai.anthropic.api.AnthropicApi.ChatCompletionRequest;
import org.springframework.ai.anthropic.api.AnthropicApi.ContentBlock;
import org.springframework.ai.anthropic.api.AnthropicApi.AnthropicMessage;
import org.springframework.ai.anthropic.api.AnthropicApi.ChatCompletionRequest;
import org.springframework.ai.anthropic.api.AnthropicApi.ChatCompletionResponse;
import org.springframework.ai.anthropic.api.AnthropicApi.ContentBlock;
import org.springframework.ai.anthropic.api.AnthropicApi.Role;
import org.springframework.ai.anthropic.api.tool.XmlHelper.FunctionCalls;
import org.springframework.ai.anthropic.api.tool.XmlHelper.Tools;
@@ -60,10 +61,6 @@ import static org.assertj.core.api.Assertions.assertThat;
@SuppressWarnings("null")
public class AnthropicApiLegacyToolIT {
private static final Logger logger = LoggerFactory.getLogger(AnthropicApiLegacyToolIT.class);
AnthropicApi anthropicApi = new AnthropicApi(System.getenv("ANTHROPIC_API_KEY"));
public static final String TOO_SYSTEM_PROMPT_TEMPLATE = """
In this environment you have access to a set of tools you can use to answer the user's question.
@@ -84,9 +81,9 @@ public class AnthropicApiLegacyToolIT {
public static final ConcurrentHashMap<String, Function> FUNCTIONS = new ConcurrentHashMap<>();
static {
FUNCTIONS.put("getCurrentWeather", new MockWeatherService());
}
private static final Logger logger = LoggerFactory.getLogger(AnthropicApiLegacyToolIT.class);
AnthropicApi anthropicApi = new AnthropicApi(System.getenv("ANTHROPIC_API_KEY"));
@Test
void toolCalls() {
@@ -120,7 +117,7 @@ public class AnthropicApiLegacyToolIT {
private ResponseEntity<ChatCompletionResponse> doCall(ChatCompletionRequest chatCompletionRequest) {
ResponseEntity<ChatCompletionResponse> response = anthropicApi.chatCompletionEntity(chatCompletionRequest);
ResponseEntity<ChatCompletionResponse> response = this.anthropicApi.chatCompletionEntity(chatCompletionRequest);
FunctionCalls functionCalls = XmlHelper.extractFunctionCalls(response.getBody().content().get(0).text());
@@ -150,4 +147,8 @@ public class AnthropicApiLegacyToolIT {
List.of(chatCompletionMessage2), null, 500, 0.8, false));
}
static {
FUNCTIONS.put("getCurrentWeather", new MockWeatherService());
}
}

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.anthropic.api.tool;
import java.util.ArrayList;
@@ -26,11 +27,11 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.anthropic.api.AnthropicApi;
import org.springframework.ai.anthropic.api.AnthropicApi.ChatCompletionResponse;
import org.springframework.ai.anthropic.api.AnthropicApi.AnthropicMessage;
import org.springframework.ai.anthropic.api.AnthropicApi.ChatCompletionRequest;
import org.springframework.ai.anthropic.api.AnthropicApi.ChatCompletionResponse;
import org.springframework.ai.anthropic.api.AnthropicApi.ContentBlock;
import org.springframework.ai.anthropic.api.AnthropicApi.ContentBlock.Type;
import org.springframework.ai.anthropic.api.AnthropicApi.AnthropicMessage;
import org.springframework.ai.anthropic.api.AnthropicApi.Role;
import org.springframework.ai.anthropic.api.AnthropicApi.Tool;
import org.springframework.ai.model.ModelOptionsUtils;
@@ -53,16 +54,12 @@ import static org.assertj.core.api.Assertions.assertThat;
@SuppressWarnings("null")
public class AnthropicApiToolIT {
public static final ConcurrentHashMap<String, Function> FUNCTIONS = new ConcurrentHashMap<>();
private static final Logger logger = LoggerFactory.getLogger(AnthropicApiLegacyToolIT.class);
AnthropicApi anthropicApi = new AnthropicApi(System.getenv("ANTHROPIC_API_KEY"));
public static final ConcurrentHashMap<String, Function> FUNCTIONS = new ConcurrentHashMap<>();
static {
FUNCTIONS.put("getCurrentWeather", new MockWeatherService());
}
List<Tool> tools = List.of(new Tool("getCurrentWeather",
"Get the weather in location. Return temperature in 30°F or 30°C format.", ModelOptionsUtils.jsonToMap("""
{
@@ -109,10 +106,10 @@ public class AnthropicApiToolIT {
.withMessages(messageConversation)
.withMaxTokens(1500)
.withTemperature(0.8)
.withTools(tools)
.withTools(this.tools)
.build();
ResponseEntity<ChatCompletionResponse> response = anthropicApi.chatCompletionEntity(chatCompletionRequest);
ResponseEntity<ChatCompletionResponse> response = this.anthropicApi.chatCompletionEntity(chatCompletionRequest);
List<ContentBlock> toolToUseList = response.getBody()
.content()
@@ -155,4 +152,8 @@ public class AnthropicApiToolIT {
return doCall(messageConversation);
}
static {
FUNCTIONS.put("getCurrentWeather", new MockWeatherService());
}
}

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.anthropic.api.tool;
import java.util.function.Function;
@@ -28,14 +29,21 @@ import com.fasterxml.jackson.annotation.JsonPropertyDescription;
*/
public class MockWeatherService implements Function<MockWeatherService.Request, MockWeatherService.Response> {
/**
* Weather Function request.
*/
@JsonInclude(Include.NON_NULL)
@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(required = true, value = "unit") @JsonPropertyDescription("Temperature unit") Unit unit) {
@Override
public Response apply(Request request) {
double temperature = 0;
if (request.location().contains("Paris")) {
temperature = 15;
}
else if (request.location().contains("Tokyo")) {
temperature = 10;
}
else if (request.location().contains("San Francisco")) {
temperature = 30;
}
return new Response(temperature, Unit.C);
}
/**
@@ -63,27 +71,22 @@ public class MockWeatherService implements Function<MockWeatherService.Request,
}
/**
* Weather Function request.
*/
@JsonInclude(Include.NON_NULL)
@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(required = true, value = "unit") @JsonPropertyDescription("Temperature unit") Unit unit) {
}
/**
* Weather Function response.
*/
public record Response(double temp, Unit unit) {
}
@Override
public Response apply(Request request) {
double temperature = 0;
if (request.location().contains("Paris")) {
temperature = 15;
}
else if (request.location().contains("Tokyo")) {
temperature = 10;
}
else if (request.location().contains("San Francisco")) {
temperature = 30;
}
return new Response(temperature, Unit.C);
}
}
}

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.anthropic.api.tool;
import java.util.List;
@@ -45,45 +46,6 @@ public class XmlHelper {
private static final XmlMapper xmlMapper = new XmlMapper();
@JsonInclude(Include.NON_NULL) // @formatter:off
@JacksonXmlRootElement(localName = "tools")
public record Tools(
@JacksonXmlElementWrapper(useWrapping = false) @JsonProperty("tool_description") List<ToolDescription> toolDescriptions) {
public record ToolDescription(
@JsonProperty("tool_name") String toolName,
@JsonProperty("description") String description,
@JacksonXmlElementWrapper(localName = "parameters") @JsonProperty("parameter") List<Parameter> parameters) {
@JacksonXmlRootElement(localName = "parameter")
public record Parameter(
@JsonProperty("name") String name,
@JsonProperty("type") String type,
@JsonProperty("description") String description) {
}
}
} // @formatter:on
@JsonInclude(Include.NON_NULL) // @formatter:off
@JacksonXmlRootElement(localName = "function_calls")
public record FunctionCalls(@JsonProperty("invoke") Invoke invoke) {
public record Invoke(
@JsonProperty("tool_name") String toolName,
@JsonProperty("parameters") Map<String, Object> parameters) {
}
} // @formatter:on
@JsonInclude(Include.NON_NULL) // @formatter:off
@JacksonXmlRootElement(localName = "function_results")
public record FunctionResults(
@JacksonXmlElementWrapper(useWrapping = false) @JsonProperty("result") List<Result> result) {
public record Result(
@JsonProperty("tool_name") String toolName,
@JsonProperty("stdout") Object stdout) {
}
} // @formatter:on
public static String extractFunctionCallsXmlBlock(String text) {
if (!StringUtils.hasText(text)) {
return "";
@@ -149,4 +111,43 @@ public class XmlHelper {
}
@JsonInclude(Include.NON_NULL) // @formatter:off
@JacksonXmlRootElement(localName = "tools")
public record Tools(
@JacksonXmlElementWrapper(useWrapping = false) @JsonProperty("tool_description") List<ToolDescription> toolDescriptions) {
public record ToolDescription(
@JsonProperty("tool_name") String toolName,
@JsonProperty("description") String description,
@JacksonXmlElementWrapper(localName = "parameters") @JsonProperty("parameter") List<Parameter> parameters) {
@JacksonXmlRootElement(localName = "parameter")
public record Parameter(
@JsonProperty("name") String name,
@JsonProperty("type") String type,
@JsonProperty("description") String description) {
}
}
} // @formatter:on
@JsonInclude(Include.NON_NULL) // @formatter:off
@JacksonXmlRootElement(localName = "function_calls")
public record FunctionCalls(@JsonProperty("invoke") Invoke invoke) {
public record Invoke(
@JsonProperty("tool_name") String toolName,
@JsonProperty("parameters") Map<String, Object> parameters) {
}
} // @formatter:on
@JsonInclude(Include.NON_NULL) // @formatter:off
@JacksonXmlRootElement(localName = "function_results")
public record FunctionResults(
@JacksonXmlElementWrapper(useWrapping = false) @JsonProperty("result") List<Result> result) {
public record Result(
@JsonProperty("tool_name") String toolName,
@JsonProperty("stdout") Object stdout) {
}
} // @formatter:on
}

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,9 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.anthropic.client;
import static org.assertj.core.api.Assertions.assertThat;
package org.springframework.ai.anthropic.client;
import java.io.IOException;
import java.net.URL;
@@ -31,6 +30,8 @@ import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
import org.springframework.ai.anthropic.AnthropicChatOptions;
import org.springframework.ai.anthropic.AnthropicTestConfiguration;
import org.springframework.ai.anthropic.api.AnthropicApi;
@@ -51,7 +52,7 @@ import org.springframework.core.io.Resource;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.util.MimeTypeUtils;
import reactor.core.publisher.Flux;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(classes = AnthropicTestConfiguration.class, properties = "spring.ai.retry.on-http-codes=429")
@EnabledIfEnvironmentVariable(named = "ANTHROPIC_API_KEY", matches = ".+")
@@ -66,16 +67,13 @@ class AnthropicChatClientIT {
@Value("classpath:/prompts/system-message.st")
private Resource systemTextResource;
record ActorsFilms(String actor, List<String> movies) {
}
@Test
void call() {
// @formatter:off
ChatResponse response = ChatClient.create(chatModel).prompt()
ChatResponse response = ChatClient.create(this.chatModel).prompt()
.advisors(new SimpleLoggerAdvisor())
.system(s -> s.text(systemTextResource)
.system(s -> s.text(this.systemTextResource)
.param("name", "Bob")
.param("voice", "pirate"))
.user("Tell me about 3 famous pirates from the Golden Age of Piracy and what they did")
@@ -91,7 +89,7 @@ class AnthropicChatClientIT {
@Test
void listOutputConverterString() {
// @formatter:off
List<String> collection = ChatClient.create(chatModel).prompt()
List<String> collection = ChatClient.create(this.chatModel).prompt()
.user(u -> u.text("List five {subject}")
.param("subject", "ice cream flavors"))
.call()
@@ -106,7 +104,7 @@ class AnthropicChatClientIT {
void listOutputConverterBean() {
// @formatter:off
List<ActorsFilms> actorsFilms = ChatClient.create(chatModel).prompt()
List<ActorsFilms> actorsFilms = ChatClient.create(this.chatModel).prompt()
.user("Generate the filmography of 5 movies for Tom Hanks and Bill Murray.")
.call()
.entity(new ParameterizedTypeReference<List<ActorsFilms>>() {
@@ -123,7 +121,7 @@ class AnthropicChatClientIT {
var toStringListConverter = new ListOutputConverter(new DefaultConversionService());
// @formatter:off
List<String> flavors = ChatClient.create(chatModel).prompt()
List<String> flavors = ChatClient.create(this.chatModel).prompt()
.user(u -> u.text("List five {subject}")
.param("subject", "ice cream flavors"))
.call()
@@ -138,7 +136,7 @@ class AnthropicChatClientIT {
@Test
void mapOutputConverter() {
// @formatter:off
Map<String, Object> result = ChatClient.create(chatModel).prompt()
Map<String, Object> result = ChatClient.create(this.chatModel).prompt()
.user(u -> u.text("Provide me a List of {subject}")
.param("subject", "an array of numbers from 1 to 9 under they key name 'numbers'"))
.call()
@@ -153,7 +151,7 @@ class AnthropicChatClientIT {
void beanOutputConverter() {
// @formatter:off
ActorsFilms actorsFilms = ChatClient.create(chatModel).prompt()
ActorsFilms actorsFilms = ChatClient.create(this.chatModel).prompt()
.user("Generate the filmography for a random actor.")
.call()
.entity(ActorsFilms.class);
@@ -167,7 +165,7 @@ class AnthropicChatClientIT {
void beanOutputConverterRecords() {
// @formatter:off
ActorsFilms actorsFilms = ChatClient.create(chatModel).prompt()
ActorsFilms actorsFilms = ChatClient.create(this.chatModel).prompt()
.user("Generate the filmography of 5 movies for Tom Hanks.")
.call()
.entity(ActorsFilms.class);
@@ -184,7 +182,7 @@ class AnthropicChatClientIT {
BeanOutputConverter<ActorsFilms> outputConverter = new BeanOutputConverter<>(ActorsFilms.class);
// @formatter:off
Flux<String> chatResponse = ChatClient.create(chatModel)
Flux<String> chatResponse = ChatClient.create(this.chatModel)
.prompt()
.advisors(new SimpleLoggerAdvisor())
.user(u -> u
@@ -211,7 +209,7 @@ class AnthropicChatClientIT {
void functionCallTest() {
// @formatter:off
String response = ChatClient.create(chatModel).prompt()
String response = ChatClient.create(this.chatModel).prompt()
.user(u -> u.text("What's the weather like in San Francisco, Tokyo, and Paris? Use Celsius."))
.function("getCurrentWeather", "Get the weather in location", new MockWeatherService())
.call()
@@ -227,7 +225,7 @@ class AnthropicChatClientIT {
void defaultFunctionCallTest() {
// @formatter:off
String response = ChatClient.builder(chatModel)
String response = ChatClient.builder(this.chatModel)
.defaultFunction("getCurrentWeather", "Get the weather in location", new MockWeatherService())
.defaultUser(u -> u.text("What's the weather like in San Francisco, Tokyo, and Paris? Use Celsius."))
.build()
@@ -245,7 +243,7 @@ class AnthropicChatClientIT {
void streamFunctionCallTest() {
// @formatter:off
Flux<String> response = ChatClient.create(chatModel).prompt()
Flux<String> response = ChatClient.create(this.chatModel).prompt()
.user("What's the weather like in San Francisco, Tokyo, and Paris? Use Celsius.")
.function("getCurrentWeather", "Get the weather in location", new MockWeatherService())
.stream()
@@ -264,7 +262,7 @@ class AnthropicChatClientIT {
void multiModalityEmbeddedImage(String modelName) throws IOException {
// @formatter:off
String response = ChatClient.create(chatModel).prompt()
String response = ChatClient.create(this.chatModel).prompt()
.options(AnthropicChatOptions.builder().withModel(modelName).build())
.user(u -> u.text("Explain what do you see on this picture?")
.media(MimeTypeUtils.IMAGE_PNG, new ClassPathResource("/test.png")))
@@ -287,7 +285,7 @@ class AnthropicChatClientIT {
URL url = new URL("https://docs.spring.io/spring-ai/reference/1.0.0-SNAPSHOT/_images/multimodal.test.png");
// @formatter:off
String response = ChatClient.create(chatModel).prompt()
String response = ChatClient.create(this.chatModel).prompt()
// TODO consider adding model(...) method to ChatClient as a shortcut to
.options(AnthropicChatOptions.builder().withModel(modelName).build())
.user(u -> u.text("Explain what do you see on this picture?").media(MimeTypeUtils.IMAGE_PNG, url))
@@ -304,7 +302,7 @@ class AnthropicChatClientIT {
void streamingMultiModality() throws IOException {
// @formatter:off
Flux<String> response = ChatClient.create(chatModel).prompt()
Flux<String> response = ChatClient.create(this.chatModel).prompt()
.options(AnthropicChatOptions.builder().withModel(AnthropicApi.ChatModel.CLAUDE_3_5_SONNET)
.build())
.user(u -> u.text("Explain what do you see on this picture?")
@@ -320,4 +318,8 @@ class AnthropicChatClientIT {
assertThat(content).containsAnyOf("bowl", "basket");
}
}
record ActorsFilms(String actor, List<String> movies) {
}
}

View File

@@ -1 +1,17 @@
#
# 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.
#
logging.level.org.springframework.ai.chat.client.advisor=DEBUG

View File

@@ -1,4 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ 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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,13 +13,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.azure.openai;
import java.io.IOException;
import java.util.List;
import com.azure.ai.openai.OpenAIClient;
import com.azure.ai.openai.models.AudioTranscriptionFormat;
import com.azure.ai.openai.models.AudioTranscriptionOptions;
import com.azure.ai.openai.models.AudioTranscriptionTimestampGranularity;
import com.azure.core.http.rest.Response;
import org.springframework.ai.audio.transcription.AudioTranscription;
import org.springframework.ai.audio.transcription.AudioTranscriptionPrompt;
import org.springframework.ai.audio.transcription.AudioTranscriptionResponse;
@@ -35,9 +40,6 @@ import org.springframework.core.io.Resource;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import java.io.IOException;
import java.util.List;
/**
* AzureOpenAI audio transcription client implementation for backed by
* {@link OpenAIClient}. You provide as input the audio file you want to transcribe and
@@ -61,6 +63,15 @@ public class AzureOpenAiAudioTranscriptionModel implements Model<AudioTranscript
this.defaultOptions = options;
}
private static byte[] toBytes(Resource resource) {
try {
return resource.getInputStream().readAllBytes();
}
catch (IOException e) {
throw new IllegalArgumentException("Failed to read resource: " + resource, e);
}
}
public String call(Resource audioResource) {
AudioTranscriptionPrompt transcriptionRequest = new AudioTranscriptionPrompt(audioResource);
return call(transcriptionRequest).getResult().getOutput();
@@ -73,7 +84,7 @@ public class AzureOpenAiAudioTranscriptionModel implements Model<AudioTranscript
AudioTranscriptionFormat responseFormat = audioTranscriptionOptions.getResponseFormat();
if (JSON_FORMATS.contains(responseFormat)) {
var audioTranscription = openAIClient.getAudioTranscription(deploymentOrModelName, FILENAME_MARKER,
var audioTranscription = this.openAIClient.getAudioTranscription(deploymentOrModelName, FILENAME_MARKER,
audioTranscriptionOptions);
List<Word> words = null;
@@ -108,7 +119,7 @@ public class AzureOpenAiAudioTranscriptionModel implements Model<AudioTranscript
return new AudioTranscriptionResponse(transcript, metadata);
}
else {
Response<String> audioTranscription = openAIClient.getAudioTranscriptionTextWithResponse(
Response<String> audioTranscription = this.openAIClient.getAudioTranscriptionTextWithResponse(
deploymentOrModelName, FILENAME_MARKER, audioTranscriptionOptions, null);
String text = audioTranscription.getValue();
AudioTranscription transcript = new AudioTranscription(text);
@@ -119,7 +130,7 @@ public class AzureOpenAiAudioTranscriptionModel implements Model<AudioTranscript
private String getDeploymentName(AudioTranscriptionPrompt audioTranscriptionPrompt) {
var runtimeOptions = audioTranscriptionPrompt.getOptions();
if (defaultOptions != null) {
if (this.defaultOptions != null) {
runtimeOptions = ModelOptionsUtils.merge(runtimeOptions, this.defaultOptions,
AzureOpenAiAudioTranscriptionOptions.class);
}
@@ -189,13 +200,4 @@ public class AzureOpenAiAudioTranscriptionModel implements Model<AudioTranscript
return audioTranscriptionOptions;
}
private static byte[] toBytes(Resource resource) {
try {
return resource.getInputStream().readAllBytes();
}
catch (IOException e) {
throw new IllegalArgumentException("Failed to read resource: " + resource, e);
}
}
}

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,18 +13,20 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.azure.openai;
import java.util.List;
import com.azure.ai.openai.models.AudioTranscriptionFormat;
import com.azure.ai.openai.models.AudioTranscriptionTimestampGranularity;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.ai.audio.transcription.AudioTranscriptionOptions;
import org.springframework.util.Assert;
import java.util.List;
/**
* @author Piotr Olaszewski
*/
@@ -66,6 +68,171 @@ public class AzureOpenAiAudioTranscriptionOptions implements AudioTranscriptionO
return new Builder();
}
@Override
public String getModel() {
return this.model;
}
public void setModel(String model) {
this.model = model;
}
public String getDeploymentName() {
return this.deploymentName;
}
public void setDeploymentName(String deploymentName) {
this.deploymentName = deploymentName;
}
public String getLanguage() {
return this.language;
}
public void setLanguage(String language) {
this.language = language;
}
public String getPrompt() {
return this.prompt;
}
public void setPrompt(String prompt) {
this.prompt = prompt;
}
public Float getTemperature() {
return this.temperature;
}
public void setTemperature(Float temperature) {
this.temperature = temperature;
}
public TranscriptResponseFormat getResponseFormat() {
return this.responseFormat;
}
public void setResponseFormat(TranscriptResponseFormat responseFormat) {
this.responseFormat = responseFormat;
}
public List<GranularityType> getGranularityType() {
return this.granularityType;
}
public void setGranularityType(List<GranularityType> granularityType) {
this.granularityType = granularityType;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((this.model == null) ? 0 : this.model.hashCode());
result = prime * result + ((this.prompt == null) ? 0 : this.prompt.hashCode());
result = prime * result + ((this.language == null) ? 0 : this.language.hashCode());
result = prime * result + ((this.responseFormat == null) ? 0 : this.responseFormat.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
AzureOpenAiAudioTranscriptionOptions other = (AzureOpenAiAudioTranscriptionOptions) obj;
if (this.model == null) {
if (other.model != null)
return false;
}
else if (!this.model.equals(other.model))
return false;
if (this.prompt == null) {
if (other.prompt != null)
return false;
}
else if (!this.prompt.equals(other.prompt))
return false;
if (this.language == null) {
if (other.language != null)
return false;
}
else if (!this.language.equals(other.language))
return false;
if (this.responseFormat == null) {
return other.responseFormat==null;
}
else return this.responseFormat.equals(other.responseFormat);
}
public enum WhisperModel {
// @formatter:off
@JsonProperty("whisper") WHISPER("whisper");
// @formatter:on
public final String value;
WhisperModel(String value) {
this.value = value;
}
public String getValue() {
return this.value;
}
}
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);
public final AudioTranscriptionFormat value;
public final Class<?> responseType;
TranscriptResponseFormat(AudioTranscriptionFormat value, Class<?> responseType) {
this.value = value;
this.responseType = responseType;
}
public AudioTranscriptionFormat getValue() {
return this.value;
}
public Class<?> getResponseType() {
return this.responseType;
}
}
public enum GranularityType {
// @formatter:off
@JsonProperty("word") WORD(AudioTranscriptionTimestampGranularity.WORD),
@JsonProperty("segment") SEGMENT(AudioTranscriptionTimestampGranularity.SEGMENT);
// @formatter:on
public final AudioTranscriptionTimestampGranularity value;
GranularityType(AudioTranscriptionTimestampGranularity value) {
this.value = value;
}
public AudioTranscriptionTimestampGranularity getValue() {
return this.value;
}
}
public static class Builder {
protected AzureOpenAiAudioTranscriptionOptions options;
@@ -114,134 +281,14 @@ public class AzureOpenAiAudioTranscriptionOptions implements AudioTranscriptionO
}
public AzureOpenAiAudioTranscriptionOptions build() {
Assert.hasText(options.model, "model must not be empty");
Assert.notNull(options.responseFormat, "response_format must not be null");
Assert.hasText(this.options.model, "model must not be empty");
Assert.notNull(this.options.responseFormat, "response_format must not be null");
return this.options;
}
}
@Override
public String getModel() {
return this.model;
}
public void setModel(String model) {
this.model = model;
}
public String getDeploymentName() {
return deploymentName;
}
public void setDeploymentName(String deploymentName) {
this.deploymentName = deploymentName;
}
public String getLanguage() {
return this.language;
}
public void setLanguage(String language) {
this.language = language;
}
public String getPrompt() {
return this.prompt;
}
public void setPrompt(String prompt) {
this.prompt = prompt;
}
public Float getTemperature() {
return this.temperature;
}
public void setTemperature(Float temperature) {
this.temperature = temperature;
}
public TranscriptResponseFormat getResponseFormat() {
return this.responseFormat;
}
public void setResponseFormat(TranscriptResponseFormat responseFormat) {
this.responseFormat = responseFormat;
}
public List<GranularityType> getGranularityType() {
return this.granularityType;
}
public void setGranularityType(List<GranularityType> granularityType) {
this.granularityType = granularityType;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((model == null) ? 0 : model.hashCode());
result = prime * result + ((prompt == null) ? 0 : prompt.hashCode());
result = prime * result + ((language == null) ? 0 : language.hashCode());
result = prime * result + ((responseFormat == null) ? 0 : responseFormat.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
AzureOpenAiAudioTranscriptionOptions other = (AzureOpenAiAudioTranscriptionOptions) obj;
if (this.model == null) {
if (other.model != null)
return false;
}
else if (!model.equals(other.model))
return false;
if (this.prompt == null) {
if (other.prompt != null)
return false;
}
else if (!this.prompt.equals(other.prompt))
return false;
if (this.language == null) {
if (other.language != null)
return false;
}
else if (!this.language.equals(other.language))
return false;
if (this.responseFormat == null) {
return other.responseFormat==null;
}
else return this.responseFormat.equals(other.responseFormat);
}
public enum WhisperModel {
// @formatter:off
@JsonProperty("whisper") WHISPER("whisper");
// @formatter:on
public final String value;
WhisperModel(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
/**
* @param language The language of the transcribed text.
* @param duration The duration of the audio in seconds.
@@ -308,51 +355,6 @@ public class AzureOpenAiAudioTranscriptionOptions implements AudioTranscriptionO
@JsonProperty("no_speech_prob") Float noSpeechProb) {
// @formatter:on
}
}
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);
public final AudioTranscriptionFormat value;
public final Class<?> responseType;
TranscriptResponseFormat(AudioTranscriptionFormat value, Class<?> responseType) {
this.value = value;
this.responseType = responseType;
}
public AudioTranscriptionFormat getValue() {
return this.value;
}
public Class<?> getResponseType() {
return this.responseType;
}
}
public enum GranularityType {
// @formatter:off
@JsonProperty("word") WORD(AudioTranscriptionTimestampGranularity.WORD),
@JsonProperty("segment") SEGMENT(AudioTranscriptionTimestampGranularity.SEGMENT);
// @formatter:on
public final AudioTranscriptionTimestampGranularity value;
GranularityType(AudioTranscriptionTimestampGranularity value) {
this.value = value;
}
public AudioTranscriptionTimestampGranularity getValue() {
return this.value;
}
}

View File

@@ -5,7 +5,7 @@
* 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,
@@ -16,13 +16,49 @@
package org.springframework.ai.azure.openai;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import com.azure.ai.openai.OpenAIAsyncClient;
import com.azure.ai.openai.OpenAIClient;
import com.azure.ai.openai.OpenAIClientBuilder;
import com.azure.ai.openai.models.*;
import com.azure.ai.openai.models.ChatChoice;
import com.azure.ai.openai.models.ChatCompletions;
import com.azure.ai.openai.models.ChatCompletionsFunctionToolCall;
import com.azure.ai.openai.models.ChatCompletionsFunctionToolDefinition;
import com.azure.ai.openai.models.ChatCompletionsJsonResponseFormat;
import com.azure.ai.openai.models.ChatCompletionsOptions;
import com.azure.ai.openai.models.ChatCompletionsResponseFormat;
import com.azure.ai.openai.models.ChatCompletionsTextResponseFormat;
import com.azure.ai.openai.models.ChatCompletionsToolCall;
import com.azure.ai.openai.models.ChatCompletionsToolDefinition;
import com.azure.ai.openai.models.ChatMessageContentItem;
import com.azure.ai.openai.models.ChatMessageImageContentItem;
import com.azure.ai.openai.models.ChatMessageImageUrl;
import com.azure.ai.openai.models.ChatMessageTextContentItem;
import com.azure.ai.openai.models.ChatRequestAssistantMessage;
import com.azure.ai.openai.models.ChatRequestMessage;
import com.azure.ai.openai.models.ChatRequestSystemMessage;
import com.azure.ai.openai.models.ChatRequestToolMessage;
import com.azure.ai.openai.models.ChatRequestUserMessage;
import com.azure.ai.openai.models.CompletionsFinishReason;
import com.azure.ai.openai.models.ContentFilterResultsForPrompt;
import com.azure.ai.openai.models.FunctionCall;
import com.azure.ai.openai.models.FunctionDefinition;
import com.azure.core.util.BinaryData;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor;
import reactor.core.publisher.Flux;
import org.springframework.ai.azure.openai.metadata.AzureOpenAiUsage;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
@@ -54,20 +90,6 @@ import org.springframework.ai.observation.conventions.AiProvider;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* {@link ChatModel} implementation for {@literal Microsoft Azure AI} backed by
* {@link OpenAIClient}.
@@ -153,6 +175,19 @@ public class AzureOpenAiChatModel extends AbstractToolCallSupport implements Cha
this.observationRegistry = observationRegistry;
}
public static ChatResponseMetadata from(ChatCompletions chatCompletions, PromptMetadata promptFilterMetadata) {
Assert.notNull(chatCompletions, "Azure OpenAI ChatCompletions must not be null");
String id = chatCompletions.getId();
Usage usage = (chatCompletions.getUsage() != null) ? AzureOpenAiUsage.from(chatCompletions) : new EmptyUsage();
return ChatResponseMetadata.builder()
.withId(id)
.withUsage(usage)
.withModel(chatCompletions.getModel())
.withPromptMetadata(promptFilterMetadata)
.withKeyValue("system-fingerprint", chatCompletions.getSystemFingerprint())
.build();
}
public AzureOpenAiChatOptions getDefaultOptions() {
return AzureOpenAiChatOptions.fromOptions(this.defaultOptions);
}
@@ -302,19 +337,6 @@ public class AzureOpenAiChatModel extends AbstractToolCallSupport implements Cha
return new Generation(assistantMessage, generationMetadata);
}
public static ChatResponseMetadata from(ChatCompletions chatCompletions, PromptMetadata promptFilterMetadata) {
Assert.notNull(chatCompletions, "Azure OpenAI ChatCompletions must not be null");
String id = chatCompletions.getId();
Usage usage = (chatCompletions.getUsage() != null) ? AzureOpenAiUsage.from(chatCompletions) : new EmptyUsage();
return ChatResponseMetadata.builder()
.withId(id)
.withUsage(usage)
.withModel(chatCompletions.getModel())
.withPromptMetadata(promptFilterMetadata)
.withKeyValue("system-fingerprint", chatCompletions.getSystemFingerprint())
.build();
}
/**
* Test access.
*/
@@ -332,8 +354,9 @@ public class AzureOpenAiChatModel extends AbstractToolCallSupport implements Cha
options = this.merge(options, this.defaultOptions);
if (!CollectionUtils.isEmpty(this.defaultOptions.getFunctions()))
if (!CollectionUtils.isEmpty(this.defaultOptions.getFunctions())) {
functionsForThisRequest.addAll(this.defaultOptions.getFunctions());
}
if (prompt.getOptions() != null) {
AzureOpenAiChatOptions updatedRuntimeOptions;
@@ -428,14 +451,16 @@ public class AzureOpenAiChatModel extends AbstractToolCallSupport implements Cha
private String getMediaUrl(Media media) {
Object data = media.getData();
if (data instanceof String dataUrl)
if (data instanceof String dataUrl) {
return dataUrl;
}
else if (data instanceof byte[] dataBytes) {
String base64EncodedData = Base64.getEncoder().encodeToString(dataBytes);
return "data:" + media.getMimeType() + ";base64," + base64EncodedData;
}
else
else {
throw new IllegalArgumentException("Unknown media data type " + data.getClass().getName());
}
}
private ChatGenerationMetadata generateChoiceMetadata(ChatChoice choice) {

View File

@@ -5,7 +5,7 @@
* 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,
@@ -22,18 +22,18 @@ 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.boot.context.properties.NestedConfigurationProperty;
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.boot.context.properties.NestedConfigurationProperty;
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
@@ -206,129 +206,26 @@ public class AzureOpenAiChatOptions implements FunctionCallingOptions, ChatOptio
return new Builder();
}
public static class Builder {
protected AzureOpenAiChatOptions options;
public Builder() {
this.options = new AzureOpenAiChatOptions();
}
public Builder(AzureOpenAiChatOptions options) {
this.options = options;
}
public Builder withDeploymentName(String deploymentName) {
this.options.deploymentName = deploymentName;
return this;
}
public Builder withFrequencyPenalty(Double frequencyPenalty) {
this.options.frequencyPenalty = frequencyPenalty;
return this;
}
public Builder withLogitBias(Map<String, Integer> logitBias) {
this.options.logitBias = logitBias;
return this;
}
public Builder withMaxTokens(Integer maxTokens) {
this.options.maxTokens = maxTokens;
return this;
}
public Builder withN(Integer n) {
this.options.n = n;
return this;
}
public Builder withPresencePenalty(Double presencePenalty) {
this.options.presencePenalty = presencePenalty;
return this;
}
public Builder withStop(List<String> stop) {
this.options.stop = stop;
return this;
}
public Builder withTemperature(Double temperature) {
this.options.temperature = temperature;
return this;
}
public Builder withTopP(Double topP) {
this.options.topP = topP;
return this;
}
public Builder withUser(String user) {
this.options.user = user;
return this;
}
public Builder withFunctionCallbacks(List<FunctionCallback> functionCallbacks) {
this.options.functionCallbacks = functionCallbacks;
return this;
}
public Builder withFunctions(Set<String> functionNames) {
Assert.notNull(functionNames, "Function names must not be null");
this.options.functions = functionNames;
return this;
}
public Builder withFunction(String functionName) {
Assert.hasText(functionName, "Function name must not be empty");
this.options.functions.add(functionName);
return this;
}
public Builder withResponseFormat(AzureOpenAiResponseFormat responseFormat) {
this.options.responseFormat = responseFormat;
return this;
}
public Builder withProxyToolCalls(Boolean proxyToolCalls) {
this.options.proxyToolCalls = proxyToolCalls;
return this;
}
public Builder withSeed(Long seed) {
this.options.seed = seed;
return this;
}
public Builder withLogprobs(Boolean logprobs) {
this.options.logprobs = logprobs;
return this;
}
public Builder withTopLogprobs(Integer topLogprobs) {
this.options.topLogProbs = topLogprobs;
return this;
}
public Builder withEnhancements(AzureChatEnhancementConfiguration enhancements) {
this.options.enhancements = enhancements;
return this;
}
public Builder withToolContext(Map<String, Object> toolContext) {
if (this.options.toolContext == null) {
this.options.toolContext = toolContext;
}
else {
this.options.toolContext.putAll(toolContext);
}
return this;
}
public AzureOpenAiChatOptions build() {
return this.options;
}
public static AzureOpenAiChatOptions fromOptions(AzureOpenAiChatOptions fromOptions) {
return builder().withDeploymentName(fromOptions.getDeploymentName())
.withFrequencyPenalty(fromOptions.getFrequencyPenalty() != null ? fromOptions.getFrequencyPenalty() : null)
.withLogitBias(fromOptions.getLogitBias())
.withMaxTokens(fromOptions.getMaxTokens())
.withN(fromOptions.getN())
.withPresencePenalty(fromOptions.getPresencePenalty() != null ? fromOptions.getPresencePenalty() : null)
.withStop(fromOptions.getStop())
.withTemperature(fromOptions.getTemperature())
.withTopP(fromOptions.getTopP())
.withUser(fromOptions.getUser())
.withFunctionCallbacks(fromOptions.getFunctionCallbacks())
.withFunctions(fromOptions.getFunctions())
.withResponseFormat(fromOptions.getResponseFormat())
.withSeed(fromOptions.getSeed())
.withLogprobs(fromOptions.isLogprobs())
.withTopLogprobs(fromOptions.getTopLogProbs())
.withEnhancements(fromOptions.getEnhancements())
.withToolContext(fromOptions.getToolContext())
.build();
}
@Override
@@ -526,26 +423,129 @@ public class AzureOpenAiChatOptions implements FunctionCallingOptions, ChatOptio
return fromOptions(this);
}
public static AzureOpenAiChatOptions fromOptions(AzureOpenAiChatOptions fromOptions) {
return builder().withDeploymentName(fromOptions.getDeploymentName())
.withFrequencyPenalty(fromOptions.getFrequencyPenalty() != null ? fromOptions.getFrequencyPenalty() : null)
.withLogitBias(fromOptions.getLogitBias())
.withMaxTokens(fromOptions.getMaxTokens())
.withN(fromOptions.getN())
.withPresencePenalty(fromOptions.getPresencePenalty() != null ? fromOptions.getPresencePenalty() : null)
.withStop(fromOptions.getStop())
.withTemperature(fromOptions.getTemperature())
.withTopP(fromOptions.getTopP())
.withUser(fromOptions.getUser())
.withFunctionCallbacks(fromOptions.getFunctionCallbacks())
.withFunctions(fromOptions.getFunctions())
.withResponseFormat(fromOptions.getResponseFormat())
.withSeed(fromOptions.getSeed())
.withLogprobs(fromOptions.isLogprobs())
.withTopLogprobs(fromOptions.getTopLogProbs())
.withEnhancements(fromOptions.getEnhancements())
.withToolContext(fromOptions.getToolContext())
.build();
public static class Builder {
protected AzureOpenAiChatOptions options;
public Builder() {
this.options = new AzureOpenAiChatOptions();
}
public Builder(AzureOpenAiChatOptions options) {
this.options = options;
}
public Builder withDeploymentName(String deploymentName) {
this.options.deploymentName = deploymentName;
return this;
}
public Builder withFrequencyPenalty(Double frequencyPenalty) {
this.options.frequencyPenalty = frequencyPenalty;
return this;
}
public Builder withLogitBias(Map<String, Integer> logitBias) {
this.options.logitBias = logitBias;
return this;
}
public Builder withMaxTokens(Integer maxTokens) {
this.options.maxTokens = maxTokens;
return this;
}
public Builder withN(Integer n) {
this.options.n = n;
return this;
}
public Builder withPresencePenalty(Double presencePenalty) {
this.options.presencePenalty = presencePenalty;
return this;
}
public Builder withStop(List<String> stop) {
this.options.stop = stop;
return this;
}
public Builder withTemperature(Double temperature) {
this.options.temperature = temperature;
return this;
}
public Builder withTopP(Double topP) {
this.options.topP = topP;
return this;
}
public Builder withUser(String user) {
this.options.user = user;
return this;
}
public Builder withFunctionCallbacks(List<FunctionCallback> functionCallbacks) {
this.options.functionCallbacks = functionCallbacks;
return this;
}
public Builder withFunctions(Set<String> functionNames) {
Assert.notNull(functionNames, "Function names must not be null");
this.options.functions = functionNames;
return this;
}
public Builder withFunction(String functionName) {
Assert.hasText(functionName, "Function name must not be empty");
this.options.functions.add(functionName);
return this;
}
public Builder withResponseFormat(AzureOpenAiResponseFormat responseFormat) {
this.options.responseFormat = responseFormat;
return this;
}
public Builder withProxyToolCalls(Boolean proxyToolCalls) {
this.options.proxyToolCalls = proxyToolCalls;
return this;
}
public Builder withSeed(Long seed) {
this.options.seed = seed;
return this;
}
public Builder withLogprobs(Boolean logprobs) {
this.options.logprobs = logprobs;
return this;
}
public Builder withTopLogprobs(Integer topLogprobs) {
this.options.topLogProbs = topLogprobs;
return this;
}
public Builder withEnhancements(AzureChatEnhancementConfiguration enhancements) {
this.options.enhancements = enhancements;
return this;
}
public Builder withToolContext(Map<String, Object> toolContext) {
if (this.options.toolContext == null) {
this.options.toolContext = toolContext;
}
else {
this.options.toolContext.putAll(toolContext);
}
return this;
}
public AzureOpenAiChatOptions build() {
return this.options;
}
}
}

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,17 +13,20 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.azure.openai;
import java.util.ArrayList;
import java.util.List;
import com.azure.ai.openai.OpenAIClient;
import com.azure.ai.openai.models.EmbeddingItem;
import com.azure.ai.openai.models.Embeddings;
import com.azure.ai.openai.models.EmbeddingsOptions;
import io.micrometer.observation.ObservationRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.azure.openai.metadata.AzureOpenAiEmbeddingUsage;
import org.springframework.ai.document.Document;
import org.springframework.ai.document.MetadataMode;
@@ -41,9 +44,6 @@ import org.springframework.ai.observation.conventions.AiProvider;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.List;
/**
* Azure Open AI Embedding Model implementation.
*
@@ -56,14 +56,14 @@ public class AzureOpenAiEmbeddingModel extends AbstractEmbeddingModel {
private static final Logger logger = LoggerFactory.getLogger(AzureOpenAiEmbeddingModel.class);
private static final EmbeddingModelObservationConvention DEFAULT_OBSERVATION_CONVENTION = new DefaultEmbeddingModelObservationConvention();
private final OpenAIClient azureOpenAiClient;
private final AzureOpenAiEmbeddingOptions defaultOptions;
private final MetadataMode metadataMode;
private static final EmbeddingModelObservationConvention DEFAULT_OBSERVATION_CONVENTION = new DefaultEmbeddingModelObservationConvention();
/**
* Observation registry used for instrumentation.
*/

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,11 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.azure.openai;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.ai.embedding.EmbeddingOptions;
/**
@@ -58,6 +60,61 @@ public class AzureOpenAiEmbeddingOptions implements EmbeddingOptions {
return new Builder();
}
@Override
@JsonIgnore
public String getModel() {
return getDeploymentName();
}
@JsonIgnore
public void setModel(String model) {
setDeploymentName(model);
}
public String getUser() {
return this.user;
}
public void setUser(String user) {
this.user = user;
}
public String getDeploymentName() {
return this.deploymentName;
}
public void setDeploymentName(String deploymentName) {
this.deploymentName = deploymentName;
}
public String getInputType() {
return this.inputType;
}
public void setInputType(String inputType) {
this.inputType = inputType;
}
@Override
public Integer getDimensions() {
return this.dimensions;
}
public void setDimensions(Integer dimensions) {
this.dimensions = dimensions;
}
public com.azure.ai.openai.models.EmbeddingsOptions toAzureOptions(List<String> instructions) {
var azureOptions = new com.azure.ai.openai.models.EmbeddingsOptions(instructions);
azureOptions.setModel(this.getDeploymentName());
azureOptions.setUser(this.getUser());
azureOptions.setInputType(this.getInputType());
azureOptions.setDimensions(this.getDimensions());
return azureOptions;
}
public static class Builder {
private final AzureOpenAiEmbeddingOptions options = new AzureOpenAiEmbeddingOptions();
@@ -125,59 +182,4 @@ public class AzureOpenAiEmbeddingOptions implements EmbeddingOptions {
}
@Override
@JsonIgnore
public String getModel() {
return getDeploymentName();
}
@JsonIgnore
public void setModel(String model) {
setDeploymentName(model);
}
public String getUser() {
return this.user;
}
public void setUser(String user) {
this.user = user;
}
public String getDeploymentName() {
return this.deploymentName;
}
public void setDeploymentName(String deploymentName) {
this.deploymentName = deploymentName;
}
public String getInputType() {
return this.inputType;
}
public void setInputType(String inputType) {
this.inputType = inputType;
}
@Override
public Integer getDimensions() {
return this.dimensions;
}
public void setDimensions(Integer dimensions) {
this.dimensions = dimensions;
}
public com.azure.ai.openai.models.EmbeddingsOptions toAzureOptions(List<String> instructions) {
var azureOptions = new com.azure.ai.openai.models.EmbeddingsOptions(instructions);
azureOptions.setModel(this.getDeploymentName());
azureOptions.setUser(this.getUser());
azureOptions.setInputType(this.getInputType());
azureOptions.setDimensions(this.getDimensions());
return azureOptions;
}
}

View File

@@ -1,5 +1,23 @@
/*
* 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.azure.openai;
import java.util.List;
import com.azure.ai.openai.OpenAIClient;
import com.azure.ai.openai.models.ImageGenerationOptions;
import com.azure.ai.openai.models.ImageGenerationQuality;
@@ -13,6 +31,7 @@ import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.json.JsonMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.azure.openai.metadata.AzureOpenAiImageGenerationMetadata;
import org.springframework.ai.azure.openai.metadata.AzureOpenAiImageResponseMetadata;
import org.springframework.ai.image.Image;
@@ -25,8 +44,6 @@ import org.springframework.ai.model.ModelOptionsUtils;
import org.springframework.ai.util.JacksonUtils;
import org.springframework.util.Assert;
import java.util.List;
import static java.lang.String.format;
/**
@@ -68,22 +85,22 @@ public class AzureOpenAiImageModel implements ImageModel {
}
public AzureOpenAiImageOptions getDefaultOptions() {
return defaultOptions;
return this.defaultOptions;
}
@Override
public ImageResponse call(ImagePrompt imagePrompt) {
ImageGenerationOptions imageGenerationOptions = toOpenAiImageOptions(imagePrompt);
String deploymentOrModelName = getDeploymentName(imagePrompt);
if (logger.isTraceEnabled()) {
logger.trace("Azure ImageGenerationOptions call {} with the following options : {} ", deploymentOrModelName,
toPrettyJson(imageGenerationOptions));
if (this.logger.isTraceEnabled()) {
this.logger.trace("Azure ImageGenerationOptions call {} with the following options : {} ",
deploymentOrModelName, toPrettyJson(imageGenerationOptions));
}
var images = openAIClient.getImageGenerations(deploymentOrModelName, imageGenerationOptions);
var images = this.openAIClient.getImageGenerations(deploymentOrModelName, imageGenerationOptions);
if (logger.isTraceEnabled()) {
logger.trace("Azure ImageGenerations: {}", toPrettyJson(images));
if (this.logger.isTraceEnabled()) {
this.logger.trace("Azure ImageGenerations: {}", toPrettyJson(images));
}
List<ImageGeneration> imageGenerations = images.getData().stream().map(entry -> {

View File

@@ -1,12 +1,28 @@
/*
* 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.azure.openai;
import java.util.Objects;
import com.fasterxml.jackson.annotation.JsonInclude;
import org.springframework.ai.image.ImageOptions;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.ai.image.ImageOptions;
/**
* The configuration information for a image generation request.
*
@@ -89,9 +105,13 @@ public class AzureOpenAiImageOptions implements ImageOptions {
@JsonProperty("user")
private String user;
public static Builder builder() {
return new Builder();
}
@Override
public Integer getN() {
return n;
return this.n;
}
public void setN(Integer n) {
@@ -100,7 +120,7 @@ public class AzureOpenAiImageOptions implements ImageOptions {
@Override
public String getModel() {
return model;
return this.model;
}
public void setModel(String model) {
@@ -109,7 +129,7 @@ public class AzureOpenAiImageOptions implements ImageOptions {
@Override
public Integer getWidth() {
return width;
return this.width;
}
public void setWidth(Integer width) {
@@ -119,7 +139,7 @@ public class AzureOpenAiImageOptions implements ImageOptions {
@Override
public Integer getHeight() {
return height;
return this.height;
}
public void setHeight(Integer height) {
@@ -129,7 +149,7 @@ public class AzureOpenAiImageOptions implements ImageOptions {
@Override
public String getResponseFormat() {
return responseFormat;
return this.responseFormat;
}
public void setResponseFormat(String responseFormat) {
@@ -148,7 +168,7 @@ public class AzureOpenAiImageOptions implements ImageOptions {
}
public String getUser() {
return user;
return this.user;
}
public void setUser(String user) {
@@ -156,7 +176,7 @@ public class AzureOpenAiImageOptions implements ImageOptions {
}
public String getQuality() {
return quality;
return this.quality;
}
public void setQuality(String quality) {
@@ -165,7 +185,7 @@ public class AzureOpenAiImageOptions implements ImageOptions {
@Override
public String getStyle() {
return style;
return this.style;
}
public void setStyle(String style) {
@@ -173,95 +193,40 @@ public class AzureOpenAiImageOptions implements ImageOptions {
}
public String getDeploymentName() {
return deploymentName;
return this.deploymentName;
}
public void setDeploymentName(String deploymentName) {
this.deploymentName = deploymentName;
}
public static Builder builder() {
return new Builder();
}
@Override
public boolean equals(Object o) {
if (this == o)
if (this == o) {
return true;
if (!(o instanceof AzureOpenAiImageOptions that))
}
if (!(o instanceof AzureOpenAiImageOptions that)) {
return false;
return Objects.equals(n, that.n) && Objects.equals(model, that.model)
&& Objects.equals(deploymentName, that.deploymentName) && Objects.equals(width, that.width)
&& Objects.equals(height, that.height) && Objects.equals(quality, that.quality)
&& Objects.equals(responseFormat, that.responseFormat) && Objects.equals(size, that.size)
&& Objects.equals(style, that.style) && Objects.equals(user, that.user);
}
return Objects.equals(this.n, that.n) && Objects.equals(this.model, that.model)
&& Objects.equals(this.deploymentName, that.deploymentName) && Objects.equals(this.width, that.width)
&& Objects.equals(this.height, that.height) && Objects.equals(this.quality, that.quality)
&& Objects.equals(this.responseFormat, that.responseFormat) && Objects.equals(this.size, that.size)
&& Objects.equals(this.style, that.style) && Objects.equals(this.user, that.user);
}
@Override
public int hashCode() {
return Objects.hash(n, model, deploymentName, width, height, quality, responseFormat, size, style, user);
return Objects.hash(this.n, this.model, this.deploymentName, this.width, this.height, this.quality,
this.responseFormat, this.size, this.style, this.user);
}
@Override
public String toString() {
return "AzureOpenAiImageOptions{" + "n=" + n + ", model='" + model + '\'' + ", deploymentName='"
+ deploymentName + '\'' + ", width=" + width + ", height=" + height + ", quality='" + quality + '\''
+ ", responseFormat='" + responseFormat + '\'' + ", size='" + size + '\'' + ", style='" + style + '\''
+ ", user='" + user + '\'' + '}';
}
public static class Builder {
private final AzureOpenAiImageOptions options;
private Builder() {
this.options = new AzureOpenAiImageOptions();
}
public Builder withN(Integer n) {
options.setN(n);
return this;
}
public Builder withModel(String model) {
options.setModel(model);
return this;
}
public Builder withDeploymentName(String deploymentName) {
options.setDeploymentName(deploymentName);
return this;
}
public Builder withResponseFormat(String responseFormat) {
options.setResponseFormat(responseFormat);
return this;
}
public Builder withWidth(Integer width) {
options.setWidth(width);
return this;
}
public Builder withHeight(Integer height) {
options.setHeight(height);
return this;
}
public Builder withUser(String user) {
options.setUser(user);
return this;
}
public AzureOpenAiImageOptions build() {
return options;
}
public Builder withStyle(String style) {
options.setStyle(style);
return this;
}
return "AzureOpenAiImageOptions{" + "n=" + this.n + ", model='" + this.model + '\'' + ", deploymentName='"
+ this.deploymentName + '\'' + ", width=" + this.width + ", height=" + this.height + ", quality='"
+ this.quality + '\'' + ", responseFormat='" + this.responseFormat + '\'' + ", size='" + this.size
+ '\'' + ", style='" + this.style + '\'' + ", user='" + this.user + '\'' + '}';
}
public enum ImageModel {
@@ -290,4 +255,58 @@ public class AzureOpenAiImageOptions implements ImageOptions {
}
public static class Builder {
private final AzureOpenAiImageOptions options;
private Builder() {
this.options = new AzureOpenAiImageOptions();
}
public Builder withN(Integer n) {
this.options.setN(n);
return this;
}
public Builder withModel(String model) {
this.options.setModel(model);
return this;
}
public Builder withDeploymentName(String deploymentName) {
this.options.setDeploymentName(deploymentName);
return this;
}
public Builder withResponseFormat(String responseFormat) {
this.options.setResponseFormat(responseFormat);
return this;
}
public Builder withWidth(Integer width) {
this.options.setWidth(width);
return this;
}
public Builder withHeight(Integer height) {
this.options.setHeight(height);
return this;
}
public Builder withUser(String user) {
this.options.setUser(user);
return this;
}
public AzureOpenAiImageOptions build() {
return this.options;
}
public Builder withStyle(String style) {
this.options.setStyle(style);
return this;
}
}
}

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.azure.openai;
/**

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.azure.openai;
import java.lang.reflect.Constructor;
@@ -49,6 +50,15 @@ import org.springframework.util.CollectionUtils;
*/
public class MergeUtils {
private static final Class<?>[] CHAT_COMPLETIONS_CONSTRUCTOR_ARG_TYPES = new Class<?>[] { String.class,
OffsetDateTime.class, List.class, CompletionsUsage.class };
private static final Class<?>[] chatChoiceConstructorArgumentTypes = new Class<?>[] {
ChatChoiceLogProbabilityInfo.class, int.class, CompletionsFinishReason.class };
private static final Class<?>[] chatResponseMessageConstructorArgumentTypes = new Class<?>[] { ChatRole.class,
String.class };
/**
* Create a new instance of the given class using the constructor at the given index.
* Can be used to create instances with private constructors.
@@ -106,9 +116,6 @@ public class MergeUtils {
return chatCompletionsInstance;
}
private static final Class<?>[] CHAT_COMPLETIONS_CONSTRUCTOR_ARG_TYPES = new Class<?>[] { String.class,
OffsetDateTime.class, List.class, CompletionsUsage.class };
/**
* Merge two ChatCompletions instances into a single ChatCompletions instance.
* @param left the left ChatCompletions instance.
@@ -158,9 +165,6 @@ public class MergeUtils {
return instance;
}
private static final Class<?>[] chatChoiceConstructorArgumentTypes = new Class<?>[] {
ChatChoiceLogProbabilityInfo.class, int.class, CompletionsFinishReason.class };
/**
* Merge two ChatChoice instances into a single ChatChoice instance.
* @param left the left ChatChoice instance to merge.
@@ -211,9 +215,6 @@ public class MergeUtils {
return instance;
}
private static final Class<?>[] chatResponseMessageConstructorArgumentTypes = new Class<?>[] { ChatRole.class,
String.class };
/**
* Merge two ChatResponseMessage instances into a single ChatResponseMessage instance.
* @param left the left ChatResponseMessage instance to merge.

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.azure.openai.aot;
import com.azure.ai.openai.OpenAIAsyncClient;

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.azure.openai.metadata;
import org.springframework.ai.audio.transcription.AudioTranscriptionResponseMetadata;
@@ -26,10 +27,14 @@ import org.springframework.util.Assert;
*/
public class AzureOpenAiAudioTranscriptionResponseMetadata extends AudioTranscriptionResponseMetadata {
public static final AzureOpenAiAudioTranscriptionResponseMetadata NULL = new AzureOpenAiAudioTranscriptionResponseMetadata() {
};
protected static final String AI_METADATA_STRING = "{ @type: %1$s }";
public static final AzureOpenAiAudioTranscriptionResponseMetadata NULL = new AzureOpenAiAudioTranscriptionResponseMetadata() {
};
protected AzureOpenAiAudioTranscriptionResponseMetadata() {
}
public static AzureOpenAiAudioTranscriptionResponseMetadata from(
AzureOpenAiAudioTranscriptionOptions.StructuredResponse result) {
@@ -42,9 +47,6 @@ public class AzureOpenAiAudioTranscriptionResponseMetadata extends AudioTranscri
return new AzureOpenAiAudioTranscriptionResponseMetadata();
}
protected AzureOpenAiAudioTranscriptionResponseMetadata() {
}
@Override
public String toString() {
return AI_METADATA_STRING.formatted(getClass().getName());

View File

@@ -1,11 +1,11 @@
/*
* Copyright 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,9 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.azure.openai.metadata;
import com.azure.ai.openai.models.EmbeddingsUsage;
import org.springframework.ai.chat.metadata.Usage;
import org.springframework.util.Assert;
@@ -27,11 +29,6 @@ import org.springframework.util.Assert;
*/
public class AzureOpenAiEmbeddingUsage implements Usage {
public static AzureOpenAiEmbeddingUsage from(EmbeddingsUsage usage) {
Assert.notNull(usage, "EmbeddingsUsage must not be null");
return new AzureOpenAiEmbeddingUsage(usage);
}
private final EmbeddingsUsage usage;
public AzureOpenAiEmbeddingUsage(EmbeddingsUsage usage) {
@@ -39,6 +36,11 @@ public class AzureOpenAiEmbeddingUsage implements Usage {
this.usage = usage;
}
public static AzureOpenAiEmbeddingUsage from(EmbeddingsUsage usage) {
Assert.notNull(usage, "EmbeddingsUsage must not be null");
return new AzureOpenAiEmbeddingUsage(usage);
}
protected EmbeddingsUsage getUsage() {
return this.usage;
}

View File

@@ -1,9 +1,25 @@
/*
* 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.azure.openai.metadata;
import org.springframework.ai.image.ImageGenerationMetadata;
import java.util.Objects;
import org.springframework.ai.image.ImageGenerationMetadata;
/**
* Represents the metadata for image generation using Azure OpenAI.
*
@@ -19,25 +35,27 @@ public class AzureOpenAiImageGenerationMetadata implements ImageGenerationMetada
}
public String getRevisedPrompt() {
return revisedPrompt;
return this.revisedPrompt;
}
public String toString() {
return "AzureOpenAiImageGenerationMetadata{" + "revisedPrompt='" + revisedPrompt + '\'' + '}';
return "AzureOpenAiImageGenerationMetadata{" + "revisedPrompt='" + this.revisedPrompt + '\'' + '}';
}
@Override
public boolean equals(Object o) {
if (this == o)
if (this == o) {
return true;
if (!(o instanceof AzureOpenAiImageGenerationMetadata that))
}
if (!(o instanceof AzureOpenAiImageGenerationMetadata that)) {
return false;
return Objects.equals(revisedPrompt, that.revisedPrompt);
}
return Objects.equals(this.revisedPrompt, that.revisedPrompt);
}
@Override
public int hashCode() {
return Objects.hash(revisedPrompt);
return Objects.hash(this.revisedPrompt);
}
}

View File

@@ -1,13 +1,28 @@
/*
* 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.azure.openai.metadata;
import com.azure.ai.openai.models.ImageGenerations;
import org.springframework.ai.image.ImageResponseMetadata;
import org.springframework.ai.model.MutableResponseMetadata;
import org.springframework.util.Assert;
import java.util.HashMap;
import java.util.Objects;
import com.azure.ai.openai.models.ImageGenerations;
import org.springframework.ai.image.ImageResponseMetadata;
import org.springframework.util.Assert;
/**
* Represents metadata associated with an image response from the Azure OpenAI image
* model. It provides additional information about the generative response from the Azure
@@ -20,15 +35,15 @@ public class AzureOpenAiImageResponseMetadata extends ImageResponseMetadata {
private final Long created;
protected AzureOpenAiImageResponseMetadata(Long created) {
this.created = created;
}
public static AzureOpenAiImageResponseMetadata from(ImageGenerations openAiImageResponse) {
Assert.notNull(openAiImageResponse, "OpenAiImageResponse must not be null");
return new AzureOpenAiImageResponseMetadata(openAiImageResponse.getCreatedAt().toEpochSecond());
}
protected AzureOpenAiImageResponseMetadata(Long created) {
this.created = created;
}
@Override
public Long getCreated() {
return this.created;
@@ -36,21 +51,23 @@ public class AzureOpenAiImageResponseMetadata extends ImageResponseMetadata {
@Override
public String toString() {
return "AzureOpenAiImageResponseMetadata{" + "created=" + created + '}';
return "AzureOpenAiImageResponseMetadata{" + "created=" + this.created + '}';
}
@Override
public boolean equals(Object o) {
if (this == o)
if (this == o) {
return true;
if (!(o instanceof AzureOpenAiImageResponseMetadata that))
}
if (!(o instanceof AzureOpenAiImageResponseMetadata that)) {
return false;
return Objects.equals(created, that.created);
}
return Objects.equals(this.created, that.created);
}
@Override
public int hashCode() {
return Objects.hash(created);
return Objects.hash(this.created);
}
}

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.azure.openai.metadata;
import com.azure.ai.openai.models.ChatCompletions;
@@ -30,6 +31,13 @@ import org.springframework.util.Assert;
*/
public class AzureOpenAiUsage implements Usage {
private final CompletionsUsage usage;
public AzureOpenAiUsage(CompletionsUsage usage) {
Assert.notNull(usage, "CompletionsUsage must not be null");
this.usage = usage;
}
public static AzureOpenAiUsage from(ChatCompletions chatCompletions) {
Assert.notNull(chatCompletions, "ChatCompletions must not be null");
return from(chatCompletions.getUsage());
@@ -39,13 +47,6 @@ public class AzureOpenAiUsage implements Usage {
return new AzureOpenAiUsage(usage);
}
private final CompletionsUsage usage;
public AzureOpenAiUsage(CompletionsUsage usage) {
Assert.notNull(usage, "CompletionsUsage must not be null");
this.usage = usage;
}
protected CompletionsUsage getUsage() {
return this.usage;
}

View File

@@ -5,7 +5,7 @@
* 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,
@@ -16,10 +16,12 @@
package org.springframework.ai.azure.openai;
import com.azure.ai.openai.OpenAIClient;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import com.azure.ai.openai.OpenAIClientBuilder;
import com.azure.ai.openai.models.AzureChatEnhancementConfiguration;
import com.azure.ai.openai.models.AzureChatOCREnhancementConfiguration;
import com.azure.ai.openai.models.ChatCompletionsJsonResponseFormat;
import com.azure.ai.openai.models.ChatCompletionsTextResponseFormat;
import org.junit.jupiter.api.Test;
@@ -30,10 +32,6 @@ import org.mockito.Mockito;
import org.springframework.ai.chat.prompt.Prompt;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
/**
@@ -42,6 +40,11 @@ import static org.assertj.core.api.Assertions.assertThat;
*/
public class AzureChatCompletionsOptionsTests {
private static Stream<Arguments> providePresencePenaltyAndFrequencyPenaltyTest() {
return Stream.of(Arguments.of(0.0, 0.0), Arguments.of(0.0, 1.0), Arguments.of(1.0, 0.0), Arguments.of(1.0, 1.0),
Arguments.of(1.0, null), Arguments.of(null, 1.0), Arguments.of(null, null));
}
@Test
public void createRequestWithChatOptions() {
@@ -132,11 +135,6 @@ public class AzureChatCompletionsOptionsTests {
assertThat(requestOptions.getResponseFormat()).isInstanceOf(ChatCompletionsJsonResponseFormat.class);
}
private static Stream<Arguments> providePresencePenaltyAndFrequencyPenaltyTest() {
return Stream.of(Arguments.of(0.0, 0.0), Arguments.of(0.0, 1.0), Arguments.of(1.0, 0.0), Arguments.of(1.0, 1.0),
Arguments.of(1.0, null), Arguments.of(null, 1.0), Arguments.of(null, null));
}
@ParameterizedTest
@MethodSource("providePresencePenaltyAndFrequencyPenaltyTest")
public void createChatOptionsWithPresencePenaltyAndFrequencyPenalty(Double presencePenalty,

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.azure.openai;
import java.util.List;

View File

@@ -1,3 +1,19 @@
/*
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.azure.openai;
import com.azure.ai.openai.OpenAIClient;
@@ -6,6 +22,7 @@ import com.azure.ai.openai.OpenAIServiceVersion;
import com.azure.core.credential.AzureKeyCredential;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.springframework.ai.audio.transcription.AudioTranscriptionPrompt;
import org.springframework.ai.audio.transcription.AudioTranscriptionResponse;
import org.springframework.beans.factory.annotation.Autowired;
@@ -38,8 +55,9 @@ class AzureOpenAiAudioTranscriptionModelIT {
.withResponseFormat(AzureOpenAiAudioTranscriptionOptions.TranscriptResponseFormat.TEXT)
.withTemperature(0f)
.build();
AudioTranscriptionPrompt transcriptionRequest = new AudioTranscriptionPrompt(audioFile, transcriptionOptions);
AudioTranscriptionResponse response = transcriptionModel.call(transcriptionRequest);
AudioTranscriptionPrompt transcriptionRequest = new AudioTranscriptionPrompt(this.audioFile,
transcriptionOptions);
AudioTranscriptionResponse response = this.transcriptionModel.call(transcriptionRequest);
assertThat(response.getResults()).hasSize(1);
assertThat(response.getResults().get(0).getOutput().toLowerCase().contains("fellow")).isTrue();
}
@@ -54,8 +72,9 @@ class AzureOpenAiAudioTranscriptionModelIT {
.withTemperature(0f)
.withResponseFormat(responseFormat)
.build();
AudioTranscriptionPrompt transcriptionRequest = new AudioTranscriptionPrompt(audioFile, transcriptionOptions);
AudioTranscriptionResponse response = transcriptionModel.call(transcriptionRequest);
AudioTranscriptionPrompt transcriptionRequest = new AudioTranscriptionPrompt(this.audioFile,
transcriptionOptions);
AudioTranscriptionResponse response = this.transcriptionModel.call(transcriptionRequest);
assertThat(response.getResults()).hasSize(1);
assertThat(response.getResults().get(0).getOutput().toLowerCase().contains("fellow")).isTrue();
}

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -16,15 +16,17 @@
package org.springframework.ai.azure.openai;
import static com.azure.core.http.policy.HttpLogDetailLevel.BODY_AND_HEADERS;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import com.azure.ai.openai.OpenAIClientBuilder;
import com.azure.ai.openai.OpenAIServiceVersion;
import com.azure.core.credential.AzureKeyCredential;
import com.azure.core.http.policy.HttpLogOptions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import reactor.core.publisher.Flux;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
@@ -35,13 +37,10 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import com.azure.ai.openai.OpenAIClientBuilder;
import com.azure.ai.openai.OpenAIServiceVersion;
import com.azure.core.credential.AzureKeyCredential;
import com.azure.core.http.policy.HttpLogOptions;
import org.springframework.core.io.Resource;
import reactor.core.publisher.Flux;
import static com.azure.core.http.policy.HttpLogDetailLevel.BODY_AND_HEADERS;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Soby Chacko
@@ -57,16 +56,13 @@ public class AzureOpenAiChatClientIT {
@Value("classpath:/prompts/system-message.st")
private Resource systemTextResource;
record ActorsFilms(String actor, List<String> movies) {
}
@Test
void call() {
// @formatter:off
ChatResponse response = chatClient.prompt()
ChatResponse response = this.chatClient.prompt()
.advisors(new SimpleLoggerAdvisor())
.system(s -> s.text(systemTextResource)
.system(s -> s.text(this.systemTextResource)
.param("name", "Bob")
.param("voice", "pirate"))
.user("Tell me about 3 famous pirates from the Golden Age of Piracy and what they did")
@@ -84,7 +80,7 @@ public class AzureOpenAiChatClientIT {
BeanOutputConverter<ActorsFilms> outputConverter = new BeanOutputConverter<>(ActorsFilms.class);
// @formatter:off
Flux<ChatResponse> chatResponse = chatClient
Flux<ChatResponse> chatResponse = this.chatClient
.prompt()
.advisors(new SimpleLoggerAdvisor())
.user(u -> u
@@ -117,12 +113,12 @@ public class AzureOpenAiChatClientIT {
+ "List them with a numerical index. Do not use any abbreviations in state or capitals.";
// Imperative call
String rawDataFromImperativeCall = chatClient.prompt(prompt).call().content();
String rawDataFromImperativeCall = this.chatClient.prompt(prompt).call().content();
String imperativeStatesData = extractStatesData(rawDataFromImperativeCall);
String formattedImperativeResponse = formatResponse(imperativeStatesData);
// Streaming call
String stitchedResponseFromStream = chatClient.prompt(prompt)
String stitchedResponseFromStream = this.chatClient.prompt(prompt)
.stream()
.content()
.collectList()
@@ -150,6 +146,10 @@ public class AzureOpenAiChatClientIT {
return String.join("\n", Arrays.stream(response.split("\n")).map(String::strip).toArray(String[]::new));
}
record ActorsFilms(String actor, List<String> movies) {
}
@SpringBootConfiguration
public static class TestConfiguration {

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,8 +13,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.azure.openai;
import java.io.IOException;
import java.net.URL;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import com.azure.ai.openai.OpenAIClientBuilder;
import com.azure.ai.openai.OpenAIServiceVersion;
import com.azure.core.credential.AzureKeyCredential;
@@ -23,6 +32,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
@@ -44,14 +54,6 @@ import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.util.MimeTypeUtils;
import java.io.IOException;
import java.net.URL;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import static com.azure.core.http.policy.HttpLogDetailLevel.BODY_AND_HEADERS;
import static org.assertj.core.api.Assertions.assertThat;
@@ -77,7 +79,7 @@ class AzureOpenAiChatModelIT {
UserMessage userMessage = new UserMessage("Generate the names of 5 famous pirates.");
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");
}
@@ -96,12 +98,12 @@ class AzureOpenAiChatModelIT {
Prompt prompt = new Prompt(List.of(userMessage, systemMessage));
ChatResponse response = chatModel.call(prompt);
ChatResponse response = this.chatModel.call(prompt);
assertThat(response.getResult().getOutput().getContent()).containsAnyOf("Blackbeard");
var promptWithMessageHistory = new Prompt(List.of(new UserMessage("Dummy"), response.getResult().getOutput(),
new UserMessage("Repeat the last assistant message.")));
response = chatModel.call(promptWithMessageHistory);
response = this.chatModel.call(promptWithMessageHistory);
System.out.println(response.getResult().getOutput().getContent());
assertThat(response.getResult().getOutput().getContent()).containsAnyOf("Blackbeard");
@@ -120,7 +122,7 @@ class AzureOpenAiChatModelIT {
PromptTemplate promptTemplate = new PromptTemplate(template,
Map.of("subject", "ice cream flavors", "format", format));
Prompt prompt = new Prompt(promptTemplate.createMessage());
Generation generation = chatModel.call(prompt).getResult();
Generation generation = this.chatModel.call(prompt).getResult();
List<String> list = outputConverter.convert(generation.getOutput().getContent());
assertThat(list).hasSize(5);
@@ -139,7 +141,7 @@ class AzureOpenAiChatModelIT {
PromptTemplate promptTemplate = new PromptTemplate(template,
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));
@@ -158,7 +160,7 @@ class AzureOpenAiChatModelIT {
""";
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();
ActorsFilms actorsFilms = outputConverter.convert(generation.getOutput().getContent());
assertThat(actorsFilms.actor()).isNotNull();
@@ -176,7 +178,7 @@ class AzureOpenAiChatModelIT {
""";
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());
logger.info("" + actorsFilms);
@@ -197,7 +199,7 @@ class AzureOpenAiChatModelIT {
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()
@@ -221,7 +223,7 @@ class AzureOpenAiChatModelIT {
URL url = new URL("https://docs.spring.io/spring-ai/reference/_images/multimodal.test.png");
// @formatter:off
String response = ChatClient.create(chatModel).prompt()
String response = ChatClient.create(this.chatModel).prompt()
.options(AzureOpenAiChatOptions.builder().withDeploymentName("gpt-4o").build())
.user(u -> u.text("Explain what do you see on this picture?").media(MimeTypeUtils.IMAGE_PNG, url))
.call()
@@ -239,7 +241,7 @@ class AzureOpenAiChatModelIT {
Resource resource = new ClassPathResource("multimodality/multimodal.test.png");
// @formatter:off
String response = ChatClient.create(chatModel).prompt()
String response = ChatClient.create(this.chatModel).prompt()
.options(AzureOpenAiChatOptions.builder().withDeploymentName("gpt-4o").build())
.user(u -> u.text("Explain what do you see on this picture?").media(MimeTypeUtils.IMAGE_PNG, resource))
.call()
@@ -252,9 +254,11 @@ class AzureOpenAiChatModelIT {
}
record ActorsFilms(String actor, List<String> movies) {
}
record ActorsFilmsRecord(String actor, List<String> movies) {
}
@SpringBootConfiguration

View File

@@ -5,7 +5,7 @@
* 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,17 +13,22 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.azure.openai;
import static com.azure.core.http.policy.HttpLogDetailLevel.BODY_AND_HEADERS;
import static org.assertj.core.api.Assertions.assertThat;
package org.springframework.ai.azure.openai;
import java.util.List;
import java.util.stream.Collectors;
import com.azure.ai.openai.OpenAIClientBuilder;
import com.azure.ai.openai.OpenAIServiceVersion;
import com.azure.core.credential.AzureKeyCredential;
import com.azure.core.http.policy.HttpLogOptions;
import io.micrometer.observation.tck.TestObservationRegistry;
import io.micrometer.observation.tck.TestObservationRegistryAssert;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import reactor.core.publisher.Flux;
import org.springframework.ai.chat.metadata.ChatResponseMetadata;
import org.springframework.ai.chat.model.ChatResponse;
@@ -37,13 +42,8 @@ import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import com.azure.ai.openai.OpenAIClientBuilder;
import com.azure.ai.openai.OpenAIServiceVersion;
import com.azure.core.credential.AzureKeyCredential;
import com.azure.core.http.policy.HttpLogOptions;
import io.micrometer.observation.tck.TestObservationRegistry;
import io.micrometer.observation.tck.TestObservationRegistryAssert;
import reactor.core.publisher.Flux;
import static com.azure.core.http.policy.HttpLogDetailLevel.BODY_AND_HEADERS;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Soby Chacko
@@ -54,14 +54,14 @@ import reactor.core.publisher.Flux;
class AzureOpenAiChatModelObservationIT {
@Autowired
private AzureOpenAiChatModel chatModel;
TestObservationRegistry observationRegistry;
@Autowired
TestObservationRegistry observationRegistry;
private AzureOpenAiChatModel chatModel;
@BeforeEach
void beforeEach() {
observationRegistry.clear();
this.observationRegistry.clear();
}
@Test
@@ -78,7 +78,7 @@ class AzureOpenAiChatModelObservationIT {
Prompt prompt = new Prompt("Why does a raven look like a desk?", options);
ChatResponse chatResponse = chatModel.call(prompt);
ChatResponse chatResponse = this.chatModel.call(prompt);
assertThat(chatResponse.getResult().getOutput().getContent()).isNotEmpty();
ChatResponseMetadata responseMetadata = chatResponse.getMetadata();
@@ -102,7 +102,7 @@ class AzureOpenAiChatModelObservationIT {
Prompt prompt = new Prompt("Why does a raven look like a desk?", options);
Flux<ChatResponse> chatResponseFlux = chatModel.stream(prompt);
Flux<ChatResponse> chatResponseFlux = this.chatModel.stream(prompt);
List<ChatResponse> responses = chatResponseFlux.collectList().block();
assertThat(responses).isNotEmpty();
assertThat(responses).hasSizeGreaterThan(10);
@@ -123,7 +123,7 @@ class AzureOpenAiChatModelObservationIT {
private void validate(ChatResponseMetadata responseMetadata, boolean checkModel) {
TestObservationRegistryAssert.That that = TestObservationRegistryAssert.assertThat(observationRegistry)
TestObservationRegistryAssert.That that = TestObservationRegistryAssert.assertThat(this.observationRegistry)
.doesNotHaveAnyRemainingCurrentObservation()
.hasObservationWithNameEqualTo(DefaultChatModelObservationConvention.DEFAULT_NAME);

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.azure.openai;
import java.util.List;
@@ -22,6 +23,7 @@ import com.azure.ai.openai.OpenAIClientBuilder;
import com.azure.core.credential.AzureKeyCredential;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.springframework.ai.document.MetadataMode;
import org.springframework.ai.embedding.EmbeddingResponse;
import org.springframework.beans.factory.annotation.Autowired;
@@ -41,18 +43,18 @@ class AzureOpenAiEmbeddingModelIT {
@Test
void singleEmbedding() {
assertThat(embeddingModel).isNotNull();
EmbeddingResponse embeddingResponse = embeddingModel.embedForResponse(List.of("Hello World"));
assertThat(this.embeddingModel).isNotNull();
EmbeddingResponse embeddingResponse = this.embeddingModel.embedForResponse(List.of("Hello World"));
assertThat(embeddingResponse.getResults()).hasSize(1);
assertThat(embeddingResponse.getResults().get(0).getOutput()).isNotEmpty();
System.out.println(embeddingModel.dimensions());
assertThat(embeddingModel.dimensions()).isEqualTo(1536);
System.out.println(this.embeddingModel.dimensions());
assertThat(this.embeddingModel.dimensions()).isEqualTo(1536);
}
@Test
void batchEmbedding() {
assertThat(embeddingModel).isNotNull();
EmbeddingResponse embeddingResponse = embeddingModel
assertThat(this.embeddingModel).isNotNull();
EmbeddingResponse embeddingResponse = this.embeddingModel
.embedForResponse(List.of("Hello World", "World is big and salvation is near"));
assertThat(embeddingResponse.getResults()).hasSize(2);
assertThat(embeddingResponse.getResults().get(0).getOutput()).isNotEmpty();
@@ -60,7 +62,7 @@ class AzureOpenAiEmbeddingModelIT {
assertThat(embeddingResponse.getResults().get(1).getOutput()).isNotEmpty();
assertThat(embeddingResponse.getResults().get(1).getIndex()).isEqualTo(1);
assertThat(embeddingModel.dimensions()).isEqualTo(1536);
assertThat(this.embeddingModel.dimensions()).isEqualTo(1536);
}
@SpringBootConfiguration

View File

@@ -1,11 +1,11 @@
/*
* Copyright 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,14 +13,19 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.azure.openai;
import static org.assertj.core.api.Assertions.assertThat;
package org.springframework.ai.azure.openai;
import java.util.List;
import com.azure.ai.openai.OpenAIClient;
import com.azure.ai.openai.OpenAIClientBuilder;
import com.azure.core.credential.AzureKeyCredential;
import io.micrometer.observation.tck.TestObservationRegistry;
import io.micrometer.observation.tck.TestObservationRegistryAssert;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.springframework.ai.document.MetadataMode;
import org.springframework.ai.embedding.EmbeddingRequest;
import org.springframework.ai.embedding.EmbeddingResponse;
@@ -35,12 +40,7 @@ import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import com.azure.ai.openai.OpenAIClient;
import com.azure.ai.openai.OpenAIClientBuilder;
import com.azure.core.credential.AzureKeyCredential;
import io.micrometer.observation.tck.TestObservationRegistry;
import io.micrometer.observation.tck.TestObservationRegistryAssert;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for observation instrumentation in {@link AzureOpenAiEmbeddingModel}.
@@ -69,13 +69,13 @@ public class AzureOpenAiEmbeddingModelObservationIT {
EmbeddingRequest embeddingRequest = new EmbeddingRequest(List.of("Here comes the sun"), options);
EmbeddingResponse embeddingResponse = embeddingModel.call(embeddingRequest);
EmbeddingResponse embeddingResponse = this.embeddingModel.call(embeddingRequest);
assertThat(embeddingResponse.getResults()).isNotEmpty();
EmbeddingResponseMetadata responseMetadata = embeddingResponse.getMetadata();
assertThat(responseMetadata).isNotNull();
TestObservationRegistryAssert.assertThat(observationRegistry)
TestObservationRegistryAssert.assertThat(this.observationRegistry)
.doesNotHaveAnyRemainingCurrentObservation()
.hasObservationWithNameEqualTo(DefaultEmbeddingModelObservationConvention.DEFAULT_NAME)
.that()

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,9 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.azure.openai;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
package org.springframework.ai.azure.openai;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
@@ -26,8 +25,14 @@ import java.util.Optional;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedDeque;
import okhttp3.mockwebserver.Dispatcher;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import okio.Buffer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;
@@ -43,11 +48,7 @@ import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import okhttp3.mockwebserver.Dispatcher;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import okio.Buffer;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* Spring {@link Configuration} for AI integration testing using mock objects.
@@ -205,22 +206,22 @@ public class MockAiTestConfiguration {
*/
static class MockWebServerFactoryBean implements FactoryBean<MockWebServer>, InitializingBean, DisposableBean {
private Dispatcher dispatcher;
private final Logger logger = LoggerFactory.getLogger(getClass().getName());
private MockWebServer mockWebServer;
private final Queue<MockResponse> queuedResponses = new ConcurrentLinkedDeque<>();
public void setDispatcher(@Nullable Dispatcher dispatcher) {
this.dispatcher = dispatcher;
}
private Dispatcher dispatcher;
private MockWebServer mockWebServer;
protected Optional<Dispatcher> getDispatcher() {
return Optional.ofNullable(this.dispatcher);
}
public void setDispatcher(@Nullable Dispatcher dispatcher) {
this.dispatcher = dispatcher;
}
protected Logger getLogger() {
return this.logger;
}

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,10 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.azure.openai;
import com.azure.ai.openai.OpenAIClient;
import com.azure.ai.openai.OpenAIClientBuilder;
import okhttp3.HttpUrl;
import okhttp3.mockwebserver.Dispatcher;
import okhttp3.mockwebserver.MockWebServer;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.context.annotation.Bean;
@@ -24,10 +27,6 @@ import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Profile;
import org.springframework.test.web.servlet.MockMvc;
import okhttp3.HttpUrl;
import okhttp3.mockwebserver.Dispatcher;
import okhttp3.mockwebserver.MockWebServer;
/**
* {@link SpringBootConfiguration} for testing {@literal Azure OpenAI's} API using mock
* objects.

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.azure.openai.aot;
import java.util.Set;

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.azure.openai.function;
import java.util.ArrayList;
@@ -22,21 +23,21 @@ import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import com.azure.ai.openai.OpenAIClient;
import com.azure.ai.openai.OpenAIClientBuilder;
import com.azure.core.credential.AzureKeyCredential;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
import org.springframework.ai.azure.openai.AzureOpenAiChatModel;
import org.springframework.ai.azure.openai.AzureOpenAiChatOptions;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.function.FunctionCallbackWrapper;
import org.springframework.beans.factory.annotation.Autowired;
@@ -44,7 +45,6 @@ import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Flux;
import static org.assertj.core.api.Assertions.assertThat;
@@ -69,7 +69,7 @@ class AzureOpenAiChatModelFunctionCallIT {
List<Message> messages = new ArrayList<>(List.of(userMessage));
var promptOptions = AzureOpenAiChatOptions.builder()
.withDeploymentName(selectedModel)
.withDeploymentName(this.selectedModel)
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("getCurrentWeather")
.withDescription("Get the current weather in a given location")
@@ -77,7 +77,7 @@ class AzureOpenAiChatModelFunctionCallIT {
.build()))
.build();
ChatResponse response = chatModel.call(new Prompt(messages, promptOptions));
ChatResponse response = this.chatModel.call(new Prompt(messages, promptOptions));
logger.info("Response: {}", response);
@@ -93,7 +93,7 @@ class AzureOpenAiChatModelFunctionCallIT {
List<Message> messages = new ArrayList<>(List.of(userMessage));
var promptOptions = AzureOpenAiChatOptions.builder()
.withDeploymentName(selectedModel)
.withDeploymentName(this.selectedModel)
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("getCurrentWeather")
.withDescription("Get the current weather in a given location")
@@ -101,7 +101,7 @@ class AzureOpenAiChatModelFunctionCallIT {
.build()))
.build();
ChatResponse response = chatModel.call(new Prompt(messages, promptOptions));
ChatResponse response = this.chatModel.call(new Prompt(messages, promptOptions));
logger.info("Response: {}", response);
@@ -115,7 +115,7 @@ class AzureOpenAiChatModelFunctionCallIT {
List<Message> messages = new ArrayList<>(List.of(userMessage));
var promptOptions = AzureOpenAiChatOptions.builder()
.withDeploymentName(selectedModel)
.withDeploymentName(this.selectedModel)
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("getCurrentWeather")
.withDescription("Get the current weather in a given location")
@@ -123,7 +123,7 @@ class AzureOpenAiChatModelFunctionCallIT {
.build()))
.build();
Flux<ChatResponse> response = chatModel.stream(new Prompt(messages, promptOptions));
Flux<ChatResponse> response = this.chatModel.stream(new Prompt(messages, promptOptions));
final var counter = new AtomicInteger();
String content = response.doOnEach(listSignal -> counter.getAndIncrement())
@@ -152,7 +152,7 @@ class AzureOpenAiChatModelFunctionCallIT {
List<Message> messages = new ArrayList<>(List.of(userMessage));
var promptOptions = AzureOpenAiChatOptions.builder()
.withDeploymentName(selectedModel)
.withDeploymentName(this.selectedModel)
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("getCurrentWeather")
.withDescription("Get the current weather in a given location")
@@ -160,7 +160,7 @@ class AzureOpenAiChatModelFunctionCallIT {
.build()))
.build();
var response = chatModel.stream(new Prompt(messages, promptOptions));
var response = this.chatModel.stream(new Prompt(messages, promptOptions));
final var counter = new AtomicInteger();
String content = response.doOnEach(listSignal -> counter.getAndIncrement())
@@ -182,6 +182,16 @@ class AzureOpenAiChatModelFunctionCallIT {
@SpringBootConfiguration
public static class TestConfiguration {
public static String getDeploymentName() {
String deploymentName = System.getenv("AZURE_OPENAI_DEPLOYMENT_NAME");
if (StringUtils.hasText(deploymentName)) {
return deploymentName;
}
else {
return "gpt-4o";
}
}
@Bean
public OpenAIClientBuilder openAIClient() {
return new OpenAIClientBuilder().credential(new AzureKeyCredential(System.getenv("AZURE_OPENAI_API_KEY")))
@@ -199,16 +209,6 @@ class AzureOpenAiChatModelFunctionCallIT {
return Optional.ofNullable(System.getenv("AZURE_OPENAI_MODEL")).orElse(getDeploymentName());
}
public static String getDeploymentName() {
String deploymentName = System.getenv("AZURE_OPENAI_DEPLOYMENT_NAME");
if (StringUtils.hasText(deploymentName)) {
return deploymentName;
}
else {
return "gpt-4o";
}
}
}
}

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,29 +13,37 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.azure.openai.function;
import java.util.function.Function;
import com.fasterxml.jackson.annotation.JsonClassDescription;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import java.util.function.Function;
/**
* @author Christian Tzolov
*/
public class MockWeatherService implements Function<MockWeatherService.Request, MockWeatherService.Response> {
/**
* Weather Function request.
*/
@JsonInclude(Include.NON_NULL)
@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(required = true, value = "unit") @JsonPropertyDescription("Temperature unit") Unit unit) {
@Override
public Response apply(Request request) {
double temperature = 0;
if (request.location().contains("Paris")) {
temperature = 15;
}
else if (request.location().contains("Tokyo")) {
temperature = 10;
}
else if (request.location().contains("San Francisco")) {
temperature = 30;
}
return new Response(temperature, 15, 20, 2, 53, 45, Unit.C);
}
/**
@@ -63,28 +71,23 @@ public class MockWeatherService implements Function<MockWeatherService.Request,
}
/**
* Weather Function request.
*/
@JsonInclude(Include.NON_NULL)
@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(required = true, value = "unit") @JsonPropertyDescription("Temperature unit") Unit unit) {
}
/**
* Weather Function response.
*/
public record Response(double temp, double feels_like, double temp_min, double temp_max, int pressure, int humidity,
Unit unit) {
}
@Override
public Response apply(Request request) {
double temperature = 0;
if (request.location().contains("Paris")) {
temperature = 15;
}
else if (request.location().contains("Tokyo")) {
temperature = 10;
}
else if (request.location().contains("San Francisco")) {
temperature = 30;
}
return new Response(temperature, 15, 20, 2, 53, 45, Unit.C);
}
}

View File

@@ -1,3 +1,19 @@
/*
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.azure.openai.image;
import com.azure.ai.openai.OpenAIClient;
@@ -6,6 +22,7 @@ import com.azure.core.credential.AzureKeyCredential;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.springframework.ai.azure.openai.AzureOpenAiImageModel;
import org.springframework.ai.azure.openai.AzureOpenAiImageOptions;
import org.springframework.ai.azure.openai.metadata.AzureOpenAiImageGenerationMetadata;
@@ -39,7 +56,7 @@ public class AzureOpenAiImageModelIT {
ImagePrompt imagePrompt = new ImagePrompt(instructions, options);
ImageResponse imageResponse = imageModel.call(imagePrompt);
ImageResponse imageResponse = this.imageModel.call(imagePrompt);
assertThat(imageResponse.getResults()).hasSize(1);

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.azure.openai.metadata;
import java.nio.charset.StandardCharsets;
@@ -25,14 +26,14 @@ import org.junit.jupiter.api.Test;
import org.springframework.ai.azure.openai.AzureOpenAiChatModel;
import org.springframework.ai.azure.openai.MockAzureOpenAiTestConfiguration;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.metadata.ChatGenerationMetadata;
import org.springframework.ai.chat.metadata.ChatResponseMetadata;
import org.springframework.ai.chat.metadata.EmptyRateLimit;
import org.springframework.ai.chat.metadata.PromptMetadata;
import org.springframework.ai.chat.metadata.Usage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;

View File

@@ -1,4 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ 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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.bedrock;
import org.springframework.ai.bedrock.api.AbstractBedrockApi.AmazonBedrockInvocationMetrics;
@@ -27,10 +28,6 @@ import org.springframework.util.Assert;
*/
public class BedrockUsage implements Usage {
public static BedrockUsage from(AmazonBedrockInvocationMetrics usage) {
return new BedrockUsage(usage);
}
private final AmazonBedrockInvocationMetrics usage;
protected BedrockUsage(AmazonBedrockInvocationMetrics usage) {
@@ -38,6 +35,10 @@ public class BedrockUsage implements Usage {
this.usage = usage;
}
public static BedrockUsage from(AmazonBedrockInvocationMetrics usage) {
return new BedrockUsage(usage);
}
protected AmazonBedrockInvocationMetrics getUsage() {
return this.usage;
}

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.bedrock;
import java.util.List;
@@ -33,12 +34,12 @@ public class MessageToPromptConverter {
private static final String ASSISTANT_PROMPT = "Assistant:";
private final String lineSeparator;
private String humanPrompt = HUMAN_PROMPT;
private String assistantPrompt = ASSISTANT_PROMPT;
private final String lineSeparator;
private MessageToPromptConverter(String lineSeparator) {
this.lineSeparator = lineSeparator;
}
@@ -84,9 +85,9 @@ public class MessageToPromptConverter {
case SYSTEM:
return message.getContent();
case USER:
return humanPrompt + " " + message.getContent();
return this.humanPrompt + " " + message.getContent();
case ASSISTANT:
return assistantPrompt + " " + message.getContent();
return this.assistantPrompt + " " + message.getContent();
case TOOL:
throw new IllegalArgumentException("Tool execution results are not supported for Bedrock models");
}

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.bedrock.anthropic;
import java.util.List;
@@ -20,11 +21,10 @@ import java.util.List;
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 com.fasterxml.jackson.annotation.JsonProperty;
/**
* @author Christian Tzolov
* @author Thomas Vitale
@@ -75,44 +75,14 @@ public class AnthropicChatOptions implements ChatOptions {
return new Builder();
}
public static class Builder {
private final AnthropicChatOptions options = new AnthropicChatOptions();
public Builder withTemperature(Double temperature) {
this.options.setTemperature(temperature);
return this;
}
public Builder withMaxTokensToSample(Integer maxTokensToSample) {
this.options.setMaxTokensToSample(maxTokensToSample);
return this;
}
public Builder withTopK(Integer topK) {
this.options.setTopK(topK);
return this;
}
public Builder withTopP(Double topP) {
this.options.setTopP(topP);
return this;
}
public Builder withStopSequences(List<String> stopSequences) {
this.options.setStopSequences(stopSequences);
return this;
}
public Builder withAnthropicVersion(String anthropicVersion) {
this.options.setAnthropicVersion(anthropicVersion);
return this;
}
public AnthropicChatOptions build() {
return this.options;
}
public static AnthropicChatOptions fromOptions(AnthropicChatOptions fromOptions) {
return builder().withTemperature(fromOptions.getTemperature())
.withMaxTokensToSample(fromOptions.getMaxTokensToSample())
.withTopK(fromOptions.getTopK())
.withTopP(fromOptions.getTopP())
.withStopSequences(fromOptions.getStopSequences())
.withAnthropicVersion(fromOptions.getAnthropicVersion())
.build();
}
@Override
@@ -201,14 +171,44 @@ public class AnthropicChatOptions implements ChatOptions {
return fromOptions(this);
}
public static AnthropicChatOptions fromOptions(AnthropicChatOptions fromOptions) {
return builder().withTemperature(fromOptions.getTemperature())
.withMaxTokensToSample(fromOptions.getMaxTokensToSample())
.withTopK(fromOptions.getTopK())
.withTopP(fromOptions.getTopP())
.withStopSequences(fromOptions.getStopSequences())
.withAnthropicVersion(fromOptions.getAnthropicVersion())
.build();
public static class Builder {
private final AnthropicChatOptions options = new AnthropicChatOptions();
public Builder withTemperature(Double temperature) {
this.options.setTemperature(temperature);
return this;
}
public Builder withMaxTokensToSample(Integer maxTokensToSample) {
this.options.setMaxTokensToSample(maxTokensToSample);
return this;
}
public Builder withTopK(Integer topK) {
this.options.setTopK(topK);
return this;
}
public Builder withTopP(Double topP) {
this.options.setTopP(topP);
return this;
}
public Builder withStopSequences(List<String> stopSequences) {
this.options.setStopSequences(stopSequences);
return this;
}
public Builder withAnthropicVersion(String anthropicVersion) {
this.options.setAnthropicVersion(anthropicVersion);
return this;
}
public AnthropicChatOptions build() {
return this.options;
}
}
}

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,22 +13,23 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.bedrock.anthropic;
import java.util.List;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.metadata.ChatGenerationMetadata;
import reactor.core.publisher.Flux;
import org.springframework.ai.bedrock.MessageToPromptConverter;
import org.springframework.ai.bedrock.anthropic.api.AnthropicChatBedrockApi;
import org.springframework.ai.bedrock.anthropic.api.AnthropicChatBedrockApi.AnthropicChatRequest;
import org.springframework.ai.bedrock.anthropic.api.AnthropicChatBedrockApi.AnthropicChatResponse;
import org.springframework.ai.chat.model.StreamingChatModel;
import org.springframework.ai.chat.metadata.ChatGenerationMetadata;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.model.StreamingChatModel;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.ModelOptionsUtils;

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.bedrock.anthropic.api;
import java.time.Duration;
@@ -118,6 +119,54 @@ public class AnthropicChatBedrockApi extends
// Anthropic Claude models: https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-claude.html
@Override
public AnthropicChatResponse chatCompletion(AnthropicChatRequest anthropicRequest) {
Assert.notNull(anthropicRequest, "'anthropicRequest' must not be null");
return this.internalInvocation(anthropicRequest, AnthropicChatResponse.class);
}
@Override
public Flux<AnthropicChatResponse> chatCompletionStream(AnthropicChatRequest anthropicRequest) {
Assert.notNull(anthropicRequest, "'anthropicRequest' must not be null");
return this.internalInvocationStream(anthropicRequest, AnthropicChatResponse.class);
}
/**
* Anthropic models version.
*/
public enum AnthropicChatModel implements ChatModelDescription {
/**
* anthropic.claude-instant-v1
*/
CLAUDE_INSTANT_V1("anthropic.claude-instant-v1"),
/**
* anthropic.claude-v2
*/
CLAUDE_V2("anthropic.claude-v2"),
/**
* anthropic.claude-v2:1
*/
CLAUDE_V21("anthropic.claude-v2:1");
private final String id;
AnthropicChatModel(String value) {
this.id = value;
}
/**
* @return The model id.
*/
public String id() {
return this.id;
}
@Override
public String getName() {
return this.id;
}
}
/**
* AnthropicChatRequest encapsulates the request parameters for the Anthropic chat model.
* https://docs.anthropic.com/claude/reference/complete_post
@@ -196,13 +245,13 @@ public class AnthropicChatBedrockApi extends
public AnthropicChatRequest build() {
return new AnthropicChatRequest(
prompt,
temperature,
maxTokensToSample,
topK,
topP,
stopSequences,
anthropicVersion
this.prompt,
this.temperature,
this.maxTokensToSample,
this.topK,
this.topP,
this.stopSequences,
this.anthropicVersion
);
}
}
@@ -225,53 +274,5 @@ public class AnthropicChatBedrockApi extends
@JsonProperty("amazon-bedrock-invocationMetrics") AmazonBedrockInvocationMetrics amazonBedrockInvocationMetrics) {
}
/**
* Anthropic models version.
*/
public enum AnthropicChatModel implements ChatModelDescription {
/**
* anthropic.claude-instant-v1
*/
CLAUDE_INSTANT_V1("anthropic.claude-instant-v1"),
/**
* anthropic.claude-v2
*/
CLAUDE_V2("anthropic.claude-v2"),
/**
* anthropic.claude-v2:1
*/
CLAUDE_V21("anthropic.claude-v2:1");
private final String id;
/**
* @return The model id.
*/
public String id() {
return id;
}
AnthropicChatModel(String value) {
this.id = value;
}
@Override
public String getName() {
return this.id;
}
}
@Override
public AnthropicChatResponse chatCompletion(AnthropicChatRequest anthropicRequest) {
Assert.notNull(anthropicRequest, "'anthropicRequest' must not be null");
return this.internalInvocation(anthropicRequest, AnthropicChatResponse.class);
}
@Override
public Flux<AnthropicChatResponse> chatCompletionStream(AnthropicChatRequest anthropicRequest) {
Assert.notNull(anthropicRequest, "'anthropicRequest' must not be null");
return this.internalInvocationStream(anthropicRequest, AnthropicChatResponse.class);
}
}
// @formatter:on

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,15 +13,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.bedrock.anthropic3;
import java.util.List;
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 java.util.List;
import org.springframework.ai.chat.prompt.ChatOptions;
/**
* @author Ben Middleton
@@ -74,44 +76,14 @@ public class Anthropic3ChatOptions implements ChatOptions {
return new Builder();
}
public static class Builder {
private final Anthropic3ChatOptions options = new Anthropic3ChatOptions();
public Builder withTemperature(Double temperature) {
this.options.setTemperature(temperature);
return this;
}
public Builder withMaxTokens(Integer maxTokens) {
this.options.setMaxTokens(maxTokens);
return this;
}
public Builder withTopK(Integer topK) {
this.options.setTopK(topK);
return this;
}
public Builder withTopP(Double topP) {
this.options.setTopP(topP);
return this;
}
public Builder withStopSequences(List<String> stopSequences) {
this.options.setStopSequences(stopSequences);
return this;
}
public Builder withAnthropicVersion(String anthropicVersion) {
this.options.setAnthropicVersion(anthropicVersion);
return this;
}
public Anthropic3ChatOptions build() {
return this.options;
}
public static Anthropic3ChatOptions fromOptions(Anthropic3ChatOptions fromOptions) {
return builder().withTemperature(fromOptions.getTemperature())
.withMaxTokens(fromOptions.getMaxTokens())
.withTopK(fromOptions.getTopK())
.withTopP(fromOptions.getTopP())
.withStopSequences(fromOptions.getStopSequences())
.withAnthropicVersion(fromOptions.getAnthropicVersion())
.build();
}
@Override
@@ -190,14 +162,44 @@ public class Anthropic3ChatOptions implements ChatOptions {
return fromOptions(this);
}
public static Anthropic3ChatOptions fromOptions(Anthropic3ChatOptions fromOptions) {
return builder().withTemperature(fromOptions.getTemperature())
.withMaxTokens(fromOptions.getMaxTokens())
.withTopK(fromOptions.getTopK())
.withTopP(fromOptions.getTopP())
.withStopSequences(fromOptions.getStopSequences())
.withAnthropicVersion(fromOptions.getAnthropicVersion())
.build();
public static class Builder {
private final Anthropic3ChatOptions options = new Anthropic3ChatOptions();
public Builder withTemperature(Double temperature) {
this.options.setTemperature(temperature);
return this;
}
public Builder withMaxTokens(Integer maxTokens) {
this.options.setMaxTokens(maxTokens);
return this;
}
public Builder withTopK(Integer topK) {
this.options.setTopK(topK);
return this;
}
public Builder withTopP(Double topP) {
this.options.setTopP(topP);
return this;
}
public Builder withStopSequences(List<String> stopSequences) {
this.options.setStopSequences(stopSequences);
return this;
}
public Builder withAnthropicVersion(String anthropicVersion) {
this.options.setAnthropicVersion(anthropicVersion);
return this;
}
public Anthropic3ChatOptions build() {
return this.options;
}
}
}

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.bedrock.anthropic3;
import java.util.ArrayList;
@@ -21,11 +22,6 @@ import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.metadata.ChatResponseMetadata;
import org.springframework.ai.chat.metadata.DefaultUsage;
import org.springframework.ai.chat.metadata.Usage;
import reactor.core.publisher.Flux;
import org.springframework.ai.bedrock.anthropic3.api.Anthropic3ChatBedrockApi;
@@ -35,13 +31,18 @@ import org.springframework.ai.bedrock.anthropic3.api.Anthropic3ChatBedrockApi.An
import org.springframework.ai.bedrock.anthropic3.api.Anthropic3ChatBedrockApi.ChatCompletionMessage;
import org.springframework.ai.bedrock.anthropic3.api.Anthropic3ChatBedrockApi.ChatCompletionMessage.Role;
import org.springframework.ai.bedrock.anthropic3.api.Anthropic3ChatBedrockApi.MediaContent;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.MessageType;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.metadata.ChatGenerationMetadata;
import org.springframework.ai.chat.metadata.ChatResponseMetadata;
import org.springframework.ai.chat.metadata.DefaultUsage;
import org.springframework.ai.chat.metadata.Usage;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.model.StreamingChatModel;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.MessageType;
import org.springframework.ai.chat.metadata.ChatGenerationMetadata;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.ModelOptionsUtils;

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,24 +13,26 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.bedrock.anthropic3.api;
import java.time.Duration;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import reactor.core.publisher.Flux;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import org.springframework.ai.bedrock.anthropic3.api.Anthropic3ChatBedrockApi.AnthropicChatRequest;
import org.springframework.ai.bedrock.anthropic3.api.Anthropic3ChatBedrockApi.AnthropicChatResponse;
import org.springframework.ai.bedrock.anthropic3.api.Anthropic3ChatBedrockApi.AnthropicChatStreamingResponse;
import org.springframework.ai.bedrock.api.AbstractBedrockApi;
import org.springframework.ai.model.ChatModelDescription;
import org.springframework.util.Assert;
import reactor.core.publisher.Flux;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import java.time.Duration;
import java.util.List;
/**
* Based on Bedrock's <a href=
@@ -122,6 +124,76 @@ public class Anthropic3ChatBedrockApi extends
// Anthropic Claude models: https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-claude.html
@Override
public AnthropicChatResponse chatCompletion(AnthropicChatRequest anthropicRequest) {
Assert.notNull(anthropicRequest, "'anthropicRequest' must not be null");
return this.internalInvocation(anthropicRequest, AnthropicChatResponse.class);
}
@Override
public Flux<AnthropicChatStreamingResponse> chatCompletionStream(AnthropicChatRequest anthropicRequest) {
Assert.notNull(anthropicRequest, "'anthropicRequest' must not be null");
return this.internalInvocationStream(anthropicRequest, AnthropicChatStreamingResponse.class);
}
/**
* Anthropic models version.
*/
public enum AnthropicChatModel implements ChatModelDescription {
/**
* anthropic.claude-instant-v1
*/
CLAUDE_INSTANT_V1("anthropic.claude-instant-v1"),
/**
* anthropic.claude-v2
*/
CLAUDE_V2("anthropic.claude-v2"),
/**
* anthropic.claude-v2:1
*/
CLAUDE_V21("anthropic.claude-v2:1"),
/**
* anthropic.claude-3-sonnet-20240229-v1:0
*/
CLAUDE_V3_SONNET("anthropic.claude-3-sonnet-20240229-v1:0"),
/**
* anthropic.claude-3-haiku-20240307-v1:0
*/
CLAUDE_V3_HAIKU("anthropic.claude-3-haiku-20240307-v1:0"),
/**
* anthropic.claude-3-opus-20240229-v1:0
*/
CLAUDE_V3_OPUS("anthropic.claude-3-opus-20240229-v1:0"),
/**
* anthropic.claude-3-5-sonnet-20240620-v1:0
*/
CLAUDE_V3_5_SONNET("anthropic.claude-3-5-sonnet-20240620-v1:0"),
/**
* anthropic.claude-3-5-sonnet-20241022-v2:0
*/
CLAUDE_V3_5_SONNET_V2("anthropic.claude-3-5-sonnet-20241022-v2:0");
private final String id;
AnthropicChatModel(String value) {
this.id = value;
}
/**
* @return The model id.
*/
public String id() {
return this.id;
}
@Override
public String getName() {
return this.id;
}
}
/**
* AnthropicChatRequest encapsulates the request parameters for the Anthropic messages model.
* https://docs.anthropic.com/claude/reference/messages_post
@@ -208,14 +280,14 @@ public class Anthropic3ChatBedrockApi extends
public AnthropicChatRequest build() {
return new AnthropicChatRequest(
messages,
system,
temperature,
maxTokens,
topK,
topP,
stopSequences,
anthropicVersion
this.messages,
this.system,
this.temperature,
this.maxTokens,
this.topK,
this.topP,
this.stopSequences,
this.anthropicVersion
);
}
}
@@ -286,7 +358,9 @@ public class Anthropic3ChatBedrockApi extends
public Source(String mediaType, String data) {
this("base64", mediaType, data);
}
}
}
/**
@@ -317,6 +391,7 @@ public class Anthropic3ChatBedrockApi extends
ASSISTANT
}
}
/**
@@ -329,6 +404,7 @@ public class Anthropic3ChatBedrockApi extends
@JsonInclude(Include.NON_NULL)
public record AnthropicUsage(@JsonProperty("input_tokens") Integer inputTokens,
@JsonProperty("output_tokens") Integer outputTokens) {
}
/**
@@ -356,6 +432,7 @@ public class Anthropic3ChatBedrockApi extends
@JsonProperty("stop_reason") String stopReason, @JsonProperty("stop_sequence") String stopSequence,
@JsonProperty("usage") AnthropicUsage usage,
@JsonProperty("amazon-bedrock-invocationMetrics") AmazonBedrockInvocationMetrics amazonBedrockInvocationMetrics) { // formatter:on
}
/**
@@ -432,77 +509,9 @@ public class Anthropic3ChatBedrockApi extends
@JsonInclude(Include.NON_NULL)
public record Delta(@JsonProperty("type") String type, @JsonProperty("text") String text,
@JsonProperty("stop_reason") String stopReason, @JsonProperty("stop_sequence") String stopSequence) {
}
}
/**
* Anthropic models version.
*/
public enum AnthropicChatModel implements ChatModelDescription {
/**
* anthropic.claude-instant-v1
*/
CLAUDE_INSTANT_V1("anthropic.claude-instant-v1"),
/**
* anthropic.claude-v2
*/
CLAUDE_V2("anthropic.claude-v2"),
/**
* anthropic.claude-v2:1
*/
CLAUDE_V21("anthropic.claude-v2:1"),
/**
* anthropic.claude-3-sonnet-20240229-v1:0
*/
CLAUDE_V3_SONNET("anthropic.claude-3-sonnet-20240229-v1:0"),
/**
* anthropic.claude-3-haiku-20240307-v1:0
*/
CLAUDE_V3_HAIKU("anthropic.claude-3-haiku-20240307-v1:0"),
/**
* anthropic.claude-3-opus-20240229-v1:0
*/
CLAUDE_V3_OPUS("anthropic.claude-3-opus-20240229-v1:0"),
/**
* anthropic.claude-3-5-sonnet-20240620-v1:0
*/
CLAUDE_V3_5_SONNET("anthropic.claude-3-5-sonnet-20240620-v1:0"),
/**
* anthropic.claude-3-5-sonnet-20241022-v2:0
*/
CLAUDE_V3_5_SONNET_V2("anthropic.claude-3-5-sonnet-20241022-v2:0");
private final String id;
/**
* @return The model id.
*/
public String id() {
return id;
}
AnthropicChatModel(String value) {
this.id = value;
}
@Override
public String getName() {
return this.id;
}
}
@Override
public AnthropicChatResponse chatCompletion(AnthropicChatRequest anthropicRequest) {
Assert.notNull(anthropicRequest, "'anthropicRequest' must not be null");
return this.internalInvocation(anthropicRequest, AnthropicChatResponse.class);
}
@Override
public Flux<AnthropicChatStreamingResponse> chatCompletionStream(AnthropicChatRequest anthropicRequest) {
Assert.notNull(anthropicRequest, "'anthropicRequest' must not be null");
return this.internalInvocationStream(anthropicRequest, AnthropicChatStreamingResponse.class);
}
}

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.bedrock.aot;
import org.springframework.ai.bedrock.anthropic.AnthropicChatOptions;

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -175,24 +175,6 @@ public abstract class AbstractBedrockApi<I, O, SO> {
return this.region;
}
/**
* Encapsulates the metrics about the model invocation.
* https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-claude.html
*
* @param inputTokenCount The number of tokens in the input prompt.
* @param firstByteLatency The time in milliseconds between the request being sent and the first byte of the
* response being received.
* @param outputTokenCount The number of tokens in the generated text.
* @param invocationLatency The time in milliseconds between the request being sent and the response being received.
*/
@JsonInclude(Include.NON_NULL)
public record AmazonBedrockInvocationMetrics(
@JsonProperty("inputTokenCount") Long inputTokenCount,
@JsonProperty("firstByteLatency") Long firstByteLatency,
@JsonProperty("outputTokenCount") Long outputTokenCount,
@JsonProperty("invocationLatency") Long invocationLatency) {
}
/**
* Compute the embedding for the given text.
*
@@ -337,5 +319,23 @@ public abstract class AbstractBedrockApi<I, O, SO> {
return eventSink.asFlux();
}
/**
* Encapsulates the metrics about the model invocation.
* https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-claude.html
*
* @param inputTokenCount The number of tokens in the input prompt.
* @param firstByteLatency The time in milliseconds between the request being sent and the first byte of the
* response being received.
* @param outputTokenCount The number of tokens in the generated text.
* @param invocationLatency The time in milliseconds between the request being sent and the response being received.
*/
@JsonInclude(Include.NON_NULL)
public record AmazonBedrockInvocationMetrics(
@JsonProperty("inputTokenCount") Long inputTokenCount,
@JsonProperty("firstByteLatency") Long firstByteLatency,
@JsonProperty("outputTokenCount") Long outputTokenCount,
@JsonProperty("invocationLatency") Long invocationLatency) {
}
}
// @formatter:on

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.bedrock.cohere;
import java.util.List;
@@ -24,13 +25,13 @@ import org.springframework.ai.bedrock.MessageToPromptConverter;
import org.springframework.ai.bedrock.cohere.api.CohereChatBedrockApi;
import org.springframework.ai.bedrock.cohere.api.CohereChatBedrockApi.CohereChatRequest;
import org.springframework.ai.bedrock.cohere.api.CohereChatBedrockApi.CohereChatResponse;
import org.springframework.ai.chat.metadata.ChatGenerationMetadata;
import org.springframework.ai.chat.metadata.Usage;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.model.StreamingChatModel;
import org.springframework.ai.chat.metadata.ChatGenerationMetadata;
import org.springframework.ai.chat.metadata.Usage;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.ModelOptionsUtils;
import org.springframework.util.Assert;

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.bedrock.cohere;
import java.util.List;
@@ -85,59 +86,17 @@ public class BedrockCohereChatOptions implements ChatOptions {
return new Builder();
}
public static class Builder {
private final BedrockCohereChatOptions options = new BedrockCohereChatOptions();
public Builder withTemperature(Double temperature) {
this.options.setTemperature(temperature);
return this;
}
public Builder withTopP(Double topP) {
this.options.setTopP(topP);
return this;
}
public Builder withTopK(Integer topK) {
this.options.setTopK(topK);
return this;
}
public Builder withMaxTokens(Integer maxTokens) {
this.options.setMaxTokens(maxTokens);
return this;
}
public Builder withStopSequences(List<String> stopSequences) {
this.options.setStopSequences(stopSequences);
return this;
}
public Builder withReturnLikelihoods(ReturnLikelihoods returnLikelihoods) {
this.options.setReturnLikelihoods(returnLikelihoods);
return this;
}
public Builder withNumGenerations(Integer numGenerations) {
this.options.setNumGenerations(numGenerations);
return this;
}
public Builder withLogitBias(LogitBias logitBias) {
this.options.setLogitBias(logitBias);
return this;
}
public Builder withTruncate(Truncate truncate) {
this.options.setTruncate(truncate);
return this;
}
public BedrockCohereChatOptions build() {
return this.options;
}
public static BedrockCohereChatOptions fromOptions(BedrockCohereChatOptions fromOptions) {
return builder().withTemperature(fromOptions.getTemperature())
.withTopP(fromOptions.getTopP())
.withTopK(fromOptions.getTopK())
.withMaxTokens(fromOptions.getMaxTokens())
.withStopSequences(fromOptions.getStopSequences())
.withReturnLikelihoods(fromOptions.getReturnLikelihoods())
.withNumGenerations(fromOptions.getNumGenerations())
.withLogitBias(fromOptions.getLogitBias())
.withTruncate(fromOptions.getTruncate())
.build();
}
@Override
@@ -240,17 +199,59 @@ public class BedrockCohereChatOptions implements ChatOptions {
return fromOptions(this);
}
public static BedrockCohereChatOptions fromOptions(BedrockCohereChatOptions fromOptions) {
return builder().withTemperature(fromOptions.getTemperature())
.withTopP(fromOptions.getTopP())
.withTopK(fromOptions.getTopK())
.withMaxTokens(fromOptions.getMaxTokens())
.withStopSequences(fromOptions.getStopSequences())
.withReturnLikelihoods(fromOptions.getReturnLikelihoods())
.withNumGenerations(fromOptions.getNumGenerations())
.withLogitBias(fromOptions.getLogitBias())
.withTruncate(fromOptions.getTruncate())
.build();
public static class Builder {
private final BedrockCohereChatOptions options = new BedrockCohereChatOptions();
public Builder withTemperature(Double temperature) {
this.options.setTemperature(temperature);
return this;
}
public Builder withTopP(Double topP) {
this.options.setTopP(topP);
return this;
}
public Builder withTopK(Integer topK) {
this.options.setTopK(topK);
return this;
}
public Builder withMaxTokens(Integer maxTokens) {
this.options.setMaxTokens(maxTokens);
return this;
}
public Builder withStopSequences(List<String> stopSequences) {
this.options.setStopSequences(stopSequences);
return this;
}
public Builder withReturnLikelihoods(ReturnLikelihoods returnLikelihoods) {
this.options.setReturnLikelihoods(returnLikelihoods);
return this;
}
public Builder withNumGenerations(Integer numGenerations) {
this.options.setNumGenerations(numGenerations);
return this;
}
public Builder withLogitBias(LogitBias logitBias) {
this.options.setLogitBias(logitBias);
return this;
}
public Builder withTruncate(Truncate truncate) {
this.options.setTruncate(truncate);
return this;
}
public BedrockCohereChatOptions build() {
return this.options;
}
}
}

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.bedrock.cohere;
import java.util.List;

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.bedrock.cohere;
import com.fasterxml.jackson.annotation.JsonIgnore;
@@ -52,26 +53,6 @@ public class BedrockCohereEmbeddingOptions implements EmbeddingOptions {
return new Builder();
}
public static class Builder {
private BedrockCohereEmbeddingOptions options = new BedrockCohereEmbeddingOptions();
public Builder withInputType(InputType inputType) {
this.options.setInputType(inputType);
return this;
}
public Builder withTruncate(Truncate truncate) {
this.options.setTruncate(truncate);
return this;
}
public BedrockCohereEmbeddingOptions build() {
return this.options;
}
}
public InputType getInputType() {
return this.inputType;
}
@@ -100,4 +81,24 @@ public class BedrockCohereEmbeddingOptions implements EmbeddingOptions {
return null;
}
public static class Builder {
private BedrockCohereEmbeddingOptions options = new BedrockCohereEmbeddingOptions();
public Builder withInputType(InputType inputType) {
this.options.setInputType(inputType);
return this;
}
public Builder withTruncate(Truncate truncate) {
this.options.setTruncate(truncate);
return this;
}
public BedrockCohereEmbeddingOptions build() {
return this.options;
}
}
}

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -109,6 +109,52 @@ public class CohereChatBedrockApi extends
super(modelId, credentialsProvider, region, objectMapper, timeout);
}
@Override
public CohereChatResponse chatCompletion(CohereChatRequest request) {
Assert.isTrue(!request.stream(), "The request must be configured to return the complete response!");
return this.internalInvocation(request, CohereChatResponse.class);
}
@Override
public Flux<CohereChatResponse.Generation> chatCompletionStream(CohereChatRequest request) {
Assert.isTrue(request.stream(), "The request must be configured to stream the response!");
return this.internalInvocationStream(request, CohereChatResponse.Generation.class);
}
/**
* Cohere models version.
*/
public enum CohereChatModel implements ChatModelDescription {
/**
* cohere.command-light-text-v14
*/
COHERE_COMMAND_LIGHT_V14("cohere.command-light-text-v14"),
/**
* cohere.command-text-v14
*/
COHERE_COMMAND_V14("cohere.command-text-v14");
private final String id;
CohereChatModel(String value) {
this.id = value;
}
/**
* @return The model id.
*/
public String id() {
return this.id;
}
@Override
public String getName() {
return this.id;
}
}
/**
* CohereChatRequest encapsulates the request parameters for the Cohere command model.
*
@@ -143,15 +189,12 @@ public class CohereChatBedrockApi extends
@JsonProperty("truncate") Truncate truncate) {
/**
* Prevents the model from generating unwanted tokens or incentivize the model to include desired tokens.
*
* @param token The token likelihoods.
* @param bias A float between -10 and 10.
* Get CohereChatRequest builder.
* @param prompt compulsory request prompt parameter.
* @return CohereChatRequest builder.
*/
@JsonInclude(Include.NON_NULL)
public record LogitBias(
@JsonProperty("token") String token,
@JsonProperty("bias") Float bias) {
public static Builder builder(String prompt) {
return new Builder(prompt);
}
/**
@@ -192,12 +235,15 @@ public class CohereChatBedrockApi extends
}
/**
* Get CohereChatRequest builder.
* @param prompt compulsory request prompt parameter.
* @return CohereChatRequest builder.
* Prevents the model from generating unwanted tokens or incentivize the model to include desired tokens.
*
* @param token The token likelihoods.
* @param bias A float between -10 and 10.
*/
public static Builder builder(String prompt) {
return new Builder(prompt);
@JsonInclude(Include.NON_NULL)
public record LogitBias(
@JsonProperty("token") String token,
@JsonProperty("bias") Float bias) {
}
/**
@@ -272,17 +318,17 @@ public class CohereChatBedrockApi extends
public CohereChatRequest build() {
return new CohereChatRequest(
prompt,
temperature,
topP,
topK,
maxTokens,
stopSequences,
returnLikelihoods,
stream,
numGenerations,
logitBias,
truncate
this.prompt,
this.temperature,
this.topP,
this.topK,
this.maxTokens,
this.stopSequences,
this.returnLikelihoods,
this.stream,
this.numGenerations,
this.logitBias,
this.truncate
);
}
}
@@ -331,16 +377,6 @@ public class CohereChatBedrockApi extends
@JsonProperty("index") Integer index,
@JsonProperty("amazon-bedrock-invocationMetrics") AmazonBedrockInvocationMetrics amazonBedrockInvocationMetrics) {
/**
* @param token The token.
* @param likelihood The likelihood of the token.
*/
@JsonInclude(Include.NON_NULL)
public record TokenLikelihood(
@JsonProperty("token") String token,
@JsonProperty("likelihood") Float likelihood) {
}
/**
* The reason the response finished being generated.
*/
@@ -363,53 +399,17 @@ public class CohereChatBedrockApi extends
*/
ERROR_TOXIC
}
/**
* @param token The token.
* @param likelihood The likelihood of the token.
*/
@JsonInclude(Include.NON_NULL)
public record TokenLikelihood(
@JsonProperty("token") String token,
@JsonProperty("likelihood") Float likelihood) {
}
}
}
/**
* Cohere models version.
*/
public enum CohereChatModel implements ChatModelDescription {
/**
* cohere.command-light-text-v14
*/
COHERE_COMMAND_LIGHT_V14("cohere.command-light-text-v14"),
/**
* cohere.command-text-v14
*/
COHERE_COMMAND_V14("cohere.command-text-v14");
private final String id;
/**
* @return The model id.
*/
public String id() {
return id;
}
CohereChatModel(String value) {
this.id = value;
}
@Override
public String getName() {
return this.id;
}
}
@Override
public CohereChatResponse chatCompletion(CohereChatRequest request) {
Assert.isTrue(!request.stream(), "The request must be configured to return the complete response!");
return this.internalInvocation(request, CohereChatResponse.class);
}
@Override
public Flux<CohereChatResponse.Generation> chatCompletionStream(CohereChatRequest request) {
Assert.isTrue(request.stream(), "The request must be configured to stream the response!");
return this.internalInvocationStream(request, CohereChatResponse.Generation.class);
}
}
// @formatter:on

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -109,6 +109,39 @@ public class CohereEmbeddingBedrockApi extends
super(modelId, credentialsProvider, region, objectMapper, timeout);
}
@Override
public CohereEmbeddingResponse embedding(CohereEmbeddingRequest request) {
return this.internalInvocation(request, CohereEmbeddingResponse.class);
}
/**
* Cohere Embedding model ids. https://docs.aws.amazon.com/bedrock/latest/userguide/model-ids-arns.html
*/
public enum CohereEmbeddingModel {
/**
* cohere.embed-multilingual-v3
*/
COHERE_EMBED_MULTILINGUAL_V1("cohere.embed-multilingual-v3"),
/**
* cohere.embed-english-v3
*/
COHERE_EMBED_ENGLISH_V3("cohere.embed-english-v3");
private final String id;
CohereEmbeddingModel(String value) {
this.id = value;
}
/**
* @return The model id.
*/
public String id() {
return this.id;
}
}
/**
* The Cohere Embed model request.
*
@@ -190,38 +223,5 @@ public class CohereEmbeddingBedrockApi extends
@JsonProperty("amazon-bedrock-invocationMetrics") AmazonBedrockInvocationMetrics amazonBedrockInvocationMetrics) {
}
/**
* Cohere Embedding model ids. https://docs.aws.amazon.com/bedrock/latest/userguide/model-ids-arns.html
*/
public enum CohereEmbeddingModel {
/**
* cohere.embed-multilingual-v3
*/
COHERE_EMBED_MULTILINGUAL_V1("cohere.embed-multilingual-v3"),
/**
* cohere.embed-english-v3
*/
COHERE_EMBED_ENGLISH_V3("cohere.embed-english-v3");
private final String id;
/**
* @return The model id.
*/
public String id() {
return this.id;
}
CohereEmbeddingModel(String value) {
this.id = value;
}
}
@Override
public CohereEmbeddingResponse embedding(CohereEmbeddingRequest request) {
return this.internalInvocation(request, CohereEmbeddingResponse.class);
}
}
// @formatter:on

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -19,10 +19,10 @@ package org.springframework.ai.bedrock.jurassic2;
import org.springframework.ai.bedrock.MessageToPromptConverter;
import org.springframework.ai.bedrock.jurassic2.api.Ai21Jurassic2ChatBedrockApi;
import org.springframework.ai.bedrock.jurassic2.api.Ai21Jurassic2ChatBedrockApi.Ai21Jurassic2ChatRequest;
import org.springframework.ai.chat.metadata.ChatGenerationMetadata;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.metadata.ChatGenerationMetadata;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.ModelOptionsUtils;
@@ -57,6 +57,10 @@ public class BedrockAi21Jurassic2ChatModel implements ChatModel {
.build());
}
public static Builder builder(Ai21Jurassic2ChatBedrockApi chatApi) {
return new Builder(chatApi);
}
@Override
public ChatResponse call(Prompt prompt) {
var request = createRequest(prompt);
@@ -88,8 +92,9 @@ public class BedrockAi21Jurassic2ChatModel implements ChatModel {
return request;
}
public static Builder builder(Ai21Jurassic2ChatBedrockApi chatApi) {
return new Builder(chatApi);
@Override
public ChatOptions getDefaultOptions() {
return BedrockAi21Jurassic2ChatOptions.fromOptions(this.defaultOptions);
}
public static class Builder {
@@ -108,15 +113,10 @@ public class BedrockAi21Jurassic2ChatModel implements ChatModel {
}
public BedrockAi21Jurassic2ChatModel build() {
return new BedrockAi21Jurassic2ChatModel(chatApi,
options != null ? options : BedrockAi21Jurassic2ChatOptions.builder().build());
return new BedrockAi21Jurassic2ChatModel(this.chatApi,
this.options != null ? this.options : BedrockAi21Jurassic2ChatOptions.builder().build());
}
}
@Override
public ChatOptions getDefaultOptions() {
return BedrockAi21Jurassic2ChatOptions.fromOptions(this.defaultOptions);
}
}

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -16,12 +16,13 @@
package org.springframework.ai.bedrock.jurassic2;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.ai.chat.prompt.ChatOptions;
import java.util.List;
import org.springframework.ai.chat.prompt.ChatOptions;
/**
* Request body for the /complete endpoint of the Jurassic-2 API.
@@ -101,12 +102,31 @@ public class BedrockAi21Jurassic2ChatOptions implements ChatOptions {
// Getters and setters
public static Builder builder() {
return new Builder();
}
public static BedrockAi21Jurassic2ChatOptions fromOptions(BedrockAi21Jurassic2ChatOptions fromOptions) {
return builder().withPrompt(fromOptions.getPrompt())
.withNumResults(fromOptions.getNumResults())
.withMaxTokens(fromOptions.getMaxTokens())
.withMinTokens(fromOptions.getMinTokens())
.withTemperature(fromOptions.getTemperature())
.withTopP(fromOptions.getTopP())
.withTopK(fromOptions.getTopK())
.withStopSequences(fromOptions.getStopSequences())
.withFrequencyPenaltyOptions(fromOptions.getFrequencyPenaltyOptions())
.withPresencePenaltyOptions(fromOptions.getPresencePenaltyOptions())
.withCountPenaltyOptions(fromOptions.getCountPenaltyOptions())
.build();
}
/**
* Gets the prompt text for the model to continue.
* @return The prompt text.
*/
public String getPrompt() {
return prompt;
return this.prompt;
}
/**
@@ -122,7 +142,7 @@ public class BedrockAi21Jurassic2ChatOptions implements ChatOptions {
* @return The number of results.
*/
public Integer getNumResults() {
return numResults;
return this.numResults;
}
/**
@@ -139,7 +159,7 @@ public class BedrockAi21Jurassic2ChatOptions implements ChatOptions {
*/
@Override
public Integer getMaxTokens() {
return maxTokens;
return this.maxTokens;
}
/**
@@ -155,7 +175,7 @@ public class BedrockAi21Jurassic2ChatOptions implements ChatOptions {
* @return The minimum number of tokens.
*/
public Integer getMinTokens() {
return minTokens;
return this.minTokens;
}
/**
@@ -172,7 +192,7 @@ public class BedrockAi21Jurassic2ChatOptions implements ChatOptions {
*/
@Override
public Double getTemperature() {
return temperature;
return this.temperature;
}
/**
@@ -190,7 +210,7 @@ public class BedrockAi21Jurassic2ChatOptions implements ChatOptions {
*/
@Override
public Double getTopP() {
return topP;
return this.topP;
}
/**
@@ -208,7 +228,7 @@ public class BedrockAi21Jurassic2ChatOptions implements ChatOptions {
*/
@Override
public Integer getTopK() {
return topK;
return this.topK;
}
/**
@@ -225,7 +245,7 @@ public class BedrockAi21Jurassic2ChatOptions implements ChatOptions {
*/
@Override
public List<String> getStopSequences() {
return stopSequences;
return this.stopSequences;
}
/**
@@ -254,7 +274,7 @@ public class BedrockAi21Jurassic2ChatOptions implements ChatOptions {
* @return The frequency penalty object.
*/
public Penalty getFrequencyPenaltyOptions() {
return frequencyPenaltyOptions;
return this.frequencyPenaltyOptions;
}
/**
@@ -283,7 +303,7 @@ public class BedrockAi21Jurassic2ChatOptions implements ChatOptions {
* @return The presence penalty object.
*/
public Penalty getPresencePenaltyOptions() {
return presencePenaltyOptions;
return this.presencePenaltyOptions;
}
/**
@@ -299,7 +319,7 @@ public class BedrockAi21Jurassic2ChatOptions implements ChatOptions {
* @return The count penalty object.
*/
public Penalty getCountPenaltyOptions() {
return countPenaltyOptions;
return this.countPenaltyOptions;
}
/**
@@ -316,8 +336,9 @@ public class BedrockAi21Jurassic2ChatOptions implements ChatOptions {
return null;
}
public static Builder builder() {
return new Builder();
@Override
public BedrockAi21Jurassic2ChatOptions copy() {
return fromOptions(this);
}
public static class Builder {
@@ -325,62 +346,62 @@ public class BedrockAi21Jurassic2ChatOptions implements ChatOptions {
private final BedrockAi21Jurassic2ChatOptions request = new BedrockAi21Jurassic2ChatOptions();
public Builder withPrompt(String prompt) {
request.setPrompt(prompt);
this.request.setPrompt(prompt);
return this;
}
public Builder withNumResults(Integer numResults) {
request.setNumResults(numResults);
this.request.setNumResults(numResults);
return this;
}
public Builder withMaxTokens(Integer maxTokens) {
request.setMaxTokens(maxTokens);
this.request.setMaxTokens(maxTokens);
return this;
}
public Builder withMinTokens(Integer minTokens) {
request.setMinTokens(minTokens);
this.request.setMinTokens(minTokens);
return this;
}
public Builder withTemperature(Double temperature) {
request.setTemperature(temperature);
this.request.setTemperature(temperature);
return this;
}
public Builder withTopP(Double topP) {
request.setTopP(topP);
this.request.setTopP(topP);
return this;
}
public Builder withStopSequences(List<String> stopSequences) {
request.setStopSequences(stopSequences);
this.request.setStopSequences(stopSequences);
return this;
}
public Builder withTopK(Integer topKReturn) {
request.setTopK(topKReturn);
this.request.setTopK(topKReturn);
return this;
}
public Builder withFrequencyPenaltyOptions(BedrockAi21Jurassic2ChatOptions.Penalty frequencyPenalty) {
request.setFrequencyPenaltyOptions(frequencyPenalty);
this.request.setFrequencyPenaltyOptions(frequencyPenalty);
return this;
}
public Builder withPresencePenaltyOptions(BedrockAi21Jurassic2ChatOptions.Penalty presencePenalty) {
request.setPresencePenaltyOptions(presencePenalty);
this.request.setPresencePenaltyOptions(presencePenalty);
return this;
}
public Builder withCountPenaltyOptions(BedrockAi21Jurassic2ChatOptions.Penalty countPenalty) {
request.setCountPenaltyOptions(countPenalty);
this.request.setCountPenaltyOptions(countPenalty);
return this;
}
public BedrockAi21Jurassic2ChatOptions build() {
return request;
return this.request;
}
}
@@ -446,31 +467,12 @@ public class BedrockAi21Jurassic2ChatOptions implements ChatOptions {
}
public Penalty build() {
return new Penalty(scale, applyToNumbers, applyToPunctuations, applyToStopwords, applyToWhitespaces,
applyToEmojis);
return new Penalty(this.scale, this.applyToNumbers, this.applyToPunctuations, this.applyToStopwords,
this.applyToWhitespaces, this.applyToEmojis);
}
}
}
@Override
public BedrockAi21Jurassic2ChatOptions copy() {
return fromOptions(this);
}
public static BedrockAi21Jurassic2ChatOptions fromOptions(BedrockAi21Jurassic2ChatOptions fromOptions) {
return builder().withPrompt(fromOptions.getPrompt())
.withNumResults(fromOptions.getNumResults())
.withMaxTokens(fromOptions.getMaxTokens())
.withMinTokens(fromOptions.getMinTokens())
.withTemperature(fromOptions.getTemperature())
.withTopP(fromOptions.getTopP())
.withTopK(fromOptions.getTopK())
.withStopSequences(fromOptions.getStopSequences())
.withFrequencyPenaltyOptions(fromOptions.getFrequencyPenaltyOptions())
.withPresencePenaltyOptions(fromOptions.getPresencePenaltyOptions())
.withCountPenaltyOptions(fromOptions.getCountPenaltyOptions())
.build();
}
}

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -22,16 +22,15 @@ import java.util.List;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import org.springframework.ai.bedrock.api.AbstractBedrockApi;
import org.springframework.ai.bedrock.jurassic2.api.Ai21Jurassic2ChatBedrockApi.Ai21Jurassic2ChatRequest;
import org.springframework.ai.bedrock.jurassic2.api.Ai21Jurassic2ChatBedrockApi.Ai21Jurassic2ChatResponse;
import org.springframework.ai.model.ChatModelDescription;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.regions.Region;
/**
* Java client for the Bedrock Jurassic2 chat model.
* https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-jurassic2.html
@@ -110,6 +109,45 @@ public class Ai21Jurassic2ChatBedrockApi extends
super(modelId, credentialsProvider, region, objectMapper, timeout);
}
@Override
public Ai21Jurassic2ChatResponse chatCompletion(Ai21Jurassic2ChatRequest request) {
return this.internalInvocation(request, Ai21Jurassic2ChatResponse.class);
}
/**
* Ai21 Jurassic2 models version.
*/
public enum Ai21Jurassic2ChatModel implements ChatModelDescription {
/**
* ai21.j2-mid-v1
*/
AI21_J2_MID_V1("ai21.j2-mid-v1"),
/**
* ai21.j2-ultra-v1
*/
AI21_J2_ULTRA_V1("ai21.j2-ultra-v1");
private final String id;
Ai21Jurassic2ChatModel(String value) {
this.id = value;
}
/**
* @return The model id.
*/
public String id() {
return this.id;
}
@Override
public String getName() {
return this.id;
}
}
/**
* AI21 Jurassic2 chat request parameters.
*
@@ -141,6 +179,10 @@ public class Ai21Jurassic2ChatBedrockApi extends
@JsonProperty("presencePenalty") FloatScalePenalty presencePenalty,
@JsonProperty("frequencyPenalty") IntegerScalePenalty frequencyPenalty) {
public static Builder builder(String prompt) {
return new Builder(prompt);
}
/**
* Penalty with integer scale value.
*
@@ -192,11 +234,6 @@ public class Ai21Jurassic2ChatBedrockApi extends
@JsonProperty("applyToEmojis") boolean applyToEmojis) {
}
public static Builder builder(String prompt) {
return new Builder(prompt);
}
public static class Builder {
private String prompt;
private Double temperature;
@@ -248,14 +285,14 @@ public class Ai21Jurassic2ChatBedrockApi extends
public Ai21Jurassic2ChatRequest build() {
return new Ai21Jurassic2ChatRequest(
prompt,
temperature,
topP,
maxTokens,
stopSequences,
countPenalty,
presencePenalty,
frequencyPenalty
this.prompt,
this.temperature,
this.topP,
this.maxTokens,
this.stopSequences,
this.countPenalty,
this.presencePenalty,
this.frequencyPenalty
);
}
}
@@ -370,45 +407,6 @@ public class Ai21Jurassic2ChatBedrockApi extends
}
}
/**
* Ai21 Jurassic2 models version.
*/
public enum Ai21Jurassic2ChatModel implements ChatModelDescription {
/**
* ai21.j2-mid-v1
*/
AI21_J2_MID_V1("ai21.j2-mid-v1"),
/**
* ai21.j2-ultra-v1
*/
AI21_J2_ULTRA_V1("ai21.j2-ultra-v1");
private final String id;
/**
* @return The model id.
*/
public String id() {
return id;
}
Ai21Jurassic2ChatModel(String value) {
this.id = value;
}
@Override
public String getName() {
return this.id;
}
}
@Override
public Ai21Jurassic2ChatResponse chatCompletion(Ai21Jurassic2ChatRequest request) {
return this.internalInvocation(request, Ai21Jurassic2ChatResponse.class);
}
}
// @formatter:on

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.bedrock.llama;
import java.util.List;
@@ -23,13 +24,13 @@ import org.springframework.ai.bedrock.MessageToPromptConverter;
import org.springframework.ai.bedrock.llama.api.LlamaChatBedrockApi;
import org.springframework.ai.bedrock.llama.api.LlamaChatBedrockApi.LlamaChatRequest;
import org.springframework.ai.bedrock.llama.api.LlamaChatBedrockApi.LlamaChatResponse;
import org.springframework.ai.chat.metadata.ChatGenerationMetadata;
import org.springframework.ai.chat.metadata.Usage;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.model.StreamingChatModel;
import org.springframework.ai.chat.metadata.ChatGenerationMetadata;
import org.springframework.ai.chat.metadata.Usage;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.ModelOptionsUtils;
import org.springframework.util.Assert;

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,8 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.bedrock.llama;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
@@ -22,8 +25,6 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.ai.chat.prompt.ChatOptions;
import java.util.List;
/**
* @author Christian Tzolov
* @author Thomas Vitale
@@ -52,29 +53,11 @@ public class BedrockLlamaChatOptions implements ChatOptions {
return new Builder();
}
public static class Builder {
private BedrockLlamaChatOptions options = new BedrockLlamaChatOptions();
public Builder withTemperature(Double temperature) {
this.options.setTemperature(temperature);
return this;
}
public Builder withTopP(Double topP) {
this.options.setTopP(topP);
return this;
}
public Builder withMaxGenLen(Integer maxGenLen) {
this.options.setMaxGenLen(maxGenLen);
return this;
}
public BedrockLlamaChatOptions build() {
return this.options;
}
public static BedrockLlamaChatOptions fromOptions(BedrockLlamaChatOptions fromOptions) {
return builder().withTemperature(fromOptions.getTemperature())
.withTopP(fromOptions.getTopP())
.withMaxGenLen(fromOptions.getMaxGenLen())
.build();
}
@Override
@@ -149,11 +132,29 @@ public class BedrockLlamaChatOptions implements ChatOptions {
return fromOptions(this);
}
public static BedrockLlamaChatOptions fromOptions(BedrockLlamaChatOptions fromOptions) {
return builder().withTemperature(fromOptions.getTemperature())
.withTopP(fromOptions.getTopP())
.withMaxGenLen(fromOptions.getMaxGenLen())
.build();
public static class Builder {
private BedrockLlamaChatOptions options = new BedrockLlamaChatOptions();
public Builder withTemperature(Double temperature) {
this.options.setTemperature(temperature);
return this;
}
public Builder withTopP(Double topP) {
this.options.setTopP(topP);
return this;
}
public Builder withMaxGenLen(Integer maxGenLen) {
this.options.setMaxGenLen(maxGenLen);
return this;
}
public BedrockLlamaChatOptions build() {
return this.options;
}
}
}

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,8 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.bedrock.llama.api;
import java.time.Duration;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
@@ -28,8 +31,6 @@ import org.springframework.ai.bedrock.llama.api.LlamaChatBedrockApi.LlamaChatReq
import org.springframework.ai.bedrock.llama.api.LlamaChatBedrockApi.LlamaChatResponse;
import org.springframework.ai.model.ChatModelDescription;
import java.time.Duration;
// @formatter:off
/**
* Java client for the Bedrock Llama chat model.
@@ -109,100 +110,14 @@ public class LlamaChatBedrockApi extends
super(modelId, credentialsProvider, region, objectMapper, timeout);
}
/**
* LlamaChatRequest encapsulates the request parameters for the Meta Llama chat model.
*
* @param prompt The prompt to use for the chat.
* @param temperature The temperature value controls the randomness of the generated text. Use a lower value to
* decrease randomness in the response.
* @param topP The topP value controls the diversity of the generated text. Use a lower value to ignore less
* probable options. Set to 0 or 1.0 to disable.
* @param maxGenLen The maximum length of the generated text.
*/
@JsonInclude(Include.NON_NULL)
public record LlamaChatRequest(
@JsonProperty("prompt") String prompt,
@JsonProperty("temperature") Double temperature,
@JsonProperty("top_p") Double topP,
@JsonProperty("max_gen_len") Integer maxGenLen) {
/**
* Create a new LlamaChatRequest builder.
* @param prompt compulsory prompt parameter.
* @return a new LlamaChatRequest builder.
*/
public static Builder builder(String prompt) {
return new Builder(prompt);
}
public static class Builder {
private String prompt;
private Double temperature;
private Double topP;
private Integer maxGenLen;
public Builder(String prompt) {
this.prompt = prompt;
}
public Builder withTemperature(Double temperature) {
this.temperature = temperature;
return this;
}
public Builder withTopP(Double topP) {
this.topP = topP;
return this;
}
public Builder withMaxGenLen(Integer maxGenLen) {
this.maxGenLen = maxGenLen;
return this;
}
public LlamaChatRequest build() {
return new LlamaChatRequest(
prompt,
temperature,
topP,
maxGenLen
);
}
}
@Override
public LlamaChatResponse chatCompletion(LlamaChatRequest request) {
return this.internalInvocation(request, LlamaChatResponse.class);
}
/**
* LlamaChatResponse encapsulates the response parameters for the Meta Llama chat model.
*
* @param generation The generated text.
* @param promptTokenCount The number of tokens in the prompt.
* @param generationTokenCount The number of tokens in the response.
* @param stopReason The reason why the response stopped generating text. Possible values are: (1) stop The model
* has finished generating text for the input prompt. (2) length The length of the tokens for the generated text
* exceeds the value of max_gen_len in the call. The response is truncated to max_gen_len tokens. Consider
* increasing the value of max_gen_len and trying again.
*/
@JsonInclude(Include.NON_NULL)
public record LlamaChatResponse(
@JsonProperty("generation") String generation,
@JsonProperty("prompt_token_count") Integer promptTokenCount,
@JsonProperty("generation_token_count") Integer generationTokenCount,
@JsonProperty("stop_reason") StopReason stopReason,
@JsonProperty("amazon-bedrock-invocationMetrics") AmazonBedrockInvocationMetrics amazonBedrockInvocationMetrics) {
/**
* The reason the response finished being generated.
*/
public enum StopReason {
/**
* The model has finished generating text for the input prompt.
*/
@JsonProperty("stop") STOP,
/**
* The response was truncated because of the response length you set.
*/
@JsonProperty("length") LENGTH
}
@Override
public Flux<LlamaChatResponse> chatCompletionStream(LlamaChatRequest request) {
return this.internalInvocationStream(request, LlamaChatResponse.class);
}
/**
@@ -267,15 +182,15 @@ public class LlamaChatBedrockApi extends
private final String id;
LlamaChatModel(String value) {
this.id = value;
}
/**
* @return The model id.
*/
public String id() {
return id;
}
LlamaChatModel(String value) {
this.id = value;
return this.id;
}
@Override
@@ -284,14 +199,100 @@ public class LlamaChatBedrockApi extends
}
}
@Override
public LlamaChatResponse chatCompletion(LlamaChatRequest request) {
return this.internalInvocation(request, LlamaChatResponse.class);
/**
* LlamaChatRequest encapsulates the request parameters for the Meta Llama chat model.
*
* @param prompt The prompt to use for the chat.
* @param temperature The temperature value controls the randomness of the generated text. Use a lower value to
* decrease randomness in the response.
* @param topP The topP value controls the diversity of the generated text. Use a lower value to ignore less
* probable options. Set to 0 or 1.0 to disable.
* @param maxGenLen The maximum length of the generated text.
*/
@JsonInclude(Include.NON_NULL)
public record LlamaChatRequest(
@JsonProperty("prompt") String prompt,
@JsonProperty("temperature") Double temperature,
@JsonProperty("top_p") Double topP,
@JsonProperty("max_gen_len") Integer maxGenLen) {
/**
* Create a new LlamaChatRequest builder.
* @param prompt compulsory prompt parameter.
* @return a new LlamaChatRequest builder.
*/
public static Builder builder(String prompt) {
return new Builder(prompt);
}
public static class Builder {
private String prompt;
private Double temperature;
private Double topP;
private Integer maxGenLen;
public Builder(String prompt) {
this.prompt = prompt;
}
public Builder withTemperature(Double temperature) {
this.temperature = temperature;
return this;
}
public Builder withTopP(Double topP) {
this.topP = topP;
return this;
}
public Builder withMaxGenLen(Integer maxGenLen) {
this.maxGenLen = maxGenLen;
return this;
}
public LlamaChatRequest build() {
return new LlamaChatRequest(
this.prompt,
this.temperature,
this.topP,
this.maxGenLen
);
}
}
}
@Override
public Flux<LlamaChatResponse> chatCompletionStream(LlamaChatRequest request) {
return this.internalInvocationStream(request, LlamaChatResponse.class);
/**
* LlamaChatResponse encapsulates the response parameters for the Meta Llama chat model.
*
* @param generation The generated text.
* @param promptTokenCount The number of tokens in the prompt.
* @param generationTokenCount The number of tokens in the response.
* @param stopReason The reason why the response stopped generating text. Possible values are: (1) stop The model
* has finished generating text for the input prompt. (2) length The length of the tokens for the generated text
* exceeds the value of max_gen_len in the call. The response is truncated to max_gen_len tokens. Consider
* increasing the value of max_gen_len and trying again.
*/
@JsonInclude(Include.NON_NULL)
public record LlamaChatResponse(
@JsonProperty("generation") String generation,
@JsonProperty("prompt_token_count") Integer promptTokenCount,
@JsonProperty("generation_token_count") Integer generationTokenCount,
@JsonProperty("stop_reason") StopReason stopReason,
@JsonProperty("amazon-bedrock-invocationMetrics") AmazonBedrockInvocationMetrics amazonBedrockInvocationMetrics) {
/**
* The reason the response finished being generated.
*/
public enum StopReason {
/**
* The model has finished generating text for the input prompt.
*/
@JsonProperty("stop") STOP,
/**
* The response was truncated because of the response length you set.
*/
@JsonProperty("length") LENGTH
}
}
}
// @formatter:on

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.bedrock.titan;
import java.util.List;
@@ -24,13 +25,13 @@ import org.springframework.ai.bedrock.titan.api.TitanChatBedrockApi;
import org.springframework.ai.bedrock.titan.api.TitanChatBedrockApi.TitanChatRequest;
import org.springframework.ai.bedrock.titan.api.TitanChatBedrockApi.TitanChatResponse;
import org.springframework.ai.bedrock.titan.api.TitanChatBedrockApi.TitanChatResponseChunk;
import org.springframework.ai.chat.metadata.ChatGenerationMetadata;
import org.springframework.ai.chat.metadata.Usage;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.model.StreamingChatModel;
import org.springframework.ai.chat.metadata.ChatGenerationMetadata;
import org.springframework.ai.chat.metadata.Usage;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.ModelOptionsUtils;
import org.springframework.util.Assert;

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.bedrock.titan;
import java.util.List;
@@ -20,11 +21,10 @@ import java.util.List;
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 com.fasterxml.jackson.annotation.JsonProperty;
/**
* @author Christian Tzolov
* @author Thomas Vitale
@@ -59,39 +59,17 @@ public class BedrockTitanChatOptions implements ChatOptions {
return new Builder();
}
public static class Builder {
private BedrockTitanChatOptions options = new BedrockTitanChatOptions();
public Builder withTemperature(Double temperature) {
this.options.temperature = temperature;
return this;
}
public Builder withTopP(Double topP) {
this.options.topP = topP;
return this;
}
public Builder withMaxTokenCount(Integer maxTokenCount) {
this.options.maxTokenCount = maxTokenCount;
return this;
}
public Builder withStopSequences(List<String> stopSequences) {
this.options.stopSequences = stopSequences;
return this;
}
public BedrockTitanChatOptions build() {
return this.options;
}
public static BedrockTitanChatOptions fromOptions(BedrockTitanChatOptions fromOptions) {
return builder().withTemperature(fromOptions.getTemperature())
.withTopP(fromOptions.getTopP())
.withMaxTokenCount(fromOptions.getMaxTokenCount())
.withStopSequences(fromOptions.getStopSequences())
.build();
}
@Override
public Double getTemperature() {
return temperature;
return this.temperature;
}
public void setTemperature(Double temperature) {
@@ -100,7 +78,7 @@ public class BedrockTitanChatOptions implements ChatOptions {
@Override
public Double getTopP() {
return topP;
return this.topP;
}
public void setTopP(Double topP) {
@@ -119,7 +97,7 @@ public class BedrockTitanChatOptions implements ChatOptions {
}
public Integer getMaxTokenCount() {
return maxTokenCount;
return this.maxTokenCount;
}
public void setMaxTokenCount(Integer maxTokenCount) {
@@ -128,7 +106,7 @@ public class BedrockTitanChatOptions implements ChatOptions {
@Override
public List<String> getStopSequences() {
return stopSequences;
return this.stopSequences;
}
public void setStopSequences(List<String> stopSequences) {
@@ -164,12 +142,34 @@ public class BedrockTitanChatOptions implements ChatOptions {
return fromOptions(this);
}
public static BedrockTitanChatOptions fromOptions(BedrockTitanChatOptions fromOptions) {
return builder().withTemperature(fromOptions.getTemperature())
.withTopP(fromOptions.getTopP())
.withMaxTokenCount(fromOptions.getMaxTokenCount())
.withStopSequences(fromOptions.getStopSequences())
.build();
public static class Builder {
private BedrockTitanChatOptions options = new BedrockTitanChatOptions();
public Builder withTemperature(Double temperature) {
this.options.temperature = temperature;
return this;
}
public Builder withTopP(Double topP) {
this.options.topP = topP;
return this;
}
public Builder withMaxTokenCount(Integer maxTokenCount) {
this.options.maxTokenCount = maxTokenCount;
return this;
}
public Builder withStopSequences(List<String> stopSequences) {
this.options.stopSequences = stopSequences;
return this;
}
public BedrockTitanChatOptions build() {
return this.options;
}
}
}

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