Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
b745c76
Define API for AI provider metadata collected during an AI request.
jxblum Nov 9, 2023
d245952
Simplify AiResponse and Generation classes.
jxblum Nov 9, 2023
43023c7
Define metadata property in AiResponse.
jxblum Nov 9, 2023
8f0bbb4
Fix spelling mistakes in comments of pom.xml.
jxblum Nov 10, 2023
6243cc7
Add OpenAI implementations of AiMetadata, RateLimit and Usage interfa…
jxblum Nov 10, 2023
187582c
Add REST Assured JsonPath dependency to spring-ai-openai module.
jxblum Nov 10, 2023
cb9ce22
Add OkHttp dependency to spring-ai-openai module.
jxblum Nov 10, 2023
c02f93d
Add OkHttp Interceptor to parse OpenAI rate limit metadata from HTTP …
jxblum Nov 10, 2023
28e1df7
Integrate OpenAI API response metadata collection into OpenAiClient.
jxblum Nov 10, 2023
df6513e
Add OkHttp MockWebServer dependency to spring-ai-openai module.
jxblum Nov 10, 2023
b18d0cb
Add Jakarta Servlet API dependency to spring-ai-openai module.
jxblum Nov 10, 2023
90f4395
Add Spring Web MVC dependency to spring-ai-open-ai module.
jxblum Nov 10, 2023
1a0adf4
Define OpenAI API response headers in an Enum.
jxblum Nov 10, 2023
d7d125b
Add OpenAI test configuration using mock objects.
jxblum Nov 10, 2023
b9af509
Add integration test to assert successful extraction of OpenAI API re…
jxblum Nov 14, 2023
ddc57fe
Include Spring Boot auto-configuration for (conditional) OpenAI metad…
jxblum Nov 15, 2023
af118c2
Edit documentation and include information on AI metadata collected b…
jxblum Nov 15, 2023
c7a1364
Rename AiMetadata to GenerationMetadata.
jxblum Nov 15, 2023
8ff3f80
Implement the Null Object Pattern for absent RateLimit and Usage meta…
jxblum Nov 15, 2023
714db57
Repackage AI metadata under org.springframework.ai.metadata.
jxblum Nov 15, 2023
ea9cea8
Edit documentation on AI metadata to match API.
jxblum Nov 15, 2023
2c994e2
Provide AI metadata implementation for Microsoft Azure OpenAI Service.
jxblum Nov 15, 2023
9e16051
Define metadata for an AI prompt.
jxblum Nov 21, 2023
583fdc2
Rename getMetadata() method to getGenerationMetadata() in AiResponse.
jxblum Nov 21, 2023
f0ccfda
Capture optional PromptMetadata in AiResponse.
jxblum Nov 21, 2023
0babe21
Capture prompt metadata from Azure OpenAI.
jxblum Nov 21, 2023
402e847
Define metadata for an AI generation choice.
jxblum Nov 21, 2023
bf84a40
Capture AI choice metadata in Generation.
jxblum Nov 21, 2023
9ea9c42
Integrate ChoiceMetadata into AiResponse returned by OpenAI.
jxblum Nov 21, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -79,15 +79,15 @@
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>

<!-- prodution dependencies -->
<!-- production dependencies -->
<spring-boot.version>3.1.3</spring-boot.version>
<stringtemplate.version>4.0.2</stringtemplate.version>
<open-ai-client.version>0.16.0</open-ai-client.version>
<azure-open-ai-client.version>1.0.0-beta.3</azure-open-ai-client.version>
<jtokkit.version>0.6.1</jtokkit.version>
<victools.version>4.31.1</victools.version>

<!-- readers/writer/stores dependecies-->
<!-- readers/writer/stores dependencies-->
<pdfbox.version>3.0.0</pdfbox.version>
<pgvector.version>0.1.3</pgvector.version>
<postgresql.version>42.6.0</postgresql.version>
Expand All @@ -97,7 +97,7 @@
<fastjson.version>2.0.42</fastjson.version>
<azure-search.version>11.6.0</azure-search.version>

<!-- testing dependecies -->
<!-- testing dependencies -->
<testcontainers.version>1.19.0</testcontainers.version>

<!-- documentation dependencies -->
Expand Down Expand Up @@ -409,4 +409,4 @@
</plugins>
</reporting>

</project>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,12 @@
import com.azure.ai.openai.models.ChatRole;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.azure.openai.metadata.AzureOpenAiGenerationMetadata;
import org.springframework.ai.client.AiClient;
import org.springframework.ai.client.AiResponse;
import org.springframework.ai.client.Generation;
import org.springframework.ai.metadata.PromptMetadata;
import org.springframework.ai.metadata.PromptMetadata.PromptFilterMetadata;
import org.springframework.ai.prompt.Prompt;
import org.springframework.ai.prompt.messages.Message;
import org.springframework.util.Assert;
Expand Down Expand Up @@ -124,13 +127,22 @@ public AiResponse generate(Prompt prompt) {

for (ChatChoice choice : chatCompletions.getChoices()) {
ChatMessage choiceMessage = choice.getMessage();
// TODO investigate mapping of additional metadata/runtime info to the general
// model.
Generation generation = new Generation(choiceMessage.getContent());
generations.add(generation);
}

return new AiResponse(generations);
return new AiResponse(generations, AzureOpenAiGenerationMetadata.from(chatCompletions))
.withPromptMetadata(generatePromptMetadata(chatCompletions));
}

private PromptMetadata generatePromptMetadata(ChatCompletions chatCompletions) {

return PromptMetadata.of(chatCompletions.getPromptFilterResults()
.stream()
.map(promptFilterResult -> PromptFilterMetadata.from(promptFilterResult.getPromptIndex(),
promptFilterResult.getContentFilterResults()))
.toList());

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright 2023 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.ChatCompletions;

import org.springframework.ai.metadata.GenerationMetadata;
import org.springframework.ai.metadata.Usage;
import org.springframework.util.Assert;

/**
* {@link GenerationMetadata} implementation for
* {@literal Microsoft Azure OpenAI Service}.
*
* @author John Blum
* @see org.springframework.ai.metadata.GenerationMetadata
* @since 0.7.1
*/
public class AzureOpenAiGenerationMetadata implements GenerationMetadata {

protected static final String AI_METADATA_STRING = "{ @type: %1$s, id: %2$s, usage: %3$s, rateLimit: %4$s }";

@SuppressWarnings("all")
public static AzureOpenAiGenerationMetadata from(ChatCompletions chatCompletions) {
Assert.notNull(chatCompletions, "Azure OpenAI ChatCompletions must not be null");
String id = chatCompletions.getId();
AzureOpenAiUsage usage = AzureOpenAiUsage.from(chatCompletions);
AzureOpenAiGenerationMetadata generationMetadata = new AzureOpenAiGenerationMetadata(id, usage);
return generationMetadata;
}

private final String id;

private final Usage usage;

protected AzureOpenAiGenerationMetadata(String id, AzureOpenAiUsage usage) {
this.id = id;
this.usage = usage;
}

public String getId() {
return this.id;
}

@Override
public Usage getUsage() {
return this.usage;
}

@Override
public String toString() {
return AI_METADATA_STRING.formatted(getClass().getTypeName(), getId(), getUsage(), getRateLimit());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright 2023 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.ChatCompletions;
import com.azure.ai.openai.models.CompletionsUsage;

import org.springframework.ai.metadata.Usage;
import org.springframework.util.Assert;

/**
* {@link Usage} implementation for {@literal Microsoft Azure OpenAI Service}.
*
* @author John Blum
* @see com.azure.ai.openai.models.CompletionsUsage
* @since 0.7.0
*/
public class AzureOpenAiUsage implements Usage {

public static AzureOpenAiUsage from(ChatCompletions chatCompletions) {
Assert.notNull(chatCompletions, "ChatCompletions must not be null");
return from(chatCompletions.getUsage());
}

public static AzureOpenAiUsage from(CompletionsUsage usage) {
return new AzureOpenAiUsage(usage);
}

private final CompletionsUsage usage;

public AzureOpenAiUsage(CompletionsUsage usage) {
Assert.notNull(usage, "CompletionUsage must not be null");
this.usage = usage;
}

protected CompletionsUsage getUsage() {
return this.usage;
}

@Override
public Long getPromptTokens() {
return Integer.valueOf(getUsage().getPromptTokens()).longValue();
}

@Override
public Long getGenerationTokens() {
return Integer.valueOf(getUsage().getCompletionTokens()).longValue();
}

@Override
public Long getTotalTokens() {
return Integer.valueOf(getUsage().getTotalTokens()).longValue();
}

@Override
public String toString() {
return getUsage().toString();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,53 +19,108 @@
import java.util.List;
import java.util.Map;

import org.springframework.ai.metadata.GenerationMetadata;
import org.springframework.ai.metadata.PromptMetadata;
import org.springframework.lang.Nullable;

public class AiResponse {

private final GenerationMetadata metadata;

private final List<Generation> generations;

private Map<String, Object> providerOutput;
private final Map<String, Object> providerOutput;

private final Map<String, Object> runInfo;

private Map<String, Object> runInfo;
private PromptMetadata promptMetadata;

public AiResponse(List<Generation> generations) {
this(generations, Collections.emptyMap(), Collections.emptyMap());
this(generations, Collections.emptyMap(), Collections.emptyMap(), GenerationMetadata.NULL);
}

public AiResponse(List<Generation> generations, GenerationMetadata metadata) {
this(generations, Collections.emptyMap(), Collections.emptyMap(), metadata);
}

public AiResponse(List<Generation> generations, Map<String, Object> providerOutput) {
this(generations, providerOutput, Collections.emptyMap());
this(generations, providerOutput, Collections.emptyMap(), GenerationMetadata.NULL);
}

public AiResponse(List<Generation> generations, Map<String, Object> providerOutput, Map<String, Object> runInfo) {
this(generations, providerOutput, runInfo, GenerationMetadata.NULL);
}

public AiResponse(List<Generation> generations, Map<String, Object> providerOutput, Map<String, Object> runInfo,
GenerationMetadata metadata) {

this.metadata = metadata;
this.generations = List.copyOf(generations);
this.providerOutput = Map.copyOf(providerOutput);
this.runInfo = Map.copyOf(runInfo);
}

/**
* The list of generated outputs. It is a list of lists because the Prompt could
* request multiple output generations.
* @return
* The {@link List} of {@link Generation generated outputs}.
* <p>
* It is a {@link List} of {@link List lists} because the Prompt could request
* multiple output {@link Generation generations}.
* @return the {@link List} of {@link Generation generated outputs}.
*/
public List<Generation> getGenerations() {
return Collections.unmodifiableList(generations);
return this.generations;
}

public Generation getGeneration() {
return this.generations.get(0);
}

/**
* Returns {@link GenerationMetadata} containing information about the use of the AI
* provider's API.
* @return {@link GenerationMetadata} containing information about the use of the AI
* provider's API.
*/
public GenerationMetadata getGenerationMetadata() {
return this.metadata;
}

/**
* Returns {@link PromptMetadata} containing information on prompt processing by the
* AI.
* @return {@link PromptMetadata} containing information on prompt processing by the
* AI.
*/
public PromptMetadata getPromptMetadata() {
PromptMetadata promptMetadata = this.promptMetadata;
return promptMetadata != null ? promptMetadata : PromptMetadata.empty();
}

/**
* Arbitrary model provider specific output
*/
public Map<String, Object> getProviderOutput() {
return Collections.unmodifiableMap(providerOutput);
return this.providerOutput;
}

/**
* The run metadata information
*/
public Map<String, Object> getRunInfo() {
return Collections.unmodifiableMap(runInfo);
return this.runInfo;
}

/**
* Builder method used to include {@link PromptMetadata} returned in the AI response
* when processing the prompt.
* @param promptMetadata {@link PromptMetadata} returned by the AI in the response
* when processing the prompt.
* @return this {@link AiResponse}.
* @see #getPromptMetadata()
*/
public AiResponse withPromptMetadata(@Nullable PromptMetadata promptMetadata) {
this.promptMetadata = promptMetadata;
return this;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,28 +19,43 @@
import java.util.Collections;
import java.util.Map;

import org.springframework.ai.metadata.ChoiceMetadata;
import org.springframework.lang.Nullable;

public class Generation {

// Just text for now
private final String text;

private Map<String, Object> info;

private ChoiceMetadata choiceMetadata;

public Generation(String text) {
this(text, Collections.emptyMap());
}

public Generation(String text, Map<String, Object> info) {
this.text = text;
this.info = info;
this.info = Map.copyOf(info);
}

public String getText() {
return this.text;
}

public Map<String, Object> getInfo() {
return Collections.unmodifiableMap(this.info);
return this.info;
}

public ChoiceMetadata getChoiceMetadata() {
ChoiceMetadata choiceMetadata = this.choiceMetadata;
return choiceMetadata != null ? choiceMetadata : ChoiceMetadata.NULL;
}

public Generation withChoiceMetadata(@Nullable ChoiceMetadata choiceMetadata) {
this.choiceMetadata = choiceMetadata;
return this;
}

@Override
Expand Down
Loading