lm-lib: Merged with tp fork;
git-svn-id: svn://home0/lm-lib@3723 71dcc7fd-b1f2-4e5d-bb8e-274146e81173
This commit is contained in:
152
README.md
152
README.md
@@ -1,21 +1,151 @@
|
|||||||
This suite of Java packages provides a comprehensive framework for building and deploying conversational agents powered by language models.
|
# Purpose
|
||||||
|
|
||||||
**Purpose:**
|
The purpose of this application is to provide a comprehensive conversational AI system that enables efficient and effective human-computer interactions through natural language processing, conversation management, and text completion capabilities, utilizing caching, tokenization, and integration with external AI models and services to deliver a robust and scalable solution for applications such as chatbots, virtual assistants, and customer service platforms.
|
||||||
|
|
||||||
The primary purpose is to simplify the development and management of AI-driven chatbot applications.
|
# Program Description
|
||||||
|
|
||||||
**Functionality:**
|
The program is a comprehensive conversational AI system that provides natural language processing functionality, conversation management, and text completion capabilities. The purpose of the program is to enable efficient and effective human-computer interactions through AI-powered conversations.
|
||||||
|
|
||||||
* **Agent Framework:** The core package enables the creation of agents capable of interacting with language models, processing text, maintaining conversation context, tracking statistics, and executing in a structured manner. It supports chaining agents, handling responses, and flexible configuration.
|
The program consists of several packages that work together to achieve this goal. These packages include:
|
||||||
* **Language Model Interaction:** Several packages provide tools for interacting with various language models, including Azure AI's text generation service and remote text completion models. These tools handle tasks like retrieving models, managing conversations, generating completions, and tracking API usage.
|
|
||||||
* **Prompt Engineering:** A dedicated package focuses on constructing and manipulating prompts tailored to specific language models. It supports different formatting strategies and allows for fine-grained control over prompt structure.
|
|
||||||
* **Tokenization:** Another package handles text tokenization and detokenization, crucial for preparing text for input to language models and interpreting their outputs.
|
|
||||||
|
|
||||||
**Overall, this collection of packages offers a powerful and flexible toolkit for developers looking to build sophisticated conversational agents leveraging the capabilities of modern language models.**
|
1. **Caching layer (com.transporeon.exchange.lmlib.client.cached)**: Improves performance by reducing requests to the underlying client through caching and retrieving completion responses.
|
||||||
|
2. **Natural Language Processing (com.transporeon.exchange.lmlib.client)**: Provides conversation management and text completion functionality, generating completions, processing conversations, and managing models.
|
||||||
|
3. **Azure AI Integration (com.transporeon.exchange.lmlib.client.azureai)**: Integrates with Azure AI services, managing model profiles and handling conversation completions.
|
||||||
|
4. **Text Completion (com.transporeon.exchange.lmlib.client.textcompletion)**: Provides text completion functionality using AI models, including model selection, prompt generation, and response handling.
|
||||||
|
5. **Text Tokenization (com.transporeon.exchange.lmlib.token)**: Tokenizes text into numerical tokens and detokenizes numerical tokens back into human-readable text.
|
||||||
|
6. **Conversation Prompts (com.transporeon.exchange.lmlib.prompt)**: Manages conversation prompts, creating new prompts, continuing conversations, and removing end-of-text markers.
|
||||||
|
7. **Agent Executor (com.transporeon.exchange.lmlib.agent)**: Executes agent graphs concurrently, handles agent responses, and tracks request statistics.
|
||||||
|
|
||||||
*gemma2-27b requests=50, requestDuration=PT3M36.921314511S, inTokens=26737, outTokens=2207, tps=10.217592592592593*
|
The program's key features include:
|
||||||
|
|
||||||
*[Below announcement has been generated by the unit test AgentTest#agenticFlow:]*
|
* Agent graph execution
|
||||||
|
* Conversation management and text completion
|
||||||
|
* Integration with external AI models and services (e.g., Azure AI)
|
||||||
|
* Support for multiple prompt strategies and tokenization implementations
|
||||||
|
* Caching and performance optimization
|
||||||
|
|
||||||
|
Overall, the program is designed to provide a robust and efficient conversational AI system that can be used in various applications, such as chatbots, virtual assistants, and customer service platforms.
|
||||||
|
|
||||||
|
*AgentRequestStatistic [requests=43, cachedPrompts=2, inTokens=23437, outTokens=7403, requestDuration=PT2M31.7349284S, getEffectiveDuration()=PT1M37.2400495S, getTps()=317.15, getOutTps()=76.13, getCost()=0.01458674]*
|
||||||
|
|
||||||
|
# Setup
|
||||||
|
|
||||||
|
To keep this library lean and compatible with different dependency trees, all dependencies must be provided by the application.
|
||||||
|
|
||||||
|
## Mandatory dependencies
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
## Optional dependencies
|
||||||
|
|
||||||
|
Some services benefit from Spring cache framework, e.g. the model profile service:
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-cache</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
Don't forget to activate a CacheManager afterwards:
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public CacheManager cacheManager() {
|
||||||
|
return new CaffeineCacheManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
## Model profiles
|
||||||
|
|
||||||
|
Models are managed with profiles. You can maintain multiple profiles for different models and parameters such as temperature.
|
||||||
|
|
||||||
|
These profiles must be mapped to fixed categories with the following application properties:
|
||||||
|
|
||||||
|
profile.model.s=
|
||||||
|
profile.model.m=
|
||||||
|
profile.model.l=
|
||||||
|
profile.model.hot=
|
||||||
|
profile.model.cold=
|
||||||
|
|
||||||
|
The value is the name of the model profile .properties file, for example "llama3.3-70b-azure".
|
||||||
|
|
||||||
|
Profiles must be located in the main resources location.
|
||||||
|
|
||||||
|
Examples can be found in test resources.
|
||||||
|
|
||||||
|
## OpenAi compatible text completion client
|
||||||
|
|
||||||
|
Use tokenizer strategy jtokkit or llamaServer (if the model is hosted on a llama.cpp server)
|
||||||
|
|
||||||
|
Property:
|
||||||
|
|
||||||
|
tokenizer.strategy=llamaServer
|
||||||
|
|
||||||
|
If you use jtokkit tokenizer:
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.knuddels</groupId>
|
||||||
|
<artifactId>jtokkit</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
It is required to select the right prompt template. See package com.transporeon.exchange.lmlib.prompt for available prompt templates or implement your own PromptStrategy (must be a bean in the spring context).
|
||||||
|
|
||||||
|
Set the following property in your model profile:
|
||||||
|
|
||||||
|
prompts=llama3
|
||||||
|
|
||||||
|
Example profile: src/test/resources/llama3-local.properties
|
||||||
|
|
||||||
|
## Azure AI client
|
||||||
|
|
||||||
|
Use tokenizer strategy jtokkit.
|
||||||
|
|
||||||
|
Property:
|
||||||
|
|
||||||
|
tokenizer.strategy=jtokkit
|
||||||
|
|
||||||
|
Example profile: src/test/resources/llama3.3-70b-azure.properties
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.azure</groupId>
|
||||||
|
<artifactId>azure-ai-inference</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.squareup.okhttp3</groupId>
|
||||||
|
<artifactId>okhttp</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.knuddels</groupId>
|
||||||
|
<artifactId>jtokkit</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||||
|
<artifactId>caffeine</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
# Completions Cache
|
||||||
|
|
||||||
|
All language model requests will be cached if you add a CachedClientRepository bean to your context.
|
||||||
|
|
||||||
|
# Agentic Flows
|
||||||
|
|
||||||
|
This library contains a graph executor for implementing comprehensive processes using agent-based design patterns. See class AgentTest for examples.
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
|
||||||
|
1. Setup dependencies, see above
|
||||||
|
2. Configure model profiles, see above
|
||||||
|
3. Implement a CachedClientRepository if you need your requests cached
|
||||||
|
4. For chats or completions, use ClientService service bean
|
||||||
|
5. For agentic flows, create an AgentContext and use AgentExecutorService service bean
|
||||||
|
|
||||||
|
# Examples
|
||||||
|
|
||||||
|
Text completions: AzureAiClientTest#completion
|
||||||
|
Chat: AzureAiClientTest#chat
|
||||||
|
Agentic flow: AgentTest#agenticFlow
|
||||||
|
|
||||||
|
# Announcement generated by AgentTest#agenticFlow
|
||||||
|
|
||||||
Title: Introducing a Powerful Framework for Building Intelligent Agents
|
Title: Introducing a Powerful Framework for Building Intelligent Agents
|
||||||
|
|
||||||
|
|||||||
4
pom.xml
4
pom.xml
@@ -4,13 +4,13 @@
|
|||||||
<artifactId>lm-lib</artifactId>
|
<artifactId>lm-lib</artifactId>
|
||||||
<groupId>de.extio</groupId>
|
<groupId>de.extio</groupId>
|
||||||
<name>lm-lib</name>
|
<name>lm-lib</name>
|
||||||
<version>1.0.0</version>
|
<version>1.1.0</version>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<java.version>21</java.version>
|
<java.version>21</java.version>
|
||||||
<maven.compiler.source>21</maven.compiler.source>
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
<maven.compiler.target>21</maven.compiler.target>
|
<maven.compiler.target>21</maven.compiler.target>
|
||||||
<spring-boot.version>3.4.1</spring-boot.version>
|
<spring-boot.version>3.4.2</spring-boot.version>
|
||||||
<azureai.version>1.0.0-beta.2</azureai.version>
|
<azureai.version>1.0.0-beta.2</azureai.version>
|
||||||
<jtokkit.version>1.1.0</jtokkit.version>
|
<jtokkit.version>1.1.0</jtokkit.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import org.springframework.beans.factory.annotation.Autowired;
|
|||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import de.extio.lmlib.client.Client;
|
||||||
import de.extio.lmlib.client.ClientService;
|
import de.extio.lmlib.client.ClientService;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@@ -47,10 +48,14 @@ public class AgentExecutorService implements InitializingBean, DisposableBean {
|
|||||||
public List<AgentContext> walkGraph(final Agent agent, final AgentContext context) {
|
public List<AgentContext> walkGraph(final Agent agent, final AgentContext context) {
|
||||||
LOGGER.info("Agent: {}", agent.name());
|
LOGGER.info("Agent: {}", agent.name());
|
||||||
|
|
||||||
final var client = this.clientService.getClient(agent.modelCategory());
|
Client client = null;
|
||||||
final var responses = Collections.synchronizedList(new ArrayList<AgentContext>());
|
if (agent.agentType() != AgentType.PROCESSING_ONLY) {
|
||||||
|
client = this.clientService.getClient(agent.modelCategory());
|
||||||
|
}
|
||||||
|
|
||||||
final var branches = agent.execute(client, this.agentExecutorService, context);
|
final var branches = agent.execute(client, this.agentExecutorService, context);
|
||||||
|
|
||||||
|
final var responses = Collections.synchronizedList(new ArrayList<AgentContext>());
|
||||||
final var branchTasks = new ArrayList<CompletableFuture<?>>(branches.size());
|
final var branchTasks = new ArrayList<CompletableFuture<?>>(branches.size());
|
||||||
for (final var branchContext : branches) {
|
for (final var branchContext : branches) {
|
||||||
if (!branchContext.isError() && branchContext.getNextAgent() != null) {
|
if (!branchContext.isError() && branchContext.getNextAgent() != null) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package de.extio.lmlib.agent;
|
package de.extio.lmlib.agent;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.text.DecimalFormat;
|
import java.text.DecimalFormat;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
@@ -12,18 +13,26 @@ public final class AgentRequestStatistic {
|
|||||||
|
|
||||||
private final AtomicInteger requests = new AtomicInteger();
|
private final AtomicInteger requests = new AtomicInteger();
|
||||||
|
|
||||||
|
private final AtomicInteger cachedPrompts = new AtomicInteger();
|
||||||
|
|
||||||
private final AtomicLong inTokens = new AtomicLong();
|
private final AtomicLong inTokens = new AtomicLong();
|
||||||
|
|
||||||
private final AtomicLong outTokens = new AtomicLong();
|
private final AtomicLong outTokens = new AtomicLong();
|
||||||
|
|
||||||
private final Instant start = Instant.now();
|
private volatile Instant start = Instant.now();
|
||||||
|
|
||||||
private volatile Duration requestDuration = Duration.ofMillis(0L);
|
private volatile Duration requestDuration = Duration.ofMillis(0L);
|
||||||
|
|
||||||
|
private volatile BigDecimal cost = BigDecimal.ZERO;
|
||||||
|
|
||||||
public AtomicInteger getRequests() {
|
public AtomicInteger getRequests() {
|
||||||
return this.requests;
|
return this.requests;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AtomicInteger getCachedPrompts() {
|
||||||
|
return this.cachedPrompts;
|
||||||
|
}
|
||||||
|
|
||||||
public AtomicLong getInTokens() {
|
public AtomicLong getInTokens() {
|
||||||
return this.inTokens;
|
return this.inTokens;
|
||||||
}
|
}
|
||||||
@@ -48,15 +57,37 @@ public final class AgentRequestStatistic {
|
|||||||
return new DecimalFormat("#.##").format((double) this.outTokens.get() / (double) this.getEffectiveDuration().toMillis() * 1000.0);
|
return new DecimalFormat("#.##").format((double) this.outTokens.get() / (double) this.getEffectiveDuration().toMillis() * 1000.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void add(final Completion completion) {
|
public BigDecimal getCost() {
|
||||||
this.getRequests().addAndGet(completion.requests());
|
return this.cost;
|
||||||
this.getInTokens().addAndGet(completion.inTokens());
|
|
||||||
this.getOutTokens().addAndGet(completion.outTokens());
|
|
||||||
this.addRequestDuration(completion.duration());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void addRequestDuration(final Duration duration) {
|
public void add(final Completion completion) {
|
||||||
this.requestDuration = this.requestDuration.plus(duration);
|
if (completion.cached()) {
|
||||||
|
this.cachedPrompts.incrementAndGet();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.getRequests().addAndGet(completion.requests());
|
||||||
|
}
|
||||||
|
this.getInTokens().addAndGet(completion.inTokens());
|
||||||
|
this.getOutTokens().addAndGet(completion.outTokens());
|
||||||
|
synchronized (this) {
|
||||||
|
this.requestDuration = this.requestDuration.plus(completion.duration());
|
||||||
|
this.cost = this.cost.add(completion.cost());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void add(final AgentRequestStatistic other) {
|
||||||
|
this.getRequests().addAndGet(other.getRequests().get());
|
||||||
|
this.getCachedPrompts().addAndGet(other.getCachedPrompts().get());
|
||||||
|
this.getInTokens().addAndGet(other.getInTokens().get());
|
||||||
|
this.getOutTokens().addAndGet(other.getOutTokens().get());
|
||||||
|
synchronized (this) {
|
||||||
|
if (other.start.isBefore(this.start)) {
|
||||||
|
this.start = other.start;
|
||||||
|
}
|
||||||
|
this.requestDuration = this.requestDuration.plus(other.getRequestDuration());
|
||||||
|
this.cost = this.cost.add(other.getCost());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -64,6 +95,8 @@ public final class AgentRequestStatistic {
|
|||||||
final StringBuilder builder = new StringBuilder();
|
final StringBuilder builder = new StringBuilder();
|
||||||
builder.append("AgentRequestStatistic [requests=");
|
builder.append("AgentRequestStatistic [requests=");
|
||||||
builder.append(this.requests);
|
builder.append(this.requests);
|
||||||
|
builder.append(", cachedPrompts=");
|
||||||
|
builder.append(this.cachedPrompts);
|
||||||
builder.append(", inTokens=");
|
builder.append(", inTokens=");
|
||||||
builder.append(this.inTokens);
|
builder.append(this.inTokens);
|
||||||
builder.append(", outTokens=");
|
builder.append(", outTokens=");
|
||||||
@@ -76,6 +109,8 @@ public final class AgentRequestStatistic {
|
|||||||
builder.append(this.getTps());
|
builder.append(this.getTps());
|
||||||
builder.append(", getOutTps()=");
|
builder.append(", getOutTps()=");
|
||||||
builder.append(this.getOutTps());
|
builder.append(this.getOutTps());
|
||||||
|
builder.append(", getCost()=");
|
||||||
|
builder.append(this.getCost());
|
||||||
builder.append("]");
|
builder.append("]");
|
||||||
return builder.toString();
|
return builder.toString();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,11 @@ package de.extio.lmlib.client;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.cache.annotation.Cacheable;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import de.extio.lmlib.client.cached.CachedClient;
|
||||||
|
import de.extio.lmlib.client.cached.CachedClientRepository;
|
||||||
import de.extio.lmlib.profile.ModelCategory;
|
import de.extio.lmlib.profile.ModelCategory;
|
||||||
import de.extio.lmlib.profile.ModelProfileService;
|
import de.extio.lmlib.profile.ModelProfileService;
|
||||||
|
|
||||||
@@ -17,11 +20,16 @@ public class ClientService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private ModelProfileService modelProfileService;
|
private ModelProfileService modelProfileService;
|
||||||
|
|
||||||
|
@Autowired(required = false)
|
||||||
|
private CachedClientRepository cachedClientRepository;
|
||||||
|
|
||||||
|
@Cacheable("clients")
|
||||||
public Client getClient(final ModelCategory category) {
|
public Client getClient(final ModelCategory category) {
|
||||||
final var modelProfile = this.modelProfileService.getModelProfile(category.getModelProfile());
|
final var modelProfile = this.modelProfileService.getModelProfile(category.getModelProfile());
|
||||||
return this.clients.stream()
|
return this.clients.stream()
|
||||||
.filter(client -> client.getModelProvider() == modelProfile.modelProvider())
|
.filter(client -> client.getModelProvider() == modelProfile.modelProvider())
|
||||||
.findFirst()
|
.findFirst()
|
||||||
|
.map(client -> this.cachedClientRepository == null ? client : new CachedClient(this.cachedClientRepository, this.modelProfileService, client))
|
||||||
.orElseThrow(() -> new IllegalStateException("No client found for model profile " + category));
|
.orElseThrow(() -> new IllegalStateException("No client found for model profile " + category));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
package de.extio.lmlib.client;
|
package de.extio.lmlib.client;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
|
||||||
public final record Completion(String response, int requests, Duration duration, long inTokens, long outTokens, boolean cached) {
|
public final record Completion(String response, int requests, Duration duration, long inTokens, long outTokens, BigDecimal cost, boolean cached) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package de.extio.lmlib.client.azureai;
|
package de.extio.lmlib.client.azureai;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.ArrayDeque;
|
import java.util.ArrayDeque;
|
||||||
@@ -108,6 +109,7 @@ public final class AzureAiClient implements Client {
|
|||||||
statistics.duration,
|
statistics.duration,
|
||||||
statistics.inTokens,
|
statistics.inTokens,
|
||||||
statistics.outTokens,
|
statistics.outTokens,
|
||||||
|
new BigDecimal(statistics.inTokens).multiply(modelProfile.costInToken()).add(new BigDecimal(statistics.outTokens).multiply(modelProfile.costOutToken())),
|
||||||
false);
|
false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +122,7 @@ public final class AzureAiClient implements Client {
|
|||||||
Turn assistantTurn = null;
|
Turn assistantTurn = null;
|
||||||
boolean first = true;
|
boolean first = true;
|
||||||
|
|
||||||
final int maxLength = modelProfile.maxContextLength() - modelProfile.maxTokens() - 50; // 50 is a margin for EOT and other special tokens
|
final int maxLength = modelProfile.maxContextLength() - modelProfile.maxTokens() - 15; // 15 is a margin for EOT and other special tokens
|
||||||
final AtomicInteger tokens = new AtomicInteger();
|
final AtomicInteger tokens = new AtomicInteger();
|
||||||
|
|
||||||
while (!turns.isEmpty()) {
|
while (!turns.isEmpty()) {
|
||||||
@@ -165,21 +167,19 @@ public final class AzureAiClient implements Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean addChatRequestMessage(final String text, final int maxLength, final AtomicInteger tokens, final List<ChatRequestMessage> chat, final ModelProfile modelProfile, final Function<String, ChatRequestMessage> messageFactory) {
|
private boolean addChatRequestMessage(final String text, final int maxLength, final AtomicInteger tokens, final List<ChatRequestMessage> chat, final ModelProfile modelProfile, final Function<String, ChatRequestMessage> messageFactory) {
|
||||||
final int textTokens = this.tokenizer.count(text, modelProfile);
|
String prompt = text;
|
||||||
|
|
||||||
String trimmedText;
|
|
||||||
boolean trimmed = false;
|
boolean trimmed = false;
|
||||||
|
|
||||||
|
final int textTokens = this.tokenizer.count(prompt, modelProfile);
|
||||||
if (tokens.addAndGet(textTokens) > maxLength) {
|
if (tokens.addAndGet(textTokens) > maxLength) {
|
||||||
// TODO: Cut smarter, e.g. split conversation into parts while keeping system prompt
|
// TODO: Cut smarter, e.g. split conversation into parts while keeping system prompt
|
||||||
LOGGER.warn("Prompt too long: {} tokens / {} max. Prompt will be cut!", tokens.get(), maxLength);
|
List<Long> tokenized = this.tokenizer.tokenize(prompt, modelProfile);
|
||||||
trimmedText = text.substring(0, text.length() - (tokens.get() - maxLength) / 3);
|
LOGGER.warn("Prompt too long: {} tokens / {} max. Prompt will be cut!", tokenized.size(), maxLength);
|
||||||
|
tokenized = tokenized.subList(0, tokenized.size() - (tokens.get() - maxLength));
|
||||||
|
prompt = this.tokenizer.detokenize(tokenized, modelProfile);
|
||||||
trimmed = true;
|
trimmed = true;
|
||||||
}
|
}
|
||||||
else {
|
chat.add(messageFactory.apply(prompt));
|
||||||
trimmedText = text;
|
|
||||||
}
|
|
||||||
|
|
||||||
chat.add(messageFactory.apply(trimmedText));
|
|
||||||
|
|
||||||
return trimmed;
|
return trimmed;
|
||||||
}
|
}
|
||||||
@@ -246,7 +246,7 @@ public final class AzureAiClient implements Client {
|
|||||||
long outTokens;
|
long outTokens;
|
||||||
|
|
||||||
void add(final Duration duration, final int inTokens, final int outTokens) {
|
void add(final Duration duration, final int inTokens, final int outTokens) {
|
||||||
LOGGER.info("Request duration: {}; in: {}; out: {}; tps: {}", duration, inTokens, outTokens, String.format("%.2f", (inTokens + outTokens) / (double) duration.toMillis() * 1000.0));
|
LOGGER.debug("Request duration: {}; in: {}; out: {}; tps: {}", duration, inTokens, outTokens, String.format("%.2f", (inTokens + outTokens) / (double) duration.toMillis() * 1000.0));
|
||||||
|
|
||||||
this.requests++;
|
this.requests++;
|
||||||
this.duration = this.duration.plus(duration);
|
this.duration = this.duration.plus(duration);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package de.extio.lmlib.client.textcompletion;
|
package de.extio.lmlib.client.textcompletion;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.text.DecimalFormat;
|
import java.text.DecimalFormat;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
@@ -122,6 +123,7 @@ public class TextCompletionClient implements Client, DisposableBean {
|
|||||||
statistics.duration,
|
statistics.duration,
|
||||||
statistics.inTokens,
|
statistics.inTokens,
|
||||||
statistics.outTokens,
|
statistics.outTokens,
|
||||||
|
new BigDecimal(statistics.inTokens).multiply(modelProfile.costInToken()).add(new BigDecimal(statistics.outTokens).multiply(modelProfile.costOutToken())),
|
||||||
false);
|
false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,9 +209,10 @@ public class TextCompletionClient implements Client, DisposableBean {
|
|||||||
final var dur = Duration.between(start, LocalDateTime.now());
|
final var dur = Duration.between(start, LocalDateTime.now());
|
||||||
final var in = this.tokenizer.count(promptStr, modelProfile);
|
final var in = this.tokenizer.count(promptStr, modelProfile);
|
||||||
final var out = this.tokenizer.count(content, modelProfile);
|
final var out = this.tokenizer.count(content, modelProfile);
|
||||||
LOGGER.debug("Request duration: " + dur + "; in tokens: " + in + "; out tokens: " + out);
|
final var cost = new BigDecimal(in).multiply(modelProfile.costInToken()).add(new BigDecimal(out).multiply(modelProfile.costOutToken()));
|
||||||
statistics.add(dur, in, out);
|
LOGGER.debug("Request duration: " + dur + "; in tokens: " + in + "; out tokens: " + out + "; cost: " + new DecimalFormat("0.####").format(cost));
|
||||||
this.totalStatistics.add(dur, in, out);
|
statistics.add(dur, in, out, cost);
|
||||||
|
this.totalStatistics.add(dur, in, out, cost);
|
||||||
}
|
}
|
||||||
|
|
||||||
answer.append(content);
|
answer.append(content);
|
||||||
@@ -247,7 +250,7 @@ public class TextCompletionClient implements Client, DisposableBean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void summarize(final List<StringBuilder> answers, final ModelProfile modelProfile, final PromptStrategy promptStrategy, final CompletionStatistics statistics) {
|
private void summarize(final List<StringBuilder> answers, final ModelProfile modelProfile, final PromptStrategy promptStrategy, final CompletionStatistics statistics) {
|
||||||
// TODO: Reactivate when multiple (split) prompts have been implemented again
|
// TODO: Reactivate when multiple (split) prompts have been implemented again in #createPrompts
|
||||||
// LOGGER.debug("Summarizing answers");
|
// LOGGER.debug("Summarizing answers");
|
||||||
//
|
//
|
||||||
// final StringBuilder summary = new StringBuilder();
|
// final StringBuilder summary = new StringBuilder();
|
||||||
@@ -264,7 +267,7 @@ public class TextCompletionClient implements Client, DisposableBean {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void destroy() throws Exception {
|
public void destroy() throws Exception {
|
||||||
if (this.collectStatistics) {
|
if (this.collectStatistics && this.totalStatistics.requests > 0) {
|
||||||
LOGGER.info("Client Statistics: {}", this.totalStatistics);
|
LOGGER.info("Client Statistics: {}", this.totalStatistics);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -273,19 +276,22 @@ public class TextCompletionClient implements Client, DisposableBean {
|
|||||||
|
|
||||||
private final Instant start = Instant.now();
|
private final Instant start = Instant.now();
|
||||||
|
|
||||||
volatile int requests;
|
private int requests;
|
||||||
|
|
||||||
volatile Duration duration = Duration.ofMillis(0l);
|
private Duration duration = Duration.ofMillis(0l);
|
||||||
|
|
||||||
volatile long inTokens;
|
private long inTokens;
|
||||||
|
|
||||||
volatile long outTokens;
|
private long outTokens;
|
||||||
|
|
||||||
synchronized void add(final Duration duration, final int inTokens, final int outTokens) {
|
private BigDecimal cost = BigDecimal.ZERO;
|
||||||
|
|
||||||
|
synchronized void add(final Duration duration, final int inTokens, final int outTokens, final BigDecimal cost) {
|
||||||
this.requests++;
|
this.requests++;
|
||||||
this.duration = this.duration.plus(duration);
|
this.duration = this.duration.plus(duration);
|
||||||
this.inTokens += inTokens;
|
this.inTokens += inTokens;
|
||||||
this.outTokens += outTokens;
|
this.outTokens += outTokens;
|
||||||
|
this.cost = this.cost.add(cost);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -300,16 +306,18 @@ public class TextCompletionClient implements Client, DisposableBean {
|
|||||||
builder.append(", outTokens=");
|
builder.append(", outTokens=");
|
||||||
builder.append(this.outTokens);
|
builder.append(this.outTokens);
|
||||||
builder.append(", averageTps=");
|
builder.append(", averageTps=");
|
||||||
builder.append(new DecimalFormat("#.##").format(((double) (this.outTokens + this.inTokens) / (double) this.requests) / ((double) this.duration.toMillis() / 1000.0 / (double) this.requests)));
|
builder.append(new DecimalFormat("0.##").format(((double) (this.outTokens + this.inTokens) / (double) this.requests) / ((double) this.duration.toMillis() / 1000.0 / (double) this.requests)));
|
||||||
builder.append(", averageOutTps=");
|
builder.append(", averageOutTps=");
|
||||||
builder.append(new DecimalFormat("#.##").format((double) this.outTokens / ((double) this.duration.toMillis() / 1000.0)));
|
builder.append(new DecimalFormat("0.##").format((double) this.outTokens / ((double) this.duration.toMillis() / 1000.0)));
|
||||||
builder.append(", effectiveDuration=");
|
builder.append(", effectiveDuration=");
|
||||||
final var effectiveDuration = Duration.between(this.start, Instant.now());
|
final var effectiveDuration = Duration.between(this.start, Instant.now());
|
||||||
builder.append(effectiveDuration);
|
builder.append(effectiveDuration);
|
||||||
builder.append(", effectiveTps=");
|
builder.append(", effectiveTps=");
|
||||||
builder.append(new DecimalFormat("#.##").format(((double) (this.outTokens + this.inTokens) / (double) this.requests) / ((double) effectiveDuration.toMillis() / 1000.0 / (double) this.requests)));
|
builder.append(new DecimalFormat("0.##").format(((double) (this.outTokens + this.inTokens) / (double) this.requests) / ((double) effectiveDuration.toMillis() / 1000.0 / (double) this.requests)));
|
||||||
builder.append(", effectiveOutTps=");
|
builder.append(", effectiveOutTps=");
|
||||||
builder.append(new DecimalFormat("#.##").format((double) this.outTokens / ((double) effectiveDuration.toMillis() / 1000.0)));
|
builder.append(new DecimalFormat("0.##").format((double) this.outTokens / ((double) effectiveDuration.toMillis() / 1000.0)));
|
||||||
|
builder.append(", cost=");
|
||||||
|
builder.append(new DecimalFormat("0.###").format(cost.doubleValue()));
|
||||||
builder.append("]");
|
builder.append("]");
|
||||||
return builder.toString();
|
return builder.toString();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package de.extio.lmlib.profile;
|
package de.extio.lmlib.profile;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
public record ModelProfile(
|
public record ModelProfile(
|
||||||
String prompt,
|
String prompt,
|
||||||
String tokenEncoding,
|
String tokenEncoding,
|
||||||
@@ -11,7 +13,9 @@ public record ModelProfile(
|
|||||||
ModelProvider modelProvider,
|
ModelProvider modelProvider,
|
||||||
String modelName,
|
String modelName,
|
||||||
String url,
|
String url,
|
||||||
String apiKey) {
|
String apiKey,
|
||||||
|
BigDecimal costInToken,
|
||||||
|
BigDecimal costOutToken) {
|
||||||
|
|
||||||
public static enum ModelProvider {
|
public static enum ModelProvider {
|
||||||
AZURE_AI, OAI_TEXT_COMPLETION;
|
AZURE_AI, OAI_TEXT_COMPLETION;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package de.extio.lmlib.profile;
|
package de.extio.lmlib.profile;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@@ -12,6 +13,7 @@ import org.slf4j.LoggerFactory;
|
|||||||
import org.springframework.boot.context.properties.bind.Bindable;
|
import org.springframework.boot.context.properties.bind.Bindable;
|
||||||
import org.springframework.boot.context.properties.bind.Binder;
|
import org.springframework.boot.context.properties.bind.Binder;
|
||||||
import org.springframework.cache.annotation.Cacheable;
|
import org.springframework.cache.annotation.Cacheable;
|
||||||
|
import org.springframework.core.env.ConfigurableEnvironment;
|
||||||
import org.springframework.core.env.Environment;
|
import org.springframework.core.env.Environment;
|
||||||
import org.springframework.core.io.support.ResourcePropertySource;
|
import org.springframework.core.io.support.ResourcePropertySource;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -38,7 +40,9 @@ public class ModelProfileService {
|
|||||||
throw new IllegalStateException("No model found for profile: " + modelProfile);
|
throw new IllegalStateException("No model found for profile: " + modelProfile);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
|
||||||
final var resource = new ResourcePropertySource("classpath:" + modelName + ".properties");
|
final var resource = new ResourcePropertySource("classpath:" + modelName + ".properties");
|
||||||
|
|
||||||
final var promptTemplate = Optional.ofNullable(resource.getProperty("prompts"))
|
final var promptTemplate = Optional.ofNullable(resource.getProperty("prompts"))
|
||||||
.map(Object::toString)
|
.map(Object::toString)
|
||||||
.orElseThrow(() -> new IllegalStateException("No model name found for model: " + modelName));
|
.orElseThrow(() -> new IllegalStateException("No model name found for model: " + modelName));
|
||||||
@@ -75,11 +79,29 @@ public class ModelProfileService {
|
|||||||
final var url = Optional.ofNullable(resource.getProperty("url"))
|
final var url = Optional.ofNullable(resource.getProperty("url"))
|
||||||
.map(Object::toString)
|
.map(Object::toString)
|
||||||
.orElseThrow(() -> new IllegalStateException("No model url found for model: " + modelName));
|
.orElseThrow(() -> new IllegalStateException("No model url found for model: " + modelName));
|
||||||
final var apiKey = Optional.ofNullable(resource.getProperty("apiKey"))
|
final var costInToken = Optional.ofNullable(resource.getProperty("cost1MInTokens"))
|
||||||
|
.map(Object::toString)
|
||||||
|
.map(value -> new BigDecimal(value))
|
||||||
|
.orElse(BigDecimal.ZERO)
|
||||||
|
.divide(new BigDecimal(1000000.0));
|
||||||
|
final var costOutToken = Optional.ofNullable(resource.getProperty("cost1MOutTokens"))
|
||||||
|
.map(Object::toString)
|
||||||
|
.map(value -> new BigDecimal(value))
|
||||||
|
.orElse(BigDecimal.ZERO)
|
||||||
|
.divide(new BigDecimal(1000000.0));
|
||||||
|
|
||||||
|
//To resolve api key placeholder ${exchangeautodocumentation.azure.apikey} in model-name.properties file
|
||||||
|
String apiKey = null;
|
||||||
|
synchronized (this.environment) {
|
||||||
|
if (this.environment instanceof ConfigurableEnvironment configurableEnvironment) {
|
||||||
|
configurableEnvironment.getPropertySources().addLast(resource);
|
||||||
|
}
|
||||||
|
apiKey = Optional.ofNullable(this.environment.getProperty("apiKey"))
|
||||||
.map(Object::toString)
|
.map(Object::toString)
|
||||||
.orElseThrow(() -> new IllegalStateException("No model apiKey found for model: " + modelName));
|
.orElseThrow(() -> new IllegalStateException("No model apiKey found for model: " + modelName));
|
||||||
|
}
|
||||||
|
|
||||||
return new ModelProfile(promptTemplate, tokenEncoding, maxTokens, maxContextLength, temperature, topP, maxContinuations, modelProvider, modelNameCfg, url, apiKey);
|
return new ModelProfile(promptTemplate, tokenEncoding, maxTokens, maxContextLength, temperature, topP, maxContinuations, modelProvider, modelNameCfg, url, apiKey, costInToken, costOutToken);
|
||||||
}
|
}
|
||||||
catch (final IOException e) {
|
catch (final IOException e) {
|
||||||
LOGGER.error("Error while reading model profile", e);
|
LOGGER.error("Error while reading model profile", e);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ public class TokenizerAutoConfiguration {
|
|||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnProperty(name = "tokenizer.strategy", havingValue = "llamaServer")
|
@ConditionalOnProperty(name = "tokenizer.strategy", havingValue = "llamaServer")
|
||||||
@ConditionalOnMissingBean
|
@ConditionalOnMissingBean(type = "de.extio.lmlib.token.Tokenizer")
|
||||||
Tokenizer llamaServerTokenizer(@Qualifier("lmLibWebClientBuilder") final WebClient.Builder webClientBuilder) {
|
Tokenizer llamaServerTokenizer(@Qualifier("lmLibWebClientBuilder") final WebClient.Builder webClientBuilder) {
|
||||||
return new LlamaServerTokenizer(webClientBuilder);
|
return new LlamaServerTokenizer(webClientBuilder);
|
||||||
}
|
}
|
||||||
@@ -22,7 +22,7 @@ public class TokenizerAutoConfiguration {
|
|||||||
@Configuration(proxyBeanMethods = false)
|
@Configuration(proxyBeanMethods = false)
|
||||||
@ConditionalOnClass(name = "com.knuddels.jtokkit.Encodings")
|
@ConditionalOnClass(name = "com.knuddels.jtokkit.Encodings")
|
||||||
@ConditionalOnProperty(name = "tokenizer.strategy", havingValue = "jTokkit")
|
@ConditionalOnProperty(name = "tokenizer.strategy", havingValue = "jTokkit")
|
||||||
@ConditionalOnMissingBean
|
@ConditionalOnMissingBean(type = "de.extio.lmlib.token.Tokenizer")
|
||||||
public static class SomeServiceConfiguration {
|
public static class SomeServiceConfiguration {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
|||||||
@@ -9,3 +9,5 @@ modelProvider=OAI_TEXT_COMPLETION
|
|||||||
modelName=
|
modelName=
|
||||||
url=http://localhost:5001
|
url=http://localhost:5001
|
||||||
apiKey=
|
apiKey=
|
||||||
|
cost1MInTokens=0
|
||||||
|
cost1MOutTokens=0
|
||||||
|
|||||||
13
src/test/resources/llama3.3-70b-azure.properties
Normal file
13
src/test/resources/llama3.3-70b-azure.properties
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
prompts=
|
||||||
|
tokenEncoding=cl100k_base
|
||||||
|
maxTokens=1500
|
||||||
|
maxContextLength=32767
|
||||||
|
temperature=0.2
|
||||||
|
topP=1.0
|
||||||
|
maxContinuations=1
|
||||||
|
modelProvider=AZURE_AI
|
||||||
|
modelName=Llama-3.3-70B-Instruct
|
||||||
|
url=XXX
|
||||||
|
apiKey=XXX
|
||||||
|
cost1MInTokens=0.71
|
||||||
|
cost1MOutTokens=0.71
|
||||||
Reference in New Issue
Block a user