feat: Implement reasoning streaming and chunk handling in Csv and Json agent response handlers

This commit is contained in:
extio-de
2026-03-31 22:36:15 +02:00
parent 00a223bd64
commit 438197403c
5 changed files with 182 additions and 4 deletions

View File

@@ -2,17 +2,20 @@ package de.extio.lmlib.agent.responsehandler;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.regex.Pattern;
import java.util.function.BiConsumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import de.extio.lmlib.agent.AgentContext;
import de.extio.lmlib.client.Chunk;
import de.extio.lmlib.client.Completion;
import de.extio.lmlib.client.Conversation;
import de.extio.lmlib.util.TextUtils;
public class CsvAgentResponseHandler implements AgentResponseHandler {
public class CsvAgentResponseHandler implements StreamedAgentResponseHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(CsvAgentResponseHandler.class);
@@ -25,12 +28,20 @@ public class CsvAgentResponseHandler implements AgentResponseHandler {
private final String fieldSeparatorQuoted;
private final String errorPromptSuffix;
private final String reasoningKey;
private final BiConsumer<AgentContext, Chunk> afterChunkUpdate;
public CsvAgentResponseHandler(final List<String> headings, final String keyPrefix) {
this(headings, keyPrefix, ";");
}
public CsvAgentResponseHandler(final List<String> headings, final String keyPrefix, final String fieldSeparator) {
this(headings, keyPrefix, fieldSeparator, null);
}
public CsvAgentResponseHandler(final List<String> headings, final String keyPrefix, final String fieldSeparator, final BiConsumer<AgentContext, Chunk> afterChunkUpdate) {
if (headings == null || headings.isEmpty()) {
throw new IllegalArgumentException("Headings list cannot be null or empty");
}
@@ -38,6 +49,8 @@ public class CsvAgentResponseHandler implements AgentResponseHandler {
this.keyPrefix = keyPrefix != null ? keyPrefix : "";
this.fieldSeparator = fieldSeparator;
this.fieldSeparatorQuoted = Pattern.quote(fieldSeparator);
this.reasoningKey = this.keyPrefix + "_reasoning";
this.afterChunkUpdate = afterChunkUpdate;
this.errorPromptSuffix = "\n\nThe previous response could not be fully processed or validated. " +
"Please format the response as CSV with '" + fieldSeparator + "' as field separator.\n" +
@@ -68,6 +81,25 @@ public class CsvAgentResponseHandler implements AgentResponseHandler {
return false;
}
}
@Override
public void beforeStream(final AgentContext context) {
context.getContext().remove(this.reasoningKey);
}
@Override
public boolean handleChunk(final Chunk chunk, final AgentContext context) {
boolean handled = false;
if (chunk.reasoningContent() != null) {
context.setStringValue(this.reasoningKey, Objects.requireNonNullElseGet(context.getStringValue(this.reasoningKey), () -> "").concat(chunk.reasoningContent()));
handled = true;
}
if (this.afterChunkUpdate != null) {
this.afterChunkUpdate.accept(context, chunk);
return true;
}
return handled;
}
private boolean parseCsv(final String csvText, final AgentContext context) {
final String[] lines = TextUtils.normalizeModelResponse(csvText.replace("\\r", "\r").replace("\\n", "\n"), false).split("\n");

View File

@@ -6,6 +6,7 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.BiConsumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -17,23 +18,34 @@ import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.json.JsonMapper;
import de.extio.lmlib.agent.AgentContext;
import de.extio.lmlib.client.Chunk;
import de.extio.lmlib.client.Completion;
import de.extio.lmlib.client.Conversation;
public class JsonAgentResponseHandler implements AgentResponseHandler {
public class JsonAgentResponseHandler implements StreamedAgentResponseHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(JsonAgentResponseHandler.class);
private final ObjectMapper objectMapper;
private final String prefixFields;
private final String reasoningKey;
private final BiConsumer<AgentContext, Chunk> afterChunkUpdate;
public JsonAgentResponseHandler() {
this(null);
this(null, null);
}
public JsonAgentResponseHandler(final String prefixFields) {
this(prefixFields, null);
}
public JsonAgentResponseHandler(final String prefixFields, final BiConsumer<AgentContext, Chunk> afterChunkUpdate) {
this.prefixFields = prefixFields == null ? "" : prefixFields;
this.reasoningKey = this.prefixFields + "_reasoning";
this.afterChunkUpdate = afterChunkUpdate;
this.objectMapper = JsonMapper.builder()
.configure(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES, false)
@@ -55,6 +67,26 @@ public class JsonAgentResponseHandler implements AgentResponseHandler {
.enable(JsonReadFeature.ALLOW_UNQUOTED_PROPERTY_NAMES)
.build();
}
@Override
public void beforeStream(final AgentContext context) {
context.getContext().remove(this.reasoningKey);
}
@Override
public boolean handleChunk(final Chunk chunk, final AgentContext context) {
boolean handled = false;
if (chunk.reasoningContent() != null) {
final var existingReasoning = context.getStringValue(this.reasoningKey);
context.setStringValue(this.reasoningKey, (existingReasoning == null ? "" : existingReasoning).concat(chunk.reasoningContent()));
handled = true;
}
if (this.afterChunkUpdate != null) {
this.afterChunkUpdate.accept(context, chunk);
return true;
}
return handled;
}
@Override
public boolean handle(final Completion completion, final AgentContext context) {

View File

@@ -4,11 +4,12 @@ import java.util.ArrayList;
import java.util.List;
import de.extio.lmlib.agent.AgentContext;
import de.extio.lmlib.client.Chunk;
import de.extio.lmlib.client.Completion;
import de.extio.lmlib.client.ToolCall;
import de.extio.lmlib.client.ToolParameters;
public class ToolCallingAgentResponseHandler implements AgentResponseHandler {
public class ToolCallingAgentResponseHandler implements StreamedAgentResponseHandler {
@FunctionalInterface
public interface ToolCallHandler {
@@ -58,6 +59,21 @@ public class ToolCallingAgentResponseHandler implements AgentResponseHandler {
return this.delegate == null || this.delegate.handle(completion, context);
}
@Override
public void beforeStream(final AgentContext context) {
if (this.delegate instanceof final StreamedAgentResponseHandler streamedAgentResponseHandler) {
streamedAgentResponseHandler.beforeStream(context);
}
}
@Override
public boolean handleChunk(final Chunk chunk, final AgentContext context) {
if (this.delegate instanceof final StreamedAgentResponseHandler streamedAgentResponseHandler) {
return streamedAgentResponseHandler.handleChunk(chunk, context);
}
return true;
}
public boolean handleToolCalls(final Completion completion, final AgentContext context, final ToolCallResults toolCallResults) {
return this.toolCallHandler.handle(completion, context, toolCallResults);
}