From 353870dd79d91a73436fde22dfdfe4fe8e8778b4 Mon Sep 17 00:00:00 2001 From: extio-de Date: Tue, 9 Dec 2025 21:18:39 +0100 Subject: [PATCH] Add CsvAgentResponseHandler --- pom.xml | 2 +- .../CsvAgentResponseHandler.java | 183 +++------- .../CsvUnmarshallingAgentResponseHandler.java | 260 +++++++++++++++ .../CsvAgentResponseHandlerTest.java | 315 +++++++++++------- ...UnmarshallingAgentResponseHandlerTest.java | 263 +++++++++++++++ 5 files changed, 753 insertions(+), 270 deletions(-) create mode 100644 src/main/java/de/extio/lmlib/agent/responsehandler/CsvUnmarshallingAgentResponseHandler.java create mode 100644 src/test/java/de/extio/lmlib/agent/responsehandler/CsvUnmarshallingAgentResponseHandlerTest.java diff --git a/pom.xml b/pom.xml index 6f65991..90c2f58 100755 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ lm-lib de.extio lm-lib - 1.9.2 + 1.9.4 21 diff --git a/src/main/java/de/extio/lmlib/agent/responsehandler/CsvAgentResponseHandler.java b/src/main/java/de/extio/lmlib/agent/responsehandler/CsvAgentResponseHandler.java index ec47ceb..1c33bec 100644 --- a/src/main/java/de/extio/lmlib/agent/responsehandler/CsvAgentResponseHandler.java +++ b/src/main/java/de/extio/lmlib/agent/responsehandler/CsvAgentResponseHandler.java @@ -1,11 +1,6 @@ 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; @@ -20,68 +15,32 @@ public class CsvAgentResponseHandler implements AgentResponseHandler { private static final Logger LOGGER = LoggerFactory.getLogger(CsvAgentResponseHandler.class); - private final Class targetClass; + private final List headings; - private final String contextKey; + private final String keyPrefix; private final String fieldSeparator; - private final String listSeparator; - - private final Field[] fields; - - private final List fieldNames; - - private final Constructor constructor; - private final String fieldSeparatorQuoted; - private final String listSeparatorQuoted; - private final String errorPromptSuffix; - public CsvAgentResponseHandler(final Class targetClass, final String contextKey) { - this(targetClass, contextKey, ";", ","); + public CsvAgentResponseHandler(final List headings, final String keyPrefix) { + this(headings, keyPrefix, ";"); } - public CsvAgentResponseHandler(final Class targetClass, final String contextKey, - final String fieldSeparator, final String listSeparator) { - this.targetClass = targetClass; - this.contextKey = contextKey; + public CsvAgentResponseHandler(final List 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.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; + "Please format the response as CSV with '" + fieldSeparator + "' as field separator. " + + "Expected columns: " + String.join(", ", headings) + ". One row per line."; } @Override @@ -93,15 +52,13 @@ public class CsvAgentResponseHandler implements AgentResponseHandler { } try { - final List parsedObjects = this.parseCsv(resp); - if (parsedObjects.isEmpty()) { + final boolean success = this.parseCsv(resp, context); + if (!success) { 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) { @@ -111,8 +68,7 @@ public class CsvAgentResponseHandler implements AgentResponseHandler { } } - private List parseCsv(final String csvText) throws Exception { - final List result = new ArrayList<>(); + private boolean parseCsv(final String csvText, final AgentContext context) { final String[] lines = csvText.split("\n"); int startLine = 0; @@ -131,8 +87,8 @@ public class CsvAgentResponseHandler implements AgentResponseHandler { final String lowerLine = line.toLowerCase(); boolean isHeader = false; - for (final String fieldName : this.fieldNames) { - if (lowerLine.contains(fieldName.toLowerCase())) { + for (final String heading : this.headings) { + if (lowerLine.contains(heading.toLowerCase())) { isHeader = true; break; } @@ -146,6 +102,12 @@ public class CsvAgentResponseHandler implements AgentResponseHandler { break; } + final List> 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(); @@ -158,97 +120,36 @@ public class CsvAgentResponseHandler implements AgentResponseHandler { } try { - final Object obj = this.parseLine(line); - if (obj != null) { - result.add(obj); - LOGGER.debug("Parsed CSV object: {}", obj); + 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 = parts[col].trim(); + columnData.get(col).add(value); + } + rowCount++; } 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; + if (rowCount == 0) { + return false; } - 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]); + 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); } - 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(); - } - - final String[] items = value.split(this.listSeparatorQuoted); - final List 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()); + LOGGER.debug("Parsed {} CSV rows into {} columns", rowCount, this.headings.size()); + return true; } private void addCsvResponseErrorPrompt(final AgentContext context) { diff --git a/src/main/java/de/extio/lmlib/agent/responsehandler/CsvUnmarshallingAgentResponseHandler.java b/src/main/java/de/extio/lmlib/agent/responsehandler/CsvUnmarshallingAgentResponseHandler.java new file mode 100644 index 0000000..a19cf9c --- /dev/null +++ b/src/main/java/de/extio/lmlib/agent/responsehandler/CsvUnmarshallingAgentResponseHandler.java @@ -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 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 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 parseCsv(final String csvText) throws Exception { + final List 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(); + } + + final String[] items = value.split(this.listSeparatorQuoted); + final List 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)); + } +} diff --git a/src/test/java/de/extio/lmlib/agent/responsehandler/CsvAgentResponseHandlerTest.java b/src/test/java/de/extio/lmlib/agent/responsehandler/CsvAgentResponseHandlerTest.java index 46270fb..7213b88 100644 --- a/src/test/java/de/extio/lmlib/agent/responsehandler/CsvAgentResponseHandlerTest.java +++ b/src/test/java/de/extio/lmlib/agent/responsehandler/CsvAgentResponseHandlerTest.java @@ -1,7 +1,9 @@ package de.extio.lmlib.agent.responsehandler; 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.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.List; @@ -14,19 +16,15 @@ import de.extio.lmlib.client.Conversation; public class CsvAgentResponseHandlerTest { - public static record TestObject(String name, List tags, Integer value) { - } - @Test 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(); context.setConversation(Conversation.create("test")); final String csvResponse = """ - item1;tag1,tag2;42 - item2;tag3;100 - item3;;50 + 1;2;3 + 4;5;6 """; final Completion completion = new Completion(csvResponse, null, null, null); @@ -34,32 +32,30 @@ public class CsvAgentResponseHandlerTest { assertTrue(result); - final List objects = context.getValues("_test_", TestObject.class); - assertEquals(3, objects.size()); + final List aValues = context.getStringValues("xy_a"); + final List bValues = context.getStringValues("xy_b"); + final List cValues = context.getStringValues("xy_c"); - assertEquals("item1", objects.get(0).name()); - assertEquals(List.of("tag1", "tag2"), objects.get(0).tags()); - assertEquals(42, objects.get(0).value()); + assertNotNull(aValues); + assertNotNull(bValues); + assertNotNull(cValues); - 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()); + assertEquals(List.of("1", "4"), aValues); + assertEquals(List.of("2", "5"), bValues); + assertEquals(List.of("3", "6"), cValues); } @Test - public void testCsvWithHeader() { - final CsvAgentResponseHandler handler = new CsvAgentResponseHandler(TestObject.class, "_test_"); + public void testWithHeader() { + final CsvAgentResponseHandler handler = new CsvAgentResponseHandler(List.of("name", "age", "city"), "person_"); final AgentContext context = new AgentContext(); context.setConversation(Conversation.create("test")); final String csvResponse = """ - name;tags;value - item1;tag1,tag2;42 - item2;tag3;100 + name;age;city + Alice;30;Berlin + Bob;25;Munich + Charlie;35;Hamburg """; final Completion completion = new Completion(csvResponse, null, null, null); @@ -67,24 +63,67 @@ public class CsvAgentResponseHandlerTest { assertTrue(result); - final List objects = context.getValues("_test_", TestObject.class); - assertEquals(2, objects.size()); + final List names = context.getStringValues("person_name"); + final List ages = context.getStringValues("person_age"); + final List cities = context.getStringValues("person_city"); - assertEquals("item1", objects.get(0).name()); - assertEquals("item2", objects.get(1).name()); + assertEquals(List.of("Alice", "Bob", "Charlie"), names); + assertEquals(List.of("30", "25", "35"), ages); + assertEquals(List.of("Berlin", "Munich", "Hamburg"), cities); } @Test - public void testCsvWithPreamble() { - final CsvAgentResponseHandler handler = new CsvAgentResponseHandler(TestObject.class, "_test_"); + public void testWithEmptyPrefix() { + final CsvAgentResponseHandler handler = new CsvAgentResponseHandler(List.of("x", "y"), ""); final AgentContext context = new AgentContext(); context.setConversation(Conversation.create("test")); final String csvResponse = """ - Here is the result: + 10;20 + 30;40 + """; + + final Completion completion = new Completion(csvResponse, null, null, null); + final boolean result = handler.handle(completion, context); + + assertTrue(result); + + assertEquals(List.of("10", "30"), context.getStringValues("x")); + assertEquals(List.of("20", "40"), context.getStringValues("y")); + } + + @Test + public void testWithNullPrefix() { + final CsvAgentResponseHandler handler = new CsvAgentResponseHandler(List.of("col1", "col2"), null); + final AgentContext context = new AgentContext(); + context.setConversation(Conversation.create("test")); + + final String csvResponse = """ + a;b + c;d + """; + + final Completion completion = new Completion(csvResponse, null, null, null); + final boolean result = handler.handle(completion, context); + + assertTrue(result); + + assertEquals(List.of("a", "c"), context.getStringValues("col1")); + assertEquals(List.of("b", "d"), context.getStringValues("col2")); + } + + @Test + public void testWithExtraTextBeforeData() { + final CsvAgentResponseHandler handler = new CsvAgentResponseHandler(List.of("a", "b", "c"), "test_"); + final AgentContext context = new AgentContext(); + context.setConversation(Conversation.create("test")); + + final String csvResponse = """ + Here is the CSV data: - item1;tag1,tag2;42 - item2;tag3;100 + a;b;c + 1;2;3 + 4;5;6 """; final Completion completion = new Completion(csvResponse, null, null, null); @@ -92,50 +131,23 @@ public class CsvAgentResponseHandlerTest { assertTrue(result); - final List objects = context.getValues("_test_", TestObject.class); - assertEquals(2, objects.size()); + assertEquals(List.of("1", "4"), context.getStringValues("test_a")); + assertEquals(List.of("2", "5"), context.getStringValues("test_b")); + assertEquals(List.of("3", "6"), context.getStringValues("test_c")); } @Test - public void testCsvWithHeaderAndPreamble() { - final CsvAgentResponseHandler handler = new CsvAgentResponseHandler(TestObject.class, "_test_"); + public void testWithEmptyLines() { + final CsvAgentResponseHandler handler = new CsvAgentResponseHandler(List.of("a", "b"), "data_"); final AgentContext context = new AgentContext(); context.setConversation(Conversation.create("test")); final String csvResponse = """ - Here are the results in CSV format: + 1;2 - 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 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 CsvAgentResponseHandler handler = new CsvAgentResponseHandler(TestObject.class, "_test_"); - final AgentContext context = new AgentContext(); - context.setConversation(Conversation.create("test")); - - final String csvResponse = """ - - item1;tag1,tag2;42 - - item2;tag3;100 + 3;4 + 5;6 """; final Completion completion = new Completion(csvResponse, null, null, null); @@ -143,19 +155,19 @@ public class CsvAgentResponseHandlerTest { assertTrue(result); - final List objects = context.getValues("_test_", TestObject.class); - assertEquals(2, objects.size()); + assertEquals(List.of("1", "3", "5"), context.getStringValues("data_a")); + assertEquals(List.of("2", "4", "6"), context.getStringValues("data_b")); } @Test - public void testCsvWithNullValues() { - final CsvAgentResponseHandler handler = new CsvAgentResponseHandler(TestObject.class, "_test_"); + public void testWithWhitespace() { + final CsvAgentResponseHandler handler = new CsvAgentResponseHandler(List.of("a", "b", "c"), "ws_"); final AgentContext context = new AgentContext(); context.setConversation(Conversation.create("test")); final String csvResponse = """ - item1;tag1,tag2; - item2;;100 + 1 ; 2 ; 3 + 4 ; 5 ; 6 """; final Completion completion = new Completion(csvResponse, null, null, null); @@ -163,31 +175,20 @@ public class CsvAgentResponseHandlerTest { assertTrue(result); - final List 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 aliases, Integer importance) { + assertEquals(List.of("1", "4"), context.getStringValues("ws_a")); + assertEquals(List.of("2", "5"), context.getStringValues("ws_b")); + assertEquals(List.of("3", "6"), context.getStringValues("ws_c")); } @Test - public void testTagObjectExample() { - final CsvAgentResponseHandler handler = new CsvAgentResponseHandler(TagObject.class, "_tag_"); + public void testWithCustomSeparator() { + final CsvAgentResponseHandler handler = new CsvAgentResponseHandler(List.of("a", "b", "c"), "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 - combat_dragon;combat_wyrm,combat_ancient_dragon;65 + 1,2,3 + 4,5,6 """; final Completion completion = new Completion(csvResponse, null, null, null); @@ -195,28 +196,21 @@ public class CsvAgentResponseHandlerTest { assertTrue(result); - final List 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()); + assertEquals(List.of("1", "4"), context.getStringValues("custom_a")); + assertEquals(List.of("2", "5"), context.getStringValues("custom_b")); + assertEquals(List.of("3", "6"), context.getStringValues("custom_c")); } @Test - public void testCustomSeparators() { - final CsvAgentResponseHandler handler = new CsvAgentResponseHandler( - TagObject.class, "_custom_", "|", ":"); + public void testColumnCountMismatch() { + final CsvAgentResponseHandler handler = new CsvAgentResponseHandler(List.of("a", "b", "c"), "mismatch_"); 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 + 1;2;3 + 4;5 + 7;8;9 """; final Completion completion = new Completion(csvResponse, null, null, null); @@ -224,24 +218,82 @@ public class CsvAgentResponseHandlerTest { assertTrue(result); - final List 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()); + // Only rows with correct column count should be included + 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")); } @Test - public void testNoListSeparator() { - final CsvAgentResponseHandler handler = new CsvAgentResponseHandler( - TestObject.class, "_nolist_", ";", null); + public void testEmptyResponse() { + final CsvAgentResponseHandler handler = new CsvAgentResponseHandler(List.of("a", "b"), "empty_"); + final AgentContext context = new AgentContext(); + context.setConversation(Conversation.create("test")); + + final Completion completion = new Completion("", null, null, null); + final boolean result = handler.handle(completion, context); + + 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 = """ - item1;tag1,tag2,tag3;42 - item2;ignored;100 + 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); @@ -249,15 +301,22 @@ public class CsvAgentResponseHandlerTest { assertTrue(result); - final List 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()); + // 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_"); + }); } } diff --git a/src/test/java/de/extio/lmlib/agent/responsehandler/CsvUnmarshallingAgentResponseHandlerTest.java b/src/test/java/de/extio/lmlib/agent/responsehandler/CsvUnmarshallingAgentResponseHandlerTest.java new file mode 100644 index 0000000..e3cf71b --- /dev/null +++ b/src/test/java/de/extio/lmlib/agent/responsehandler/CsvUnmarshallingAgentResponseHandlerTest.java @@ -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 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 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 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 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 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 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 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 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 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 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 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()); + } +}