feat: Introduce reasoning capabilities in chat and text completion

- Added AbstractCompletionRequest and AbstractCompletionResponse classes to standardize request and response structures.
- Implemented StreamOptions for streaming responses in chat completions.
- Enhanced ChatCompletionClient and TextCompletionClient to support reasoning content in responses.
- Updated ModelProfile to include reasoning effort and summary details.
- Created GptOssPromptStrategy to handle specific prompt formatting and reasoning extraction.
- Modified existing tests to validate reasoning content in completions.
- Updated configuration properties for reasoning settings in local properties files.
This commit is contained in:
extio-de
2025-10-01 19:05:15 +02:00
parent 34c96f0147
commit 514dad1c7f
32 changed files with 582 additions and 392 deletions

View File

@@ -10,10 +10,11 @@ import java.util.Map.Entry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.json.JsonReadFeature;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.json.JsonMapper;
import de.extio.lmlib.agent.AgentContext;
import de.extio.lmlib.client.Completion;
@@ -34,24 +35,25 @@ public class JsonAgentResponseHandler implements AgentResponseHandler {
public JsonAgentResponseHandler(final String prefixFields) {
this.prefixFields = prefixFields == null ? "" : prefixFields;
this.objectMapper = new ObjectMapper();
this.objectMapper.configure(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES, false);
this.objectMapper.configure(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE, false);
this.objectMapper.configure(DeserializationFeature.FAIL_ON_MISSING_EXTERNAL_TYPE_ID_PROPERTY, false);
this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNRESOLVED_OBJECT_IDS, false);
this.objectMapper.configure(JsonParser.Feature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER, true);
this.objectMapper.configure(JsonParser.Feature.ALLOW_COMMENTS, true);
this.objectMapper.configure(JsonParser.Feature.ALLOW_MISSING_VALUES, true);
this.objectMapper.configure(JsonParser.Feature.ALLOW_NON_NUMERIC_NUMBERS, true);
this.objectMapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);
this.objectMapper.configure(JsonParser.Feature.ALLOW_TRAILING_COMMA, true);
this.objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS, true);
this.objectMapper.configure(JsonParser.Feature.ALLOW_YAML_COMMENTS, true);
this.objectMapper.configure(JsonParser.Feature.ALLOW_LEADING_DECIMAL_POINT_FOR_NUMBERS, true);
this.objectMapper.configure(JsonParser.Feature.ALLOW_LEADING_PLUS_SIGN_FOR_NUMBERS, true);
this.objectMapper.configure(JsonParser.Feature.ALLOW_TRAILING_DECIMAL_POINT_FOR_NUMBERS, true);
this.objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
this.objectMapper = JsonMapper.builder()
.configure(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES, false)
.configure(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE, false)
.configure(DeserializationFeature.FAIL_ON_MISSING_EXTERNAL_TYPE_ID_PROPERTY, false)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.configure(DeserializationFeature.FAIL_ON_UNRESOLVED_OBJECT_IDS, false)
.enable(JsonReadFeature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER)
.enable(JsonReadFeature.ALLOW_JAVA_COMMENTS)
.enable(JsonReadFeature.ALLOW_MISSING_VALUES)
.enable(JsonReadFeature.ALLOW_NON_NUMERIC_NUMBERS)
.enable(JsonReadFeature.ALLOW_SINGLE_QUOTES)
.enable(JsonReadFeature.ALLOW_TRAILING_COMMA)
.enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS)
.enable(JsonReadFeature.ALLOW_YAML_COMMENTS)
.enable(JsonReadFeature.ALLOW_LEADING_DECIMAL_POINT_FOR_NUMBERS)
.enable(JsonReadFeature.ALLOW_LEADING_PLUS_SIGN_FOR_NUMBERS)
.enable(JsonReadFeature.ALLOW_TRAILING_DECIMAL_POINT_FOR_NUMBERS)
.enable(JsonReadFeature.ALLOW_UNQUOTED_FIELD_NAMES)
.build();
}
@Override

View File

@@ -1,11 +1,12 @@
package de.extio.lmlib.agent.responsehandler;
import de.extio.lmlib.agent.AgentContext;
import de.extio.lmlib.client.Chunk;
public interface StreamedAgentResponseHandler extends AgentResponseHandler {
void beforeStream(AgentContext context);
boolean handleChunk(String chunk, AgentContext context);
boolean handleChunk(Chunk chunk, AgentContext context);
}

View File

@@ -1,6 +1,7 @@
package de.extio.lmlib.agent.responsehandler;
import java.util.Objects;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
@@ -8,33 +9,32 @@ 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;
public class TextAgentResponseHandler implements StreamedAgentResponseHandler {
public static final String UPDATE_KEY = "chunkUpdateKey";
private static final Logger LOGGER = LoggerFactory.getLogger(TextAgentResponseHandler.class);
private final String key;
private final String chunkKey;
private final String reasoningKey;
private final Function<String, String> transformer;
private final Consumer<AgentContext> beforeStream;
private final Function<String, String> chunkTransformer;
private final Consumer<AgentContext> afterChunkUpdate;
private final Function<Chunk, Chunk> chunkTransformer;
private final BiConsumer<AgentContext, Chunk> afterChunkUpdate;
public TextAgentResponseHandler(final String key) {
this(key, null, null, null, null);
}
public TextAgentResponseHandler(final String key, final Function<String, String> transformer, final Consumer<AgentContext> beforeStream, final Function<String, String> chunkTransformer, final Consumer<AgentContext> afterChunkUpdate) {
public TextAgentResponseHandler(final String key, final Function<String, String> transformer, final Consumer<AgentContext> beforeStream, final Function<Chunk, Chunk> chunkTransformer, final BiConsumer<AgentContext, Chunk> afterChunkUpdate) {
this.key = key;
this.chunkKey = key.concat("_chunk");
this.reasoningKey = key.concat("_reasoning");
this.transformer = transformer;
this.beforeStream = beforeStream;
this.chunkTransformer = chunkTransformer;
@@ -44,7 +44,14 @@ public class TextAgentResponseHandler implements StreamedAgentResponseHandler {
@Override
public boolean handle(final Completion completion, final AgentContext context) {
var response = completion.response();
LOGGER.debug("Response: {}", response);
final var reasoning = completion.reasoning();
if (reasoning != null && !reasoning.isBlank()) {
context.setStringValue(this.reasoningKey, reasoning);
LOGGER.debug("Reasoning: {}; Response: {}", reasoning, response);
}
else {
LOGGER.debug("Response: {}", response);
}
if (this.transformer != null) {
response = this.transformer.apply(response);
}
@@ -55,24 +62,27 @@ public class TextAgentResponseHandler implements StreamedAgentResponseHandler {
@Override
public void beforeStream(final AgentContext context) {
context.setStringValue(this.key, "");
context.getContext().remove(this.reasoningKey);
if (this.beforeStream != null) {
this.beforeStream.accept(context);
}
}
@Override
public boolean handleChunk(final String chunk_, final AgentContext context) {
public boolean handleChunk(final Chunk chunk_, final AgentContext context) {
var chunk = chunk_;
if (this.chunkTransformer != null) {
chunk = this.chunkTransformer.apply(chunk);
}
context.setStringValue(UPDATE_KEY, this.key);
if (afterChunkUpdate != null) {
context.setStringValue(this.chunkKey, chunk);
context.setStringValue(this.key, Objects.requireNonNullElse(context.getStringValue(this.key), "").concat(chunk));
afterChunkUpdate.accept(context);
context.getContext().remove(UPDATE_KEY);
context.getContext().remove(this.chunkKey);
if (chunk.content() != null) {
context.setStringValue(this.key, Objects.requireNonNullElseGet(context.getStringValue(this.key), () -> "").concat(chunk.content()));
}
if (chunk.reasoningContent() != null) {
context.setStringValue(this.reasoningKey, Objects.requireNonNullElseGet(context.getStringValue(this.reasoningKey), () -> "").concat(chunk.reasoningContent()));
}
afterChunkUpdate.accept(context, chunk);
}
return true;
}