Add CsvAgentResponseHandler

This commit is contained in:
extio-de
2025-12-09 21:18:39 +01:00
parent e7c8dc3e2a
commit 353870dd79
5 changed files with 753 additions and 270 deletions

View File

@@ -4,7 +4,7 @@
<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.9.2</version> <version>1.9.4</version>
<properties> <properties>
<java.version>21</java.version> <java.version>21</java.version>

View File

@@ -1,11 +1,6 @@
package de.extio.lmlib.agent.responsehandler; package de.extio.lmlib.agent.responsehandler;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@@ -20,68 +15,32 @@ public class CsvAgentResponseHandler implements AgentResponseHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(CsvAgentResponseHandler.class); private static final Logger LOGGER = LoggerFactory.getLogger(CsvAgentResponseHandler.class);
private final Class<?> targetClass; private final List<String> headings;
private final String contextKey; private final String keyPrefix;
private final String fieldSeparator; 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 fieldSeparatorQuoted;
private final String listSeparatorQuoted;
private final String errorPromptSuffix; private final String errorPromptSuffix;
public CsvAgentResponseHandler(final Class<?> targetClass, final String contextKey) { public CsvAgentResponseHandler(final List<String> headings, final String keyPrefix) {
this(targetClass, contextKey, ";", ","); this(headings, keyPrefix, ";");
} }
public CsvAgentResponseHandler(final Class<?> targetClass, final String contextKey, public CsvAgentResponseHandler(final List<String> headings, final String keyPrefix, final String fieldSeparator) {
final String fieldSeparator, final String listSeparator) { if (headings == null || headings.isEmpty()) {
this.targetClass = targetClass; throw new IllegalArgumentException("Headings list cannot be null or empty");
this.contextKey = contextKey; }
this.headings = List.copyOf(headings);
this.keyPrefix = keyPrefix != null ? keyPrefix : "";
this.fieldSeparator = fieldSeparator; 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.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. " + this.errorPromptSuffix = "\n\nThe previous response could not be fully processed or validated. " +
"Please format the response as CSV with '" + fieldSeparator + "' as field separator" + "Please format the response as CSV with '" + fieldSeparator + "' as field separator. " +
listInfo + ". One object per line."; "Expected columns: " + String.join(", ", headings) + ". One row 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 @Override
@@ -93,15 +52,13 @@ public class CsvAgentResponseHandler implements AgentResponseHandler {
} }
try { try {
final List<Object> parsedObjects = this.parseCsv(resp); final boolean success = this.parseCsv(resp, context);
if (parsedObjects.isEmpty()) { if (!success) {
LOGGER.warn("No valid CSV data parsed from response"); LOGGER.warn("No valid CSV data parsed from response");
this.addCsvResponseErrorPrompt(context); this.addCsvResponseErrorPrompt(context);
return false; return false;
} }
context.setValues(this.contextKey, parsedObjects);
LOGGER.debug("Parsed {} CSV objects", parsedObjects.size());
return true; return true;
} }
catch (final Exception ex) { catch (final Exception ex) {
@@ -111,8 +68,7 @@ public class CsvAgentResponseHandler implements AgentResponseHandler {
} }
} }
private List<Object> parseCsv(final String csvText) throws Exception { private boolean parseCsv(final String csvText, final AgentContext context) {
final List<Object> result = new ArrayList<>();
final String[] lines = csvText.split("\n"); final String[] lines = csvText.split("\n");
int startLine = 0; int startLine = 0;
@@ -131,8 +87,8 @@ public class CsvAgentResponseHandler implements AgentResponseHandler {
final String lowerLine = line.toLowerCase(); final String lowerLine = line.toLowerCase();
boolean isHeader = false; boolean isHeader = false;
for (final String fieldName : this.fieldNames) { for (final String heading : this.headings) {
if (lowerLine.contains(fieldName.toLowerCase())) { if (lowerLine.contains(heading.toLowerCase())) {
isHeader = true; isHeader = true;
break; break;
} }
@@ -146,6 +102,12 @@ public class CsvAgentResponseHandler implements AgentResponseHandler {
break; 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++) { for (int i = startLine; i < lines.length; i++) {
final String line = lines[i].trim(); final String line = lines[i].trim();
@@ -158,97 +120,36 @@ public class CsvAgentResponseHandler implements AgentResponseHandler {
} }
try { try {
final Object obj = this.parseLine(line); final String[] parts = line.split(this.fieldSeparatorQuoted, -1);
if (obj != null) { if (parts.length != this.headings.size()) {
result.add(obj); LOGGER.debug("Column count mismatch: expected {}, got {} for line '{}'",
LOGGER.debug("Parsed CSV object: {}", obj); this.headings.size(), parts.length, line);
continue;
} }
for (int col = 0; col < parts.length; col++) {
final String value = parts[col].trim();
columnData.get(col).add(value);
}
rowCount++;
} }
catch (final Exception ex) { catch (final Exception ex) {
LOGGER.debug("Failed to parse line '{}': {}", line, ex.getMessage()); LOGGER.debug("Failed to parse line '{}': {}", line, ex.getMessage());
} }
} }
return result; if (rowCount == 0) {
} return false;
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.headings.size(); i++) {
final String key = this.keyPrefix + this.headings.get(i);
for (int i = 0; i < this.fields.length; i++) { context.setStringValues(key, columnData.get(i));
final String value = parts[i].trim(); LOGGER.debug("Stored {} values in context key '{}'", columnData.get(i).size(), key);
args[i] = this.parseValue(value, this.fields[i]);
} }
return this.constructor.newInstance(args); LOGGER.debug("Parsed {} CSV rows into {} columns", rowCount, this.headings.size());
} return true;
private Object parseValue(final String value, final Field field) {
final Class<?> fieldType = field.getType();
if (value.isEmpty() || value.equalsIgnoreCase("null")) {
if (fieldType == List.class) {
return new ArrayList<>();
}
return null;
}
if (fieldType == String.class) {
return value;
}
if (fieldType == Integer.class || fieldType == int.class) {
return Integer.parseInt(value);
}
if (fieldType == Long.class || fieldType == long.class) {
return Long.parseLong(value);
}
if (fieldType == Double.class || fieldType == double.class) {
return Double.parseDouble(value);
}
if (fieldType == Boolean.class || fieldType == boolean.class) {
return Boolean.parseBoolean(value);
}
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 (value.isEmpty()) {
return new ArrayList<String>();
}
final String[] items = value.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) { private void addCsvResponseErrorPrompt(final AgentContext context) {

View File

@@ -0,0 +1,260 @@
package de.extio.lmlib.agent.responsehandler;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import de.extio.lmlib.agent.AgentContext;
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 Class<?> fieldType = field.getType();
if (value.isEmpty() || value.equalsIgnoreCase("null")) {
if (fieldType == List.class) {
return new ArrayList<>();
}
return null;
}
if (fieldType == String.class) {
return value;
}
if (fieldType == Integer.class || fieldType == int.class) {
return Integer.parseInt(value);
}
if (fieldType == Long.class || fieldType == long.class) {
return Long.parseLong(value);
}
if (fieldType == Double.class || fieldType == double.class) {
return Double.parseDouble(value);
}
if (fieldType == Boolean.class || fieldType == boolean.class) {
return Boolean.parseBoolean(value);
}
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 (value.isEmpty()) {
return new ArrayList<String>();
}
final String[] items = value.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));
}
}

View File

@@ -1,7 +1,9 @@
package de.extio.lmlib.agent.responsehandler; package de.extio.lmlib.agent.responsehandler;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.List; import java.util.List;
@@ -14,19 +16,15 @@ import de.extio.lmlib.client.Conversation;
public class CsvAgentResponseHandlerTest { public class CsvAgentResponseHandlerTest {
public static record TestObject(String name, List<String> tags, Integer value) {
}
@Test @Test
public void testSimpleCsvParsing() { public void testSimpleCsvParsing() {
final CsvAgentResponseHandler handler = new CsvAgentResponseHandler(TestObject.class, "_test_"); final CsvAgentResponseHandler handler = new CsvAgentResponseHandler(List.of("a", "b", "c"), "xy_");
final AgentContext context = new AgentContext(); final AgentContext context = new AgentContext();
context.setConversation(Conversation.create("test")); context.setConversation(Conversation.create("test"));
final String csvResponse = """ final String csvResponse = """
item1;tag1,tag2;42 1;2;3
item2;tag3;100 4;5;6
item3;;50
"""; """;
final Completion completion = new Completion(csvResponse, null, null, null); final Completion completion = new Completion(csvResponse, null, null, null);
@@ -34,32 +32,30 @@ public class CsvAgentResponseHandlerTest {
assertTrue(result); assertTrue(result);
final List<TestObject> objects = context.getValues("_test_", TestObject.class); final List<String> aValues = context.getStringValues("xy_a");
assertEquals(3, objects.size()); final List<String> bValues = context.getStringValues("xy_b");
final List<String> cValues = context.getStringValues("xy_c");
assertEquals("item1", objects.get(0).name()); assertNotNull(aValues);
assertEquals(List.of("tag1", "tag2"), objects.get(0).tags()); assertNotNull(bValues);
assertEquals(42, objects.get(0).value()); assertNotNull(cValues);
assertEquals("item2", objects.get(1).name()); assertEquals(List.of("1", "4"), aValues);
assertEquals(List.of("tag3"), objects.get(1).tags()); assertEquals(List.of("2", "5"), bValues);
assertEquals(100, objects.get(1).value()); assertEquals(List.of("3", "6"), cValues);
assertEquals("item3", objects.get(2).name());
assertTrue(objects.get(2).tags().isEmpty());
assertEquals(50, objects.get(2).value());
} }
@Test @Test
public void testCsvWithHeader() { public void testWithHeader() {
final CsvAgentResponseHandler handler = new CsvAgentResponseHandler(TestObject.class, "_test_"); final CsvAgentResponseHandler handler = new CsvAgentResponseHandler(List.of("name", "age", "city"), "person_");
final AgentContext context = new AgentContext(); final AgentContext context = new AgentContext();
context.setConversation(Conversation.create("test")); context.setConversation(Conversation.create("test"));
final String csvResponse = """ final String csvResponse = """
name;tags;value name;age;city
item1;tag1,tag2;42 Alice;30;Berlin
item2;tag3;100 Bob;25;Munich
Charlie;35;Hamburg
"""; """;
final Completion completion = new Completion(csvResponse, null, null, null); final Completion completion = new Completion(csvResponse, null, null, null);
@@ -67,24 +63,24 @@ public class CsvAgentResponseHandlerTest {
assertTrue(result); assertTrue(result);
final List<TestObject> objects = context.getValues("_test_", TestObject.class); final List<String> names = context.getStringValues("person_name");
assertEquals(2, objects.size()); final List<String> ages = context.getStringValues("person_age");
final List<String> cities = context.getStringValues("person_city");
assertEquals("item1", objects.get(0).name()); assertEquals(List.of("Alice", "Bob", "Charlie"), names);
assertEquals("item2", objects.get(1).name()); assertEquals(List.of("30", "25", "35"), ages);
assertEquals(List.of("Berlin", "Munich", "Hamburg"), cities);
} }
@Test @Test
public void testCsvWithPreamble() { public void testWithEmptyPrefix() {
final CsvAgentResponseHandler handler = new CsvAgentResponseHandler(TestObject.class, "_test_"); final CsvAgentResponseHandler handler = new CsvAgentResponseHandler(List.of("x", "y"), "");
final AgentContext context = new AgentContext(); final AgentContext context = new AgentContext();
context.setConversation(Conversation.create("test")); context.setConversation(Conversation.create("test"));
final String csvResponse = """ final String csvResponse = """
Here is the result: 10;20
30;40
item1;tag1,tag2;42
item2;tag3;100
"""; """;
final Completion completion = new Completion(csvResponse, null, null, null); final Completion completion = new Completion(csvResponse, null, null, null);
@@ -92,23 +88,19 @@ public class CsvAgentResponseHandlerTest {
assertTrue(result); assertTrue(result);
final List<TestObject> objects = context.getValues("_test_", TestObject.class); assertEquals(List.of("10", "30"), context.getStringValues("x"));
assertEquals(2, objects.size()); assertEquals(List.of("20", "40"), context.getStringValues("y"));
} }
@Test @Test
public void testCsvWithHeaderAndPreamble() { public void testWithNullPrefix() {
final CsvAgentResponseHandler handler = new CsvAgentResponseHandler(TestObject.class, "_test_"); final CsvAgentResponseHandler handler = new CsvAgentResponseHandler(List.of("col1", "col2"), null);
final AgentContext context = new AgentContext(); final AgentContext context = new AgentContext();
context.setConversation(Conversation.create("test")); context.setConversation(Conversation.create("test"));
final String csvResponse = """ final String csvResponse = """
Here are the results in CSV format: a;b
c;d
name;tags;value
item1;tag1,tag2;42
item2;tag3;100
item3;;50
"""; """;
final Completion completion = new Completion(csvResponse, null, null, null); final Completion completion = new Completion(csvResponse, null, null, null);
@@ -116,26 +108,22 @@ public class CsvAgentResponseHandlerTest {
assertTrue(result); assertTrue(result);
final List<TestObject> objects = context.getValues("_test_", TestObject.class); assertEquals(List.of("a", "c"), context.getStringValues("col1"));
assertEquals(3, objects.size()); assertEquals(List.of("b", "d"), context.getStringValues("col2"));
assertEquals("item1", objects.get(0).name());
assertEquals(List.of("tag1", "tag2"), objects.get(0).tags());
assertEquals(42, objects.get(0).value());
} }
@Test @Test
public void testCsvWithEmptyLines() { public void testWithExtraTextBeforeData() {
final CsvAgentResponseHandler handler = new CsvAgentResponseHandler(TestObject.class, "_test_"); final CsvAgentResponseHandler handler = new CsvAgentResponseHandler(List.of("a", "b", "c"), "test_");
final AgentContext context = new AgentContext(); final AgentContext context = new AgentContext();
context.setConversation(Conversation.create("test")); context.setConversation(Conversation.create("test"));
final String csvResponse = """ final String csvResponse = """
Here is the CSV data:
item1;tag1,tag2;42 a;b;c
1;2;3
item2;tag3;100 4;5;6
"""; """;
final Completion completion = new Completion(csvResponse, null, null, null); final Completion completion = new Completion(csvResponse, null, null, null);
@@ -143,19 +131,23 @@ public class CsvAgentResponseHandlerTest {
assertTrue(result); assertTrue(result);
final List<TestObject> objects = context.getValues("_test_", TestObject.class); assertEquals(List.of("1", "4"), context.getStringValues("test_a"));
assertEquals(2, objects.size()); assertEquals(List.of("2", "5"), context.getStringValues("test_b"));
assertEquals(List.of("3", "6"), context.getStringValues("test_c"));
} }
@Test @Test
public void testCsvWithNullValues() { public void testWithEmptyLines() {
final CsvAgentResponseHandler handler = new CsvAgentResponseHandler(TestObject.class, "_test_"); final CsvAgentResponseHandler handler = new CsvAgentResponseHandler(List.of("a", "b"), "data_");
final AgentContext context = new AgentContext(); final AgentContext context = new AgentContext();
context.setConversation(Conversation.create("test")); context.setConversation(Conversation.create("test"));
final String csvResponse = """ final String csvResponse = """
item1;tag1,tag2; 1;2
item2;;100
3;4
5;6
"""; """;
final Completion completion = new Completion(csvResponse, null, null, null); final Completion completion = new Completion(csvResponse, null, null, null);
@@ -163,31 +155,19 @@ public class CsvAgentResponseHandlerTest {
assertTrue(result); assertTrue(result);
final List<TestObject> objects = context.getValues("_test_", TestObject.class); assertEquals(List.of("1", "3", "5"), context.getStringValues("data_a"));
assertEquals(2, objects.size()); assertEquals(List.of("2", "4", "6"), context.getStringValues("data_b"));
assertEquals("item1", objects.get(0).name());
assertNotNull(objects.get(0).tags());
assertEquals(null, objects.get(0).value());
assertEquals("item2", objects.get(1).name());
assertTrue(objects.get(1).tags().isEmpty());
assertEquals(100, objects.get(1).value());
}
public static record TagObject(String tag, List<String> aliases, Integer importance) {
} }
@Test @Test
public void testTagObjectExample() { public void testWithWhitespace() {
final CsvAgentResponseHandler handler = new CsvAgentResponseHandler(TagObject.class, "_tag_"); final CsvAgentResponseHandler handler = new CsvAgentResponseHandler(List.of("a", "b", "c"), "ws_");
final AgentContext context = new AgentContext(); final AgentContext context = new AgentContext();
context.setConversation(Conversation.create("test")); context.setConversation(Conversation.create("test"));
final String csvResponse = """ final String csvResponse = """
location_dark_forest;location_forest,location_shadow_woods;25 1 ; 2 ; 3
location_ancient_shrine;location_shrine,location_vine_shrine;65 4 ; 5 ; 6
combat_dragon;combat_wyrm,combat_ancient_dragon;65
"""; """;
final Completion completion = new Completion(csvResponse, null, null, null); final Completion completion = new Completion(csvResponse, null, null, null);
@@ -195,28 +175,20 @@ public class CsvAgentResponseHandlerTest {
assertTrue(result); assertTrue(result);
final List<TagObject> objects = context.getValues("_tag_", TagObject.class); assertEquals(List.of("1", "4"), context.getStringValues("ws_a"));
assertEquals(3, objects.size()); assertEquals(List.of("2", "5"), context.getStringValues("ws_b"));
assertEquals(List.of("3", "6"), context.getStringValues("ws_c"));
assertEquals("location_dark_forest", objects.get(0).tag());
assertEquals(List.of("location_forest", "location_shadow_woods"), objects.get(0).aliases());
assertEquals(25, objects.get(0).importance());
assertEquals("combat_dragon", objects.get(2).tag());
assertEquals(List.of("combat_wyrm", "combat_ancient_dragon"), objects.get(2).aliases());
assertEquals(65, objects.get(2).importance());
} }
@Test @Test
public void testCustomSeparators() { public void testWithCustomSeparator() {
final CsvAgentResponseHandler handler = new CsvAgentResponseHandler( final CsvAgentResponseHandler handler = new CsvAgentResponseHandler(List.of("a", "b", "c"), "custom_", ",");
TagObject.class, "_custom_", "|", ":");
final AgentContext context = new AgentContext(); final AgentContext context = new AgentContext();
context.setConversation(Conversation.create("test")); context.setConversation(Conversation.create("test"));
final String csvResponse = """ final String csvResponse = """
location_dark_forest|location_forest:location_shadow_woods|25 1,2,3
location_ancient_shrine|location_shrine:location_vine_shrine|65 4,5,6
"""; """;
final Completion completion = new Completion(csvResponse, null, null, null); final Completion completion = new Completion(csvResponse, null, null, null);
@@ -224,24 +196,21 @@ public class CsvAgentResponseHandlerTest {
assertTrue(result); assertTrue(result);
final List<TagObject> objects = context.getValues("_custom_", TagObject.class); assertEquals(List.of("1", "4"), context.getStringValues("custom_a"));
assertEquals(2, objects.size()); assertEquals(List.of("2", "5"), context.getStringValues("custom_b"));
assertEquals(List.of("3", "6"), context.getStringValues("custom_c"));
assertEquals("location_dark_forest", objects.get(0).tag());
assertEquals(List.of("location_forest", "location_shadow_woods"), objects.get(0).aliases());
assertEquals(25, objects.get(0).importance());
} }
@Test @Test
public void testNoListSeparator() { public void testColumnCountMismatch() {
final CsvAgentResponseHandler handler = new CsvAgentResponseHandler( final CsvAgentResponseHandler handler = new CsvAgentResponseHandler(List.of("a", "b", "c"), "mismatch_");
TestObject.class, "_nolist_", ";", null);
final AgentContext context = new AgentContext(); final AgentContext context = new AgentContext();
context.setConversation(Conversation.create("test")); context.setConversation(Conversation.create("test"));
final String csvResponse = """ final String csvResponse = """
item1;tag1,tag2,tag3;42 1;2;3
item2;ignored;100 4;5
7;8;9
"""; """;
final Completion completion = new Completion(csvResponse, null, null, null); final Completion completion = new Completion(csvResponse, null, null, null);
@@ -249,15 +218,105 @@ public class CsvAgentResponseHandlerTest {
assertTrue(result); assertTrue(result);
final List<TestObject> objects = context.getValues("_nolist_", TestObject.class); // Only rows with correct column count should be included
assertEquals(2, objects.size()); assertEquals(List.of("1", "7"), context.getStringValues("mismatch_a"));
assertEquals(List.of("2", "8"), context.getStringValues("mismatch_b"));
assertEquals(List.of("3", "9"), context.getStringValues("mismatch_c"));
}
assertEquals("item1", objects.get(0).name()); @Test
assertTrue(objects.get(0).tags().isEmpty()); public void testEmptyResponse() {
assertEquals(42, objects.get(0).value()); final CsvAgentResponseHandler handler = new CsvAgentResponseHandler(List.of("a", "b"), "empty_");
final AgentContext context = new AgentContext();
context.setConversation(Conversation.create("test"));
assertEquals("item2", objects.get(1).name()); final Completion completion = new Completion("", null, null, null);
assertTrue(objects.get(1).tags().isEmpty()); final boolean result = handler.handle(completion, context);
assertEquals(100, objects.get(1).value());
assertFalse(result);
}
@Test
public void testNullResponse() {
final CsvAgentResponseHandler handler = new CsvAgentResponseHandler(List.of("a", "b"), "null_");
final AgentContext context = new AgentContext();
context.setConversation(Conversation.create("test"));
final Completion completion = new Completion(null, null, null, null);
final boolean result = handler.handle(completion, context);
assertFalse(result);
}
@Test
public void testNoValidData() {
final CsvAgentResponseHandler handler = new CsvAgentResponseHandler(List.of("a", "b"), "nodata_");
final AgentContext context = new AgentContext();
context.setConversation(Conversation.create("test"));
final String csvResponse = """
This is just text
without any valid CSV data
""";
final Completion completion = new Completion(csvResponse, null, null, null);
final boolean result = handler.handle(completion, context);
assertFalse(result);
}
@Test
public void testSingleRow() {
final CsvAgentResponseHandler handler = new CsvAgentResponseHandler(List.of("x", "y", "z"), "single_");
final AgentContext context = new AgentContext();
context.setConversation(Conversation.create("test"));
final String csvResponse = "100;200;300";
final Completion completion = new Completion(csvResponse, null, null, null);
final boolean result = handler.handle(completion, context);
assertTrue(result);
assertEquals(List.of("100"), context.getStringValues("single_x"));
assertEquals(List.of("200"), context.getStringValues("single_y"));
assertEquals(List.of("300"), context.getStringValues("single_z"));
}
@Test
public void testMultipleHeaders() {
final CsvAgentResponseHandler handler = new CsvAgentResponseHandler(List.of("id", "value"), "multi_");
final AgentContext context = new AgentContext();
context.setConversation(Conversation.create("test"));
final String csvResponse = """
id;value
ID;VALUE
1;100
2;200
""";
final Completion completion = new Completion(csvResponse, null, null, null);
final boolean result = handler.handle(completion, context);
assertTrue(result);
// Should skip both header rows
assertEquals(List.of("1", "2"), context.getStringValues("multi_id"));
assertEquals(List.of("100", "200"), context.getStringValues("multi_value"));
}
@Test
public void testNullHeadingsList() {
assertThrows(IllegalArgumentException.class, () -> {
new CsvAgentResponseHandler(null, "prefix_");
});
}
@Test
public void testEmptyHeadingsList() {
assertThrows(IllegalArgumentException.class, () -> {
new CsvAgentResponseHandler(List.of(), "prefix_");
});
} }
} }

View File

@@ -0,0 +1,263 @@
package de.extio.lmlib.agent.responsehandler;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.List;
import org.junit.jupiter.api.Test;
import de.extio.lmlib.agent.AgentContext;
import de.extio.lmlib.client.Completion;
import de.extio.lmlib.client.Conversation;
public class CsvUnmarshallingAgentResponseHandlerTest {
public static record TestObject(String name, List<String> tags, Integer value) {
}
@Test
public void testSimpleCsvParsing() {
final CsvUnmarshallingAgentResponseHandler handler = new CsvUnmarshallingAgentResponseHandler(TestObject.class, "_test_");
final AgentContext context = new AgentContext();
context.setConversation(Conversation.create("test"));
final String csvResponse = """
item1;tag1,tag2;42
item2;tag3;100
item3;;50
""";
final Completion completion = new Completion(csvResponse, null, null, null);
final boolean result = handler.handle(completion, context);
assertTrue(result);
final List<TestObject> objects = context.getValues("_test_", TestObject.class);
assertEquals(3, objects.size());
assertEquals("item1", objects.get(0).name());
assertEquals(List.of("tag1", "tag2"), objects.get(0).tags());
assertEquals(42, objects.get(0).value());
assertEquals("item2", objects.get(1).name());
assertEquals(List.of("tag3"), objects.get(1).tags());
assertEquals(100, objects.get(1).value());
assertEquals("item3", objects.get(2).name());
assertTrue(objects.get(2).tags().isEmpty());
assertEquals(50, objects.get(2).value());
}
@Test
public void testCsvWithHeader() {
final CsvUnmarshallingAgentResponseHandler handler = new CsvUnmarshallingAgentResponseHandler(TestObject.class, "_test_");
final AgentContext context = new AgentContext();
context.setConversation(Conversation.create("test"));
final String csvResponse = """
name;tags;value
item1;tag1,tag2;42
item2;tag3;100
""";
final Completion completion = new Completion(csvResponse, null, null, null);
final boolean result = handler.handle(completion, context);
assertTrue(result);
final List<TestObject> objects = context.getValues("_test_", TestObject.class);
assertEquals(2, objects.size());
assertEquals("item1", objects.get(0).name());
assertEquals("item2", objects.get(1).name());
}
@Test
public void testCsvWithPreamble() {
final CsvUnmarshallingAgentResponseHandler handler = new CsvUnmarshallingAgentResponseHandler(TestObject.class, "_test_");
final AgentContext context = new AgentContext();
context.setConversation(Conversation.create("test"));
final String csvResponse = """
Here is the result:
item1;tag1,tag2;42
item2;tag3;100
""";
final Completion completion = new Completion(csvResponse, null, null, null);
final boolean result = handler.handle(completion, context);
assertTrue(result);
final List<TestObject> objects = context.getValues("_test_", TestObject.class);
assertEquals(2, objects.size());
}
@Test
public void testCsvWithHeaderAndPreamble() {
final CsvUnmarshallingAgentResponseHandler handler = new CsvUnmarshallingAgentResponseHandler(TestObject.class, "_test_");
final AgentContext context = new AgentContext();
context.setConversation(Conversation.create("test"));
final String csvResponse = """
Here are the results in CSV format:
name;tags;value
item1;tag1,tag2;42
item2;tag3;100
item3;;50
""";
final Completion completion = new Completion(csvResponse, null, null, null);
final boolean result = handler.handle(completion, context);
assertTrue(result);
final List<TestObject> objects = context.getValues("_test_", TestObject.class);
assertEquals(3, objects.size());
assertEquals("item1", objects.get(0).name());
assertEquals(List.of("tag1", "tag2"), objects.get(0).tags());
assertEquals(42, objects.get(0).value());
}
@Test
public void testCsvWithEmptyLines() {
final CsvUnmarshallingAgentResponseHandler handler = new CsvUnmarshallingAgentResponseHandler(TestObject.class, "_test_");
final AgentContext context = new AgentContext();
context.setConversation(Conversation.create("test"));
final String csvResponse = """
item1;tag1,tag2;42
item2;tag3;100
""";
final Completion completion = new Completion(csvResponse, null, null, null);
final boolean result = handler.handle(completion, context);
assertTrue(result);
final List<TestObject> objects = context.getValues("_test_", TestObject.class);
assertEquals(2, objects.size());
}
@Test
public void testCsvWithNullValues() {
final CsvUnmarshallingAgentResponseHandler handler = new CsvUnmarshallingAgentResponseHandler(TestObject.class, "_test_");
final AgentContext context = new AgentContext();
context.setConversation(Conversation.create("test"));
final String csvResponse = """
item1;tag1,tag2;
item2;;100
""";
final Completion completion = new Completion(csvResponse, null, null, null);
final boolean result = handler.handle(completion, context);
assertTrue(result);
final List<TestObject> objects = context.getValues("_test_", TestObject.class);
assertEquals(2, objects.size());
assertEquals("item1", objects.get(0).name());
assertNotNull(objects.get(0).tags());
assertEquals(null, objects.get(0).value());
assertEquals("item2", objects.get(1).name());
assertTrue(objects.get(1).tags().isEmpty());
assertEquals(100, objects.get(1).value());
}
public static record TagObject(String tag, List<String> aliases, Integer importance) {
}
@Test
public void testTagObjectExample() {
final CsvUnmarshallingAgentResponseHandler handler = new CsvUnmarshallingAgentResponseHandler(TagObject.class, "_tag_");
final AgentContext context = new AgentContext();
context.setConversation(Conversation.create("test"));
final String csvResponse = """
location_dark_forest;location_forest,location_shadow_woods;25
location_ancient_shrine;location_shrine,location_vine_shrine;65
combat_dragon;combat_wyrm,combat_ancient_dragon;65
""";
final Completion completion = new Completion(csvResponse, null, null, null);
final boolean result = handler.handle(completion, context);
assertTrue(result);
final List<TagObject> objects = context.getValues("_tag_", TagObject.class);
assertEquals(3, objects.size());
assertEquals("location_dark_forest", objects.get(0).tag());
assertEquals(List.of("location_forest", "location_shadow_woods"), objects.get(0).aliases());
assertEquals(25, objects.get(0).importance());
assertEquals("combat_dragon", objects.get(2).tag());
assertEquals(List.of("combat_wyrm", "combat_ancient_dragon"), objects.get(2).aliases());
assertEquals(65, objects.get(2).importance());
}
@Test
public void testCustomSeparators() {
final CsvUnmarshallingAgentResponseHandler handler = new CsvUnmarshallingAgentResponseHandler(
TagObject.class, "_custom_", "|", ":");
final AgentContext context = new AgentContext();
context.setConversation(Conversation.create("test"));
final String csvResponse = """
location_dark_forest|location_forest:location_shadow_woods|25
location_ancient_shrine|location_shrine:location_vine_shrine|65
""";
final Completion completion = new Completion(csvResponse, null, null, null);
final boolean result = handler.handle(completion, context);
assertTrue(result);
final List<TagObject> objects = context.getValues("_custom_", TagObject.class);
assertEquals(2, objects.size());
assertEquals("location_dark_forest", objects.get(0).tag());
assertEquals(List.of("location_forest", "location_shadow_woods"), objects.get(0).aliases());
assertEquals(25, objects.get(0).importance());
}
@Test
public void testNoListSeparator() {
final CsvUnmarshallingAgentResponseHandler handler = new CsvUnmarshallingAgentResponseHandler(
TestObject.class, "_nolist_", ";", null);
final AgentContext context = new AgentContext();
context.setConversation(Conversation.create("test"));
final String csvResponse = """
item1;tag1,tag2,tag3;42
item2;ignored;100
""";
final Completion completion = new Completion(csvResponse, null, null, null);
final boolean result = handler.handle(completion, context);
assertTrue(result);
final List<TestObject> objects = context.getValues("_nolist_", TestObject.class);
assertEquals(2, objects.size());
assertEquals("item1", objects.get(0).name());
assertTrue(objects.get(0).tags().isEmpty());
assertEquals(42, objects.get(0).value());
assertEquals("item2", objects.get(1).name());
assertTrue(objects.get(1).tags().isEmpty());
assertEquals(100, objects.get(1).value());
}
}