Convert spaces to tabs
This commit is contained in:
@@ -9,89 +9,89 @@ import de.extio.lmlib.profile.ModelCategory;
|
||||
import de.extio.lmlib.profile.ModelProfile;
|
||||
|
||||
public record Agent(String name,
|
||||
AgentType agentType,
|
||||
ModelCategory modelCategory,
|
||||
ModelProfile modelProfile,
|
||||
String systemPrompt,
|
||||
String textTemplate,
|
||||
AgentResponseHandler responseHandler,
|
||||
Consumer<AgentContext> preProcessor,
|
||||
Consumer<AgentContext> postProcessor,
|
||||
Function<List<AgentContext>,
|
||||
List<AgentContext>> merger,
|
||||
Function<AgentContext, AgentNext> chooseNext) implements BaseAgent {
|
||||
|
||||
public Agent {
|
||||
if (name == null) {
|
||||
throw new IllegalArgumentException("Name cannot be null");
|
||||
}
|
||||
if (agentType == null) {
|
||||
throw new IllegalArgumentException("Agent type cannot be null");
|
||||
}
|
||||
}
|
||||
AgentType agentType,
|
||||
ModelCategory modelCategory,
|
||||
ModelProfile modelProfile,
|
||||
String systemPrompt,
|
||||
String textTemplate,
|
||||
AgentResponseHandler responseHandler,
|
||||
Consumer<AgentContext> preProcessor,
|
||||
Consumer<AgentContext> postProcessor,
|
||||
Function<List<AgentContext>,
|
||||
List<AgentContext>> merger,
|
||||
Function<AgentContext, AgentNext> chooseNext) implements BaseAgent {
|
||||
|
||||
public Agent {
|
||||
if (name == null) {
|
||||
throw new IllegalArgumentException("Name cannot be null");
|
||||
}
|
||||
if (agentType == null) {
|
||||
throw new IllegalArgumentException("Agent type cannot be null");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public AgentType agentType(final AgentContext context) {
|
||||
if (agentType != null) {
|
||||
return agentType;
|
||||
}
|
||||
return BaseAgent.super.agentType(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModelCategory modelCategory(final AgentContext context) {
|
||||
if (modelCategory != null) {
|
||||
return modelCategory;
|
||||
}
|
||||
return BaseAgent.super.modelCategory(context);
|
||||
}
|
||||
@Override
|
||||
public AgentType agentType(final AgentContext context) {
|
||||
if (agentType != null) {
|
||||
return agentType;
|
||||
}
|
||||
return BaseAgent.super.agentType(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModelCategory modelCategory(final AgentContext context) {
|
||||
if (modelCategory != null) {
|
||||
return modelCategory;
|
||||
}
|
||||
return BaseAgent.super.modelCategory(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModelProfile modelProfile(final AgentContext context) {
|
||||
if (modelProfile != null) {
|
||||
return modelProfile;
|
||||
}
|
||||
return BaseAgent.super.modelProfile(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AgentResponseHandler responseHandler(final AgentContext context) {
|
||||
if (responseHandler != null) {
|
||||
return responseHandler;
|
||||
}
|
||||
return BaseAgent.super.responseHandler(context);
|
||||
}
|
||||
@Override
|
||||
public ModelProfile modelProfile(final AgentContext context) {
|
||||
if (modelProfile != null) {
|
||||
return modelProfile;
|
||||
}
|
||||
return BaseAgent.super.modelProfile(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AgentResponseHandler responseHandler(final AgentContext context) {
|
||||
if (responseHandler != null) {
|
||||
return responseHandler;
|
||||
}
|
||||
return BaseAgent.super.responseHandler(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preProcess(final AgentContext context) {
|
||||
if (preProcessor != null) {
|
||||
preProcessor.accept(context);
|
||||
}
|
||||
BaseAgent.super.preProcess(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postProcess(final AgentContext context) {
|
||||
if (postProcessor != null) {
|
||||
postProcessor.accept(context);
|
||||
}
|
||||
BaseAgent.super.postProcess(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AgentContext> merge(final List<AgentContext> contexts) {
|
||||
if (merger != null) {
|
||||
return merger.apply(contexts);
|
||||
}
|
||||
return BaseAgent.super.merge(contexts);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AgentNext chooseNextAgent(final AgentContext context) {
|
||||
if (chooseNext != null) {
|
||||
return chooseNext.apply(context);
|
||||
}
|
||||
return BaseAgent.super.chooseNextAgent(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preProcess(final AgentContext context) {
|
||||
if (preProcessor != null) {
|
||||
preProcessor.accept(context);
|
||||
}
|
||||
BaseAgent.super.preProcess(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postProcess(final AgentContext context) {
|
||||
if (postProcessor != null) {
|
||||
postProcessor.accept(context);
|
||||
}
|
||||
BaseAgent.super.postProcess(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AgentContext> merge(final List<AgentContext> contexts) {
|
||||
if (merger != null) {
|
||||
return merger.apply(contexts);
|
||||
}
|
||||
return BaseAgent.super.merge(contexts);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AgentNext chooseNextAgent(final AgentContext context) {
|
||||
if (chooseNext != null) {
|
||||
return chooseNext.apply(context);
|
||||
}
|
||||
return BaseAgent.super.chooseNextAgent(context);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ package de.extio.lmlib.agent;
|
||||
|
||||
public final record AgentNext(String name, Boolean gradingPassed) {
|
||||
|
||||
public static final AgentNext END = new AgentNext(null, null);
|
||||
public static final AgentNext END = new AgentNext(null, null);
|
||||
|
||||
public static AgentNext end(final AgentContext context) {
|
||||
return END;
|
||||
}
|
||||
public static AgentNext end(final AgentContext context) {
|
||||
return END;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,165 +12,165 @@ import de.extio.lmlib.client.Completion;
|
||||
import de.extio.lmlib.client.Conversation;
|
||||
|
||||
public class CsvAgentResponseHandler implements AgentResponseHandler {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(CsvAgentResponseHandler.class);
|
||||
|
||||
private final List<String> headings;
|
||||
|
||||
private final String keyPrefix;
|
||||
|
||||
private final String fieldSeparator;
|
||||
|
||||
private final String fieldSeparatorQuoted;
|
||||
|
||||
private final String errorPromptSuffix;
|
||||
|
||||
public CsvAgentResponseHandler(final List<String> headings, final String keyPrefix) {
|
||||
this(headings, keyPrefix, ";");
|
||||
}
|
||||
|
||||
public CsvAgentResponseHandler(final List<String> headings, final String keyPrefix, final String fieldSeparator) {
|
||||
if (headings == null || headings.isEmpty()) {
|
||||
throw new IllegalArgumentException("Headings list cannot be null or empty");
|
||||
}
|
||||
this.headings = List.copyOf(headings);
|
||||
this.keyPrefix = keyPrefix != null ? keyPrefix : "";
|
||||
this.fieldSeparator = fieldSeparator;
|
||||
this.fieldSeparatorQuoted = Pattern.quote(fieldSeparator);
|
||||
|
||||
this.errorPromptSuffix = "\n\nThe previous response could not be fully processed or validated. " +
|
||||
"Please format the response as CSV with '" + fieldSeparator + "' as field separator. " +
|
||||
"Expected columns: " + String.join(", ", headings) + ". One row per line.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(final Completion completion, final AgentContext context) {
|
||||
final String resp = completion.response();
|
||||
if (resp == null || resp.isBlank()) {
|
||||
LOGGER.warn("Empty completion response for CSV parsing");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
final boolean success = this.parseCsv(resp, context);
|
||||
if (!success) {
|
||||
LOGGER.warn("No valid CSV data parsed from response");
|
||||
this.addCsvResponseErrorPrompt(context);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (final Exception ex) {
|
||||
LOGGER.warn("Failed to parse CSV response", ex);
|
||||
this.addCsvResponseErrorPrompt(context);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean parseCsv(final String csvText, final AgentContext context) {
|
||||
final String[] lines = csvText.split("\n");
|
||||
|
||||
int startLine = 0;
|
||||
|
||||
for (int i = 0; i < lines.length; i++) {
|
||||
final String line = lines[i].trim();
|
||||
|
||||
if (line.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!line.contains(this.fieldSeparator)) {
|
||||
startLine = i + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
final String lowerLine = line.toLowerCase();
|
||||
boolean isHeader = false;
|
||||
for (final String heading : this.headings) {
|
||||
if (lowerLine.contains(heading.toLowerCase())) {
|
||||
isHeader = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isHeader) {
|
||||
startLine = i + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
final List<List<String>> columnData = new ArrayList<>();
|
||||
for (int i = 0; i < this.headings.size(); i++) {
|
||||
columnData.add(new ArrayList<>());
|
||||
}
|
||||
|
||||
int rowCount = 0;
|
||||
for (int i = startLine; i < lines.length; i++) {
|
||||
final String line = lines[i].trim();
|
||||
|
||||
if (line.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!line.contains(this.fieldSeparator)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
final String[] parts = line.split(this.fieldSeparatorQuoted, -1);
|
||||
if (parts.length != this.headings.size()) {
|
||||
LOGGER.debug("Column count mismatch: expected {}, got {} for line '{}'",
|
||||
this.headings.size(), parts.length, line);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int col = 0; col < parts.length; col++) {
|
||||
final String value = this.stripQuotes(parts[col].trim());
|
||||
columnData.get(col).add(value);
|
||||
}
|
||||
rowCount++;
|
||||
}
|
||||
catch (final Exception ex) {
|
||||
LOGGER.debug("Failed to parse line '{}': {}", line, ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if (rowCount == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < this.headings.size(); i++) {
|
||||
final String key = this.keyPrefix + this.headings.get(i);
|
||||
context.setStringValues(key, columnData.get(i));
|
||||
LOGGER.debug("Stored {} values in context key '{}'", columnData.get(i).size(), key);
|
||||
}
|
||||
|
||||
LOGGER.debug("Parsed {} CSV rows into {} columns", rowCount, this.headings.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
private void addCsvResponseErrorPrompt(final AgentContext context) {
|
||||
final var turn = context.getConversation().getConversation().getLast();
|
||||
context.getConversation().replaceTurn(new Conversation.Turn(
|
||||
turn.type(),
|
||||
turn.text() + this.errorPromptSuffix));
|
||||
}
|
||||
|
||||
private String stripQuotes(final String value) {
|
||||
if (value == null || value.length() < 2) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if ((value.startsWith("\"")
|
||||
&& value.endsWith("\""))
|
||||
|| (value.startsWith("'")
|
||||
&& value.endsWith("'"))) {
|
||||
return value.substring(1, value.length() - 1);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(CsvAgentResponseHandler.class);
|
||||
|
||||
private final List<String> headings;
|
||||
|
||||
private final String keyPrefix;
|
||||
|
||||
private final String fieldSeparator;
|
||||
|
||||
private final String fieldSeparatorQuoted;
|
||||
|
||||
private final String errorPromptSuffix;
|
||||
|
||||
public CsvAgentResponseHandler(final List<String> headings, final String keyPrefix) {
|
||||
this(headings, keyPrefix, ";");
|
||||
}
|
||||
|
||||
public CsvAgentResponseHandler(final List<String> headings, final String keyPrefix, final String fieldSeparator) {
|
||||
if (headings == null || headings.isEmpty()) {
|
||||
throw new IllegalArgumentException("Headings list cannot be null or empty");
|
||||
}
|
||||
this.headings = List.copyOf(headings);
|
||||
this.keyPrefix = keyPrefix != null ? keyPrefix : "";
|
||||
this.fieldSeparator = fieldSeparator;
|
||||
this.fieldSeparatorQuoted = Pattern.quote(fieldSeparator);
|
||||
|
||||
this.errorPromptSuffix = "\n\nThe previous response could not be fully processed or validated. " +
|
||||
"Please format the response as CSV with '" + fieldSeparator + "' as field separator. " +
|
||||
"Expected columns: " + String.join(", ", headings) + ". One row per line.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(final Completion completion, final AgentContext context) {
|
||||
final String resp = completion.response();
|
||||
if (resp == null || resp.isBlank()) {
|
||||
LOGGER.warn("Empty completion response for CSV parsing");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
final boolean success = this.parseCsv(resp, context);
|
||||
if (!success) {
|
||||
LOGGER.warn("No valid CSV data parsed from response");
|
||||
this.addCsvResponseErrorPrompt(context);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (final Exception ex) {
|
||||
LOGGER.warn("Failed to parse CSV response", ex);
|
||||
this.addCsvResponseErrorPrompt(context);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean parseCsv(final String csvText, final AgentContext context) {
|
||||
final String[] lines = csvText.split("\n");
|
||||
|
||||
int startLine = 0;
|
||||
|
||||
for (int i = 0; i < lines.length; i++) {
|
||||
final String line = lines[i].trim();
|
||||
|
||||
if (line.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!line.contains(this.fieldSeparator)) {
|
||||
startLine = i + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
final String lowerLine = line.toLowerCase();
|
||||
boolean isHeader = false;
|
||||
for (final String heading : this.headings) {
|
||||
if (lowerLine.contains(heading.toLowerCase())) {
|
||||
isHeader = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isHeader) {
|
||||
startLine = i + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
final List<List<String>> columnData = new ArrayList<>();
|
||||
for (int i = 0; i < this.headings.size(); i++) {
|
||||
columnData.add(new ArrayList<>());
|
||||
}
|
||||
|
||||
int rowCount = 0;
|
||||
for (int i = startLine; i < lines.length; i++) {
|
||||
final String line = lines[i].trim();
|
||||
|
||||
if (line.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!line.contains(this.fieldSeparator)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
final String[] parts = line.split(this.fieldSeparatorQuoted, -1);
|
||||
if (parts.length != this.headings.size()) {
|
||||
LOGGER.debug("Column count mismatch: expected {}, got {} for line '{}'",
|
||||
this.headings.size(), parts.length, line);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int col = 0; col < parts.length; col++) {
|
||||
final String value = this.stripQuotes(parts[col].trim());
|
||||
columnData.get(col).add(value);
|
||||
}
|
||||
rowCount++;
|
||||
}
|
||||
catch (final Exception ex) {
|
||||
LOGGER.debug("Failed to parse line '{}': {}", line, ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if (rowCount == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < this.headings.size(); i++) {
|
||||
final String key = this.keyPrefix + this.headings.get(i);
|
||||
context.setStringValues(key, columnData.get(i));
|
||||
LOGGER.debug("Stored {} values in context key '{}'", columnData.get(i).size(), key);
|
||||
}
|
||||
|
||||
LOGGER.debug("Parsed {} CSV rows into {} columns", rowCount, this.headings.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
private void addCsvResponseErrorPrompt(final AgentContext context) {
|
||||
final var turn = context.getConversation().getConversation().getLast();
|
||||
context.getConversation().replaceTurn(new Conversation.Turn(
|
||||
turn.type(),
|
||||
turn.text() + this.errorPromptSuffix));
|
||||
}
|
||||
|
||||
private String stripQuotes(final String value) {
|
||||
if (value == null || value.length() < 2) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if ((value.startsWith("\"")
|
||||
&& value.endsWith("\""))
|
||||
|| (value.startsWith("'")
|
||||
&& value.endsWith("'"))) {
|
||||
return value.substring(1, value.length() - 1);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,260 +17,260 @@ import de.extio.lmlib.client.Completion;
|
||||
import de.extio.lmlib.client.Conversation;
|
||||
|
||||
public class CsvUnmarshallingAgentResponseHandler implements AgentResponseHandler {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(CsvUnmarshallingAgentResponseHandler.class);
|
||||
|
||||
private final Class<?> targetClass;
|
||||
|
||||
private final String contextKey;
|
||||
|
||||
private final String fieldSeparator;
|
||||
|
||||
private final String listSeparator;
|
||||
|
||||
private final Field[] fields;
|
||||
|
||||
private final List<String> fieldNames;
|
||||
|
||||
private final Constructor<?> constructor;
|
||||
|
||||
private final String fieldSeparatorQuoted;
|
||||
|
||||
private final String listSeparatorQuoted;
|
||||
|
||||
private final String errorPromptSuffix;
|
||||
|
||||
public CsvUnmarshallingAgentResponseHandler(final Class<?> targetClass, final String contextKey) {
|
||||
this(targetClass, contextKey, ";", ",");
|
||||
}
|
||||
|
||||
public CsvUnmarshallingAgentResponseHandler(final Class<?> targetClass, final String contextKey,
|
||||
final String fieldSeparator, final String listSeparator) {
|
||||
this.targetClass = targetClass;
|
||||
this.contextKey = contextKey;
|
||||
this.fieldSeparator = fieldSeparator;
|
||||
this.listSeparator = listSeparator;
|
||||
|
||||
// Pre-compute static data
|
||||
this.fields = targetClass.getDeclaredFields();
|
||||
this.fieldNames = Arrays.stream(this.fields)
|
||||
.map(Field::getName)
|
||||
.toList();
|
||||
this.constructor = this.findRecordConstructor();
|
||||
if (this.constructor == null) {
|
||||
throw new IllegalStateException("No suitable constructor found for " + targetClass.getName());
|
||||
}
|
||||
this.fieldSeparatorQuoted = Pattern.quote(fieldSeparator);
|
||||
this.listSeparatorQuoted = (listSeparator != null && !listSeparator.isEmpty())
|
||||
? Pattern.quote(listSeparator)
|
||||
: null;
|
||||
|
||||
// Pre-compute error message suffix
|
||||
final String listInfo = (listSeparator != null && !listSeparator.isEmpty())
|
||||
? " and '" + listSeparator + "' for list items"
|
||||
: "";
|
||||
this.errorPromptSuffix = "\n\nThe previous response could not be fully processed or validated. " +
|
||||
"Please format the response as CSV with '" + fieldSeparator + "' as field separator" +
|
||||
listInfo + ". One object per line.";
|
||||
}
|
||||
|
||||
private Constructor<?> findRecordConstructor() {
|
||||
final Field[] targetFields = this.targetClass.getDeclaredFields();
|
||||
for (final Constructor<?> ctor : this.targetClass.getDeclaredConstructors()) {
|
||||
if (ctor.getParameterCount() == targetFields.length) {
|
||||
return ctor;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(final Completion completion, final AgentContext context) {
|
||||
final String resp = completion.response();
|
||||
if (resp == null || resp.isBlank()) {
|
||||
LOGGER.warn("Empty completion response for CSV parsing");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
final List<Object> parsedObjects = this.parseCsv(resp);
|
||||
if (parsedObjects.isEmpty()) {
|
||||
LOGGER.warn("No valid CSV data parsed from response");
|
||||
this.addCsvResponseErrorPrompt(context);
|
||||
return false;
|
||||
}
|
||||
|
||||
context.setValues(this.contextKey, parsedObjects);
|
||||
LOGGER.debug("Parsed {} CSV objects", parsedObjects.size());
|
||||
return true;
|
||||
}
|
||||
catch (final Exception ex) {
|
||||
LOGGER.warn("Failed to parse CSV response", ex);
|
||||
this.addCsvResponseErrorPrompt(context);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private List<Object> parseCsv(final String csvText) throws Exception {
|
||||
final List<Object> result = new ArrayList<>();
|
||||
final String[] lines = csvText.split("\n");
|
||||
|
||||
int startLine = 0;
|
||||
|
||||
for (int i = 0; i < lines.length; i++) {
|
||||
final String line = lines[i].trim();
|
||||
|
||||
if (line.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!line.contains(this.fieldSeparator)) {
|
||||
startLine = i + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
final String lowerLine = line.toLowerCase();
|
||||
boolean isHeader = false;
|
||||
for (final String fieldName : this.fieldNames) {
|
||||
if (lowerLine.contains(fieldName.toLowerCase())) {
|
||||
isHeader = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isHeader) {
|
||||
startLine = i + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
for (int i = startLine; i < lines.length; i++) {
|
||||
final String line = lines[i].trim();
|
||||
|
||||
if (line.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!line.contains(this.fieldSeparator)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
final Object obj = this.parseLine(line);
|
||||
if (obj != null) {
|
||||
result.add(obj);
|
||||
LOGGER.debug("Parsed CSV object: {}", obj);
|
||||
}
|
||||
}
|
||||
catch (final Exception ex) {
|
||||
LOGGER.debug("Failed to parse line '{}': {}", line, ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private Object parseLine(final String line) throws Exception {
|
||||
final String[] parts = line.split(this.fieldSeparatorQuoted, -1);
|
||||
if (parts.length != this.fields.length) {
|
||||
LOGGER.debug("Field count mismatch: expected {}, got {} for line '{}'",
|
||||
this.fields.length, parts.length, line);
|
||||
return null;
|
||||
}
|
||||
|
||||
final Object[] args = new Object[this.fields.length];
|
||||
|
||||
for (int i = 0; i < this.fields.length; i++) {
|
||||
final String value = parts[i].trim();
|
||||
args[i] = this.parseValue(value, this.fields[i]);
|
||||
}
|
||||
|
||||
return this.constructor.newInstance(args);
|
||||
}
|
||||
|
||||
private Object parseValue(final String value, final Field field) {
|
||||
final String cleanValue = this.stripQuotes(value);
|
||||
final Class<?> fieldType = field.getType();
|
||||
|
||||
if (cleanValue.isEmpty() || cleanValue.equalsIgnoreCase("null")) {
|
||||
if (fieldType == List.class) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (fieldType == String.class) {
|
||||
return cleanValue;
|
||||
}
|
||||
|
||||
if (fieldType == Integer.class || fieldType == int.class) {
|
||||
return Integer.parseInt(cleanValue);
|
||||
}
|
||||
|
||||
if (fieldType == Long.class || fieldType == long.class) {
|
||||
return Long.parseLong(cleanValue);
|
||||
}
|
||||
|
||||
if (fieldType == Double.class || fieldType == double.class) {
|
||||
return Double.parseDouble(cleanValue);
|
||||
}
|
||||
|
||||
if (fieldType == Boolean.class || fieldType == boolean.class) {
|
||||
return Boolean.parseBoolean(cleanValue);
|
||||
}
|
||||
|
||||
if (fieldType == List.class) {
|
||||
if (this.listSeparatorQuoted == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
final Type genericType = field.getGenericType();
|
||||
if (genericType instanceof ParameterizedType) {
|
||||
final ParameterizedType paramType = (ParameterizedType) genericType;
|
||||
final Type[] typeArgs = paramType.getActualTypeArguments();
|
||||
|
||||
if (typeArgs.length > 0 && typeArgs[0] == String.class) {
|
||||
if (cleanValue.isEmpty()) {
|
||||
return new ArrayList<String>();
|
||||
}
|
||||
|
||||
final String[] items = cleanValue.split(this.listSeparatorQuoted);
|
||||
final List<String> list = new ArrayList<>();
|
||||
for (final String item : items) {
|
||||
final String trimmed = item.trim();
|
||||
if (!trimmed.isEmpty()) {
|
||||
list.add(trimmed);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Unsupported field type: " + fieldType.getName());
|
||||
}
|
||||
|
||||
private void addCsvResponseErrorPrompt(final AgentContext context) {
|
||||
final var turn = context.getConversation().getConversation().getLast();
|
||||
context.getConversation().replaceTurn(new Conversation.Turn(
|
||||
turn.type(),
|
||||
turn.text() + this.errorPromptSuffix));
|
||||
}
|
||||
|
||||
private String stripQuotes(final String value) {
|
||||
if (value == null || value.length() < 2) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if ((value.startsWith("\"")
|
||||
&& value.endsWith("\""))
|
||||
|| (value.startsWith("'")
|
||||
&& value.endsWith("'"))) {
|
||||
return value.substring(1, value.length() - 1);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(CsvUnmarshallingAgentResponseHandler.class);
|
||||
|
||||
private final Class<?> targetClass;
|
||||
|
||||
private final String contextKey;
|
||||
|
||||
private final String fieldSeparator;
|
||||
|
||||
private final String listSeparator;
|
||||
|
||||
private final Field[] fields;
|
||||
|
||||
private final List<String> fieldNames;
|
||||
|
||||
private final Constructor<?> constructor;
|
||||
|
||||
private final String fieldSeparatorQuoted;
|
||||
|
||||
private final String listSeparatorQuoted;
|
||||
|
||||
private final String errorPromptSuffix;
|
||||
|
||||
public CsvUnmarshallingAgentResponseHandler(final Class<?> targetClass, final String contextKey) {
|
||||
this(targetClass, contextKey, ";", ",");
|
||||
}
|
||||
|
||||
public CsvUnmarshallingAgentResponseHandler(final Class<?> targetClass, final String contextKey,
|
||||
final String fieldSeparator, final String listSeparator) {
|
||||
this.targetClass = targetClass;
|
||||
this.contextKey = contextKey;
|
||||
this.fieldSeparator = fieldSeparator;
|
||||
this.listSeparator = listSeparator;
|
||||
|
||||
// Pre-compute static data
|
||||
this.fields = targetClass.getDeclaredFields();
|
||||
this.fieldNames = Arrays.stream(this.fields)
|
||||
.map(Field::getName)
|
||||
.toList();
|
||||
this.constructor = this.findRecordConstructor();
|
||||
if (this.constructor == null) {
|
||||
throw new IllegalStateException("No suitable constructor found for " + targetClass.getName());
|
||||
}
|
||||
this.fieldSeparatorQuoted = Pattern.quote(fieldSeparator);
|
||||
this.listSeparatorQuoted = (listSeparator != null && !listSeparator.isEmpty())
|
||||
? Pattern.quote(listSeparator)
|
||||
: null;
|
||||
|
||||
// Pre-compute error message suffix
|
||||
final String listInfo = (listSeparator != null && !listSeparator.isEmpty())
|
||||
? " and '" + listSeparator + "' for list items"
|
||||
: "";
|
||||
this.errorPromptSuffix = "\n\nThe previous response could not be fully processed or validated. " +
|
||||
"Please format the response as CSV with '" + fieldSeparator + "' as field separator" +
|
||||
listInfo + ". One object per line.";
|
||||
}
|
||||
|
||||
private Constructor<?> findRecordConstructor() {
|
||||
final Field[] targetFields = this.targetClass.getDeclaredFields();
|
||||
for (final Constructor<?> ctor : this.targetClass.getDeclaredConstructors()) {
|
||||
if (ctor.getParameterCount() == targetFields.length) {
|
||||
return ctor;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(final Completion completion, final AgentContext context) {
|
||||
final String resp = completion.response();
|
||||
if (resp == null || resp.isBlank()) {
|
||||
LOGGER.warn("Empty completion response for CSV parsing");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
final List<Object> parsedObjects = this.parseCsv(resp);
|
||||
if (parsedObjects.isEmpty()) {
|
||||
LOGGER.warn("No valid CSV data parsed from response");
|
||||
this.addCsvResponseErrorPrompt(context);
|
||||
return false;
|
||||
}
|
||||
|
||||
context.setValues(this.contextKey, parsedObjects);
|
||||
LOGGER.debug("Parsed {} CSV objects", parsedObjects.size());
|
||||
return true;
|
||||
}
|
||||
catch (final Exception ex) {
|
||||
LOGGER.warn("Failed to parse CSV response", ex);
|
||||
this.addCsvResponseErrorPrompt(context);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private List<Object> parseCsv(final String csvText) throws Exception {
|
||||
final List<Object> result = new ArrayList<>();
|
||||
final String[] lines = csvText.split("\n");
|
||||
|
||||
int startLine = 0;
|
||||
|
||||
for (int i = 0; i < lines.length; i++) {
|
||||
final String line = lines[i].trim();
|
||||
|
||||
if (line.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!line.contains(this.fieldSeparator)) {
|
||||
startLine = i + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
final String lowerLine = line.toLowerCase();
|
||||
boolean isHeader = false;
|
||||
for (final String fieldName : this.fieldNames) {
|
||||
if (lowerLine.contains(fieldName.toLowerCase())) {
|
||||
isHeader = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isHeader) {
|
||||
startLine = i + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
for (int i = startLine; i < lines.length; i++) {
|
||||
final String line = lines[i].trim();
|
||||
|
||||
if (line.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!line.contains(this.fieldSeparator)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
final Object obj = this.parseLine(line);
|
||||
if (obj != null) {
|
||||
result.add(obj);
|
||||
LOGGER.debug("Parsed CSV object: {}", obj);
|
||||
}
|
||||
}
|
||||
catch (final Exception ex) {
|
||||
LOGGER.debug("Failed to parse line '{}': {}", line, ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private Object parseLine(final String line) throws Exception {
|
||||
final String[] parts = line.split(this.fieldSeparatorQuoted, -1);
|
||||
if (parts.length != this.fields.length) {
|
||||
LOGGER.debug("Field count mismatch: expected {}, got {} for line '{}'",
|
||||
this.fields.length, parts.length, line);
|
||||
return null;
|
||||
}
|
||||
|
||||
final Object[] args = new Object[this.fields.length];
|
||||
|
||||
for (int i = 0; i < this.fields.length; i++) {
|
||||
final String value = parts[i].trim();
|
||||
args[i] = this.parseValue(value, this.fields[i]);
|
||||
}
|
||||
|
||||
return this.constructor.newInstance(args);
|
||||
}
|
||||
|
||||
private Object parseValue(final String value, final Field field) {
|
||||
final String cleanValue = this.stripQuotes(value);
|
||||
final Class<?> fieldType = field.getType();
|
||||
|
||||
if (cleanValue.isEmpty() || cleanValue.equalsIgnoreCase("null")) {
|
||||
if (fieldType == List.class) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (fieldType == String.class) {
|
||||
return cleanValue;
|
||||
}
|
||||
|
||||
if (fieldType == Integer.class || fieldType == int.class) {
|
||||
return Integer.parseInt(cleanValue);
|
||||
}
|
||||
|
||||
if (fieldType == Long.class || fieldType == long.class) {
|
||||
return Long.parseLong(cleanValue);
|
||||
}
|
||||
|
||||
if (fieldType == Double.class || fieldType == double.class) {
|
||||
return Double.parseDouble(cleanValue);
|
||||
}
|
||||
|
||||
if (fieldType == Boolean.class || fieldType == boolean.class) {
|
||||
return Boolean.parseBoolean(cleanValue);
|
||||
}
|
||||
|
||||
if (fieldType == List.class) {
|
||||
if (this.listSeparatorQuoted == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
final Type genericType = field.getGenericType();
|
||||
if (genericType instanceof ParameterizedType) {
|
||||
final ParameterizedType paramType = (ParameterizedType) genericType;
|
||||
final Type[] typeArgs = paramType.getActualTypeArguments();
|
||||
|
||||
if (typeArgs.length > 0 && typeArgs[0] == String.class) {
|
||||
if (cleanValue.isEmpty()) {
|
||||
return new ArrayList<String>();
|
||||
}
|
||||
|
||||
final String[] items = cleanValue.split(this.listSeparatorQuoted);
|
||||
final List<String> list = new ArrayList<>();
|
||||
for (final String item : items) {
|
||||
final String trimmed = item.trim();
|
||||
if (!trimmed.isEmpty()) {
|
||||
list.add(trimmed);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Unsupported field type: " + fieldType.getName());
|
||||
}
|
||||
|
||||
private void addCsvResponseErrorPrompt(final AgentContext context) {
|
||||
final var turn = context.getConversation().getConversation().getLast();
|
||||
context.getConversation().replaceTurn(new Conversation.Turn(
|
||||
turn.type(),
|
||||
turn.text() + this.errorPromptSuffix));
|
||||
}
|
||||
|
||||
private String stripQuotes(final String value) {
|
||||
if (value == null || value.length() < 2) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if ((value.startsWith("\"")
|
||||
&& value.endsWith("\""))
|
||||
|| (value.startsWith("'")
|
||||
&& value.endsWith("'"))) {
|
||||
return value.substring(1, value.length() - 1);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ import de.extio.lmlib.agent.AgentContext;
|
||||
import de.extio.lmlib.client.Chunk;
|
||||
|
||||
public interface StreamedAgentResponseHandler extends AgentResponseHandler {
|
||||
|
||||
void beforeStream(AgentContext context);
|
||||
|
||||
void beforeStream(AgentContext context);
|
||||
|
||||
boolean handleChunk(Chunk chunk, AgentContext context);
|
||||
|
||||
boolean handleChunk(Chunk chunk, AgentContext context);
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user