feat: Enhance agent functionality with ModelProfile integration

- Added ModelProfile to the Agent record to allow for more detailed model configurations.
- Updated AgentExecutorService to utilize ModelProfile when fetching clients.
- Modified BaseAgent interface to include a default modelProfile method.
- Adjusted Client interface and implementations to support ModelProfile in conversation and streaming methods.
- Enhanced ClientService to retrieve clients based on ModelProfile.
- Updated CompletionInterceptor to work with ModelProfile instead of ModelCategory.
- Refactored AzureAiClient and other client implementations to handle ModelProfile correctly.
- Changed CachedClient to cache responses based on ModelProfile.
- Updated ModelCategory to a record type for better validation and immutability.
- Enhanced ModelProfile with validation checks for its fields.
- Updated ModelProfileService to support fetching profiles based on ModelCategory.
- Adjusted tests to accommodate changes in Agent and ModelProfile.
- Updated properties files to include category information for model profiles.
This commit is contained in:
extio-de
2025-10-29 12:07:36 +01:00
parent c5dca3ac07
commit 304924e223
20 changed files with 452 additions and 126 deletions

View File

@@ -1,23 +1,23 @@
package de.extio.lmlib.profile;
public enum ModelCategory {
public record ModelCategory(String modelProfile, String shortName) {
public static final ModelCategory SMALL = new ModelCategory("profile.model.s", "S");
public static final ModelCategory MEDIUM = new ModelCategory("profile.model.m", "M");
public static final ModelCategory LARGE = new ModelCategory("profile.model.l", "L");
public static final ModelCategory XLARGE = new ModelCategory("profile.model.xl", "XL");
public static final ModelCategory HOT = new ModelCategory("profile.model.hot", "HOT");
public static final ModelCategory COLD = new ModelCategory("profile.model.cold", "COLD");
SMALL("profile.model.s", "S"),
MEDIUM("profile.model.m", "M"),
LARGE("profile.model.l", "L"),
XLARGE("profile.model.xl", "XL"),
HOT("profile.model.hot", "HOT"),
COLD("profile.model.cold", "COLD");
private final String modelProfile;
private final String shortName;
ModelCategory(final String modelProfile, final String shortName) {
this.modelProfile = modelProfile;
this.shortName = shortName;
public ModelCategory {
if (modelProfile == null || modelProfile.isEmpty()) {
throw new IllegalArgumentException("modelProfile must not be null or empty");
}
if (shortName == null || shortName.isEmpty()) {
throw new IllegalArgumentException("shortName must not be null or empty");
}
}
public String getModelProfile() {
return this.modelProfile;
}

View File

@@ -16,7 +16,32 @@ public record ModelProfile(
BigDecimal costInToken,
BigDecimal costOutToken,
String reasoningEffort,
String reasoningSummaryDetails) {
String reasoningSummaryDetails,
String category) {
public ModelProfile {
if (maxTokens <= 0) {
throw new IllegalArgumentException("Model profile maxTokens must be greater than zero");
}
if (maxContextLength <= 0) {
throw new IllegalArgumentException("Model profile maxContextLength must be greater than zero");
}
if (temperature <= 0) {
throw new IllegalArgumentException("Model profile temperature must be greater than zero");
}
if (topP <= 0) {
throw new IllegalArgumentException("Model profile topP must be greater than zero");
}
if (modelProvider == null) {
throw new IllegalArgumentException("Model profile modelProvider is mandatory and cannot be null");
}
if (url == null || url.isBlank()) {
throw new IllegalArgumentException("Model profile url is mandatory and cannot be empty");
}
if (category == null || category.isBlank()) {
throw new IllegalArgumentException("Model profile category is mandatory and cannot be empty");
}
}
public static enum ModelProvider {
AZURE_AI, OAI_TEXT_COMPLETION, OAI_CHAT_COMPLETION;

View File

@@ -33,8 +33,12 @@ public class ModelProfileService {
this.environment = environment;
}
@Cacheable("modelProfiles")
public ModelProfile getModelProfile(final String modelProfile) {
return this.getModelProfile(modelProfile, null);
}
@Cacheable("modelProfiles")
public ModelProfile getModelProfile(final String modelProfile, final ModelCategory modelCategory) {
final var modelName = this.environment.getProperty(modelProfile);
if (modelName == null) {
throw new IllegalStateException("No model found for profile: " + modelProfile);
@@ -45,7 +49,7 @@ public class ModelProfileService {
final var promptTemplate = Optional.ofNullable(resource.getProperty("prompts"))
.map(Object::toString)
.orElseThrow(() -> new IllegalStateException("No model name found for model: " + modelName));
.orElse("");
final var tokenEncoding = Optional.ofNullable(resource.getProperty("tokenEncoding"))
.map(Object::toString)
.orElse("none");
@@ -71,7 +75,7 @@ public class ModelProfileService {
.orElseThrow(() -> new IllegalStateException("No model modelProvider found for model: " + modelName));
final var modelNameCfg = Optional.ofNullable(resource.getProperty("modelName"))
.map(Object::toString)
.orElseThrow(() -> new IllegalStateException("No model modelName found for model: " + modelName));
.orElse(null);
final var url = Optional.ofNullable(resource.getProperty("url"))
.map(Object::toString)
.orElseThrow(() -> new IllegalStateException("No model url found for model: " + modelName));
@@ -91,6 +95,10 @@ public class ModelProfileService {
final var reasoningSummaryDetails = Optional.ofNullable(resource.getProperty("reasoningSummaryDetails"))
.map(Object::toString)
.orElse(null);
final var category = Optional
.ofNullable(modelCategory)
.map(ModelCategory::getShortName)
.orElseGet(() -> (String)resource.getProperty("category"));
//To resolve api key placeholder ${apikey} in model-name.properties file
String apiKey = null;
@@ -100,10 +108,10 @@ public class ModelProfileService {
}
apiKey = Optional.ofNullable(this.environment.getProperty("apiKey"))
.map(Object::toString)
.orElseThrow(() -> new IllegalStateException("No model apiKey found for model: " + modelName));
.orElse(null);
}
return new ModelProfile(promptTemplate, tokenEncoding, maxTokens, maxContextLength, temperature, topP, modelProvider, modelNameCfg, url, apiKey, costInToken, costOutToken, reasoningEffort, reasoningSummaryDetails);
return new ModelProfile(promptTemplate, tokenEncoding, maxTokens, maxContextLength, temperature, topP, modelProvider, modelNameCfg, url, apiKey, costInToken, costOutToken, reasoningEffort, reasoningSummaryDetails, category);
}
catch (final IOException e) {
LOGGER.error("Error while reading model profile", e);
@@ -111,6 +119,10 @@ public class ModelProfileService {
}
}
public ModelProfile getModelProfile(final ModelCategory modelCategory) {
return this.getModelProfile(modelCategory.getModelProfile(), modelCategory);
}
public List<String> getModelProfileUrls(final ModelProvider... modelProviders) {
final var modelProfiles = Binder.get(this.environment)
.bind(MODEL_PROFILE_PREFIX, Bindable.mapOf(String.class, String.class))