feat: enhance LLM call control with AgentSkipCompletion enum and update related methods
This commit is contained in:
@@ -1069,7 +1069,13 @@ If `context.setStreaming(true)` is enabled and the handler implements `StreamedA
|
|||||||
|
|
||||||
### Skipping LLM Calls
|
### Skipping LLM Calls
|
||||||
|
|
||||||
Set `context.setSkipNextCompletion(true)` to turn the next step into a processing-only pass. The flag resets automatically after that step.
|
Use `context.setSkipNextCompletion(...)` to control LLM call behavior for the next step. The flag resets automatically after that step.
|
||||||
|
|
||||||
|
| Value | Behavior |
|
||||||
|
|-------|----------|
|
||||||
|
| `AgentSkipCompletion.NO_SKIP` | Executes the LLM call normally (default) |
|
||||||
|
| `AgentSkipCompletion.SETUP_CONVERSATION_ONLY` | Sets up the conversation but skips the LLM call |
|
||||||
|
| `AgentSkipCompletion.SKIP` | Skips the entire turn, including conversation setup |
|
||||||
|
|
||||||
### Cache Bypass Controls
|
### Cache Bypass Controls
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ public final class AgentContext {
|
|||||||
|
|
||||||
private volatile Exception errorException;
|
private volatile Exception errorException;
|
||||||
|
|
||||||
private volatile boolean skipNextCompletion;
|
private volatile AgentSkipCompletion skipNextCompletion;
|
||||||
|
|
||||||
private volatile boolean skipCache;
|
private volatile boolean skipCache;
|
||||||
|
|
||||||
@@ -235,11 +235,11 @@ public final class AgentContext {
|
|||||||
this.errorException = null;
|
this.errorException = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isSkipNextCompletion() {
|
public AgentSkipCompletion isSkipNextCompletion() {
|
||||||
return this.skipNextCompletion;
|
return this.skipNextCompletion;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSkipNextCompletion(final boolean skipNextCompletion) {
|
public void setSkipNextCompletion(final AgentSkipCompletion skipNextCompletion) {
|
||||||
this.skipNextCompletion = skipNextCompletion;
|
this.skipNextCompletion = skipNextCompletion;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
13
src/main/java/de/extio/lmlib/agent/AgentSkipCompletion.java
Normal file
13
src/main/java/de/extio/lmlib/agent/AgentSkipCompletion.java
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package de.extio.lmlib.agent;
|
||||||
|
|
||||||
|
public enum AgentSkipCompletion {
|
||||||
|
|
||||||
|
/** Execute the LLM call normally */
|
||||||
|
NO_SKIP,
|
||||||
|
|
||||||
|
/** Only set up the conversation without executing the LLM call */
|
||||||
|
SETUP_CONVERSATION_ONLY,
|
||||||
|
|
||||||
|
/** Skip the turn entirely */
|
||||||
|
SKIP
|
||||||
|
}
|
||||||
@@ -134,7 +134,7 @@ public interface BaseAgent {
|
|||||||
context.setError(AgentErrorType.GENERAL, ex);
|
context.setError(AgentErrorType.GENERAL, ex);
|
||||||
return List.of(context);
|
return List.of(context);
|
||||||
}
|
}
|
||||||
final boolean skipCompletion = context.isSkipNextCompletion();
|
final AgentSkipCompletion skipCompletion = context.isSkipNextCompletion();
|
||||||
|
|
||||||
splits.addAll(this.applyTemplate(context));
|
splits.addAll(this.applyTemplate(context));
|
||||||
final var tasks = new ArrayList<CompletableFuture<?>>(splits.size());
|
final var tasks = new ArrayList<CompletableFuture<?>>(splits.size());
|
||||||
@@ -142,9 +142,10 @@ public interface BaseAgent {
|
|||||||
tasks.add(CompletableFuture.runAsync(() -> {
|
tasks.add(CompletableFuture.runAsync(() -> {
|
||||||
try {
|
try {
|
||||||
split.context().clearError();
|
split.context().clearError();
|
||||||
final boolean skipCompletionInSplit = skipCompletion || this.agentType(split.context()) == AgentType.PROCESSING_ONLY;
|
final boolean skipCompletionInSplitEntirely = skipCompletion == AgentSkipCompletion.SKIP || this.agentType(split.context()) == AgentType.PROCESSING_ONLY;
|
||||||
if (skipCompletionInSplit) {
|
final boolean skipCompletionInSplitSetupConversation = skipCompletion == AgentSkipCompletion.SETUP_CONVERSATION_ONLY;
|
||||||
split.context().setSkipNextCompletion(false);
|
if (skipCompletionInSplitEntirely || skipCompletionInSplitSetupConversation) {
|
||||||
|
split.context().setSkipNextCompletion(AgentSkipCompletion.NO_SKIP);
|
||||||
split.context().getGraph().add("○");
|
split.context().getGraph().add("○");
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -153,133 +154,135 @@ public interface BaseAgent {
|
|||||||
}
|
}
|
||||||
split.context().getGraph().add(this.name());
|
split.context().getGraph().add(this.name());
|
||||||
|
|
||||||
if (!skipCompletionInSplit) {
|
if (!skipCompletionInSplitEntirely) {
|
||||||
final var completionClient = Objects.requireNonNull(client);
|
final var completionClient = Objects.requireNonNull(client);
|
||||||
final boolean skipCache = split.context().isSkipCache() || split.context().isAlwaysSkipCache();
|
final boolean skipCache = split.context().isSkipCache() || split.context().isAlwaysSkipCache();
|
||||||
split.context().setSkipCache(false);
|
split.context().setSkipCache(false);
|
||||||
final var conversation = this.setupConversation(split);
|
final var conversation = this.setupConversation(split);
|
||||||
final var requestedModelProfile = this.modelProfile(split.context());
|
final var requestedModelProfile = this.modelProfile(split.context());
|
||||||
final var supportsToolCalling = requestedModelProfile != null ? completionClient.supportsToolCalling(requestedModelProfile) : completionClient.supportsToolCalling(this.modelCategory(split.context()));
|
if (!skipCompletionInSplitSetupConversation) {
|
||||||
var toolCallData = supportsToolCalling ? this.toolCallData(split.context()) : null;
|
final var supportsToolCalling = requestedModelProfile != null ? completionClient.supportsToolCalling(requestedModelProfile) : completionClient.supportsToolCalling(this.modelCategory(split.context()));
|
||||||
this.rememberToolCallData(split.context(), toolCallData);
|
var toolCallData = supportsToolCalling ? this.toolCallData(split.context()) : null;
|
||||||
|
this.rememberToolCallData(split.context(), toolCallData);
|
||||||
|
|
||||||
boolean retryOuterLoop;
|
boolean retryOuterLoop;
|
||||||
boolean parseable = false;
|
boolean parseable = false;
|
||||||
Exception parseException = null;
|
Exception parseException = null;
|
||||||
int parseAttempts = 0;
|
int parseAttempts = 0;
|
||||||
int toolCallRounds = 0;
|
int toolCallRounds = 0;
|
||||||
do {
|
do {
|
||||||
retryOuterLoop = false;
|
retryOuterLoop = false;
|
||||||
while (parseAttempts < 2) {
|
while (parseAttempts < 2) {
|
||||||
LOGGER.debug("Conversation: {}", conversation);
|
LOGGER.debug("Conversation: {}", conversation);
|
||||||
|
|
||||||
final var modelProfile = this.modelProfile(split.context());
|
final var modelProfile = this.modelProfile(split.context());
|
||||||
final var responseHandler = this.responseHandler(split.context());
|
final var responseHandler = this.responseHandler(split.context());
|
||||||
long requestStart = 0;
|
long requestStart = 0;
|
||||||
Completion completion = null;
|
Completion completion = null;
|
||||||
if (split.context().isStreaming()) {
|
if (split.context().isStreaming()) {
|
||||||
if (responseHandler instanceof final StreamedAgentResponseHandler streamedResponseHandler) {
|
|
||||||
streamedResponseHandler.beforeStream(split.context());
|
|
||||||
}
|
|
||||||
final Consumer<Chunk> chunkConsumer = chunk -> {
|
|
||||||
if (responseHandler instanceof final StreamedAgentResponseHandler streamedResponseHandler) {
|
if (responseHandler instanceof final StreamedAgentResponseHandler streamedResponseHandler) {
|
||||||
if (streamedResponseHandler.handleChunk(chunk, split.context()) && split.context().getAgentContextUpdateConsumer() != null) {
|
streamedResponseHandler.beforeStream(split.context());
|
||||||
split.context().getAgentContextUpdateConsumer().accept(split.context());
|
}
|
||||||
}
|
final Consumer<Chunk> chunkConsumer = chunk -> {
|
||||||
|
if (responseHandler instanceof final StreamedAgentResponseHandler streamedResponseHandler) {
|
||||||
|
if (streamedResponseHandler.handleChunk(chunk, split.context()) && split.context().getAgentContextUpdateConsumer() != null) {
|
||||||
|
split.context().getAgentContextUpdateConsumer().accept(split.context());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (modelProfile != null) {
|
||||||
|
requestStart = System.currentTimeMillis();
|
||||||
|
completion = completionClient.streamConversation(modelProfile, conversation, chunkConsumer, toolCallData, skipCache);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
requestStart = System.currentTimeMillis();
|
||||||
|
completion = completionClient.streamConversation(this.modelCategory(split.context()), conversation, chunkConsumer, toolCallData, skipCache);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
if (modelProfile != null) {
|
|
||||||
requestStart = System.currentTimeMillis();
|
|
||||||
completion = completionClient.streamConversation(modelProfile, conversation, chunkConsumer, toolCallData, skipCache);
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
requestStart = System.currentTimeMillis();
|
if (modelProfile != null) {
|
||||||
completion = completionClient.streamConversation(this.modelCategory(split.context()), conversation, chunkConsumer, toolCallData, skipCache);
|
requestStart = System.currentTimeMillis();
|
||||||
|
completion = completionClient.conversation(modelProfile, conversation, toolCallData, skipCache);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
requestStart = System.currentTimeMillis();
|
||||||
|
completion = completionClient.conversation(this.modelCategory(split.context()), conversation, toolCallData, skipCache);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
final var durationMs = System.currentTimeMillis() - requestStart;
|
||||||
else {
|
split.context().getGraph().add("(" + durationMs + " ms)");
|
||||||
if (modelProfile != null) {
|
split.context().setLastCompletion(completion);
|
||||||
requestStart = System.currentTimeMillis();
|
split.context().getRequestStatistic().add(completion);
|
||||||
completion = completionClient.conversation(modelProfile, conversation, toolCallData, skipCache);
|
if (completion.finishReason() != null) {
|
||||||
|
switch (completion.finishReason()) {
|
||||||
|
case TOKEN_LIMIT_REACHED:
|
||||||
|
split.context().getGraph().add("⏹");
|
||||||
|
break;
|
||||||
|
case CONTENT_FILTERED:
|
||||||
|
split.context().getGraph().add("⛔");
|
||||||
|
break;
|
||||||
|
case TOOL_CALLS:
|
||||||
|
split.context().getGraph().add("🔧");
|
||||||
|
break;
|
||||||
|
case ERROR:
|
||||||
|
split.context().getGraph().add("❌");
|
||||||
|
continue;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
requestStart = System.currentTimeMillis();
|
|
||||||
completion = completionClient.conversation(this.modelCategory(split.context()), conversation, toolCallData, skipCache);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
final var durationMs = System.currentTimeMillis() - requestStart;
|
|
||||||
split.context().getGraph().add("(" + durationMs + " ms)");
|
|
||||||
split.context().setLastCompletion(completion);
|
|
||||||
split.context().getRequestStatistic().add(completion);
|
|
||||||
if (completion.finishReason() != null) {
|
|
||||||
switch (completion.finishReason()) {
|
|
||||||
case TOKEN_LIMIT_REACHED:
|
|
||||||
split.context().getGraph().add("⏹");
|
|
||||||
break;
|
|
||||||
case CONTENT_FILTERED:
|
|
||||||
split.context().getGraph().add("⛔");
|
|
||||||
break;
|
|
||||||
case TOOL_CALLS:
|
|
||||||
split.context().getGraph().add("🔧");
|
|
||||||
break;
|
|
||||||
case ERROR:
|
|
||||||
split.context().getGraph().add("❌");
|
|
||||||
continue;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!completion.toolCalls().isEmpty() && responseHandler instanceof final ToolCallingAgentResponseHandler toolCallingAgentResponseHandler) {
|
if (!completion.toolCalls().isEmpty() && responseHandler instanceof final ToolCallingAgentResponseHandler toolCallingAgentResponseHandler) {
|
||||||
if (toolCallRounds >= MAX_TOOL_CALL_ROUNDS) {
|
if (toolCallRounds >= MAX_TOOL_CALL_ROUNDS) {
|
||||||
parseException = new IllegalStateException("Too many tool call rounds");
|
parseException = new IllegalStateException("Too many tool call rounds");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
final var toolCallResults = new ToolCallingAgentResponseHandler.ToolCallResults();
|
||||||
|
if (toolCallingAgentResponseHandler.handleToolCalls(completion, split.context(), toolCallResults)) {
|
||||||
|
toolCallRounds++;
|
||||||
|
this.appendToolCallResults(conversation, completion, toolCallResults.results());
|
||||||
|
if (toolCallData != null && toolCallData.hasTools()) {
|
||||||
|
toolCallData = ToolCallData.auto(toolCallData.tools());
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!responseHandler.handle(completion, split.context())) {
|
||||||
|
split.context().getGraph().add("⚠");
|
||||||
|
LOGGER.warn("{} Cannot parse response: {}", this.name(), completion.response());
|
||||||
|
split.context().setSkipCache(true);
|
||||||
|
parseAttempts++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
parseable = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
final var toolCallResults = new ToolCallingAgentResponseHandler.ToolCallResults();
|
catch (final Exception e) {
|
||||||
if (toolCallingAgentResponseHandler.handleToolCalls(completion, split.context(), toolCallResults)) {
|
parseException = e;
|
||||||
toolCallRounds++;
|
|
||||||
this.appendToolCallResults(conversation, completion, toolCallResults.results());
|
|
||||||
if (toolCallData != null && toolCallData.hasTools()) {
|
|
||||||
toolCallData = ToolCallData.auto(toolCallData.tools());
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!responseHandler.handle(completion, split.context())) {
|
|
||||||
split.context().getGraph().add("⚠");
|
split.context().getGraph().add("⚠");
|
||||||
LOGGER.warn("{} Cannot parse response: {}", this.name(), completion.response());
|
LOGGER.warn("{} Cannot parse response: {}", this.name(), completion.response(), e);
|
||||||
split.context().setSkipCache(true);
|
split.context().setSkipCache(true);
|
||||||
parseAttempts++;
|
parseAttempts++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
parseable = true;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
catch (final Exception e) {
|
if (!parseable) {
|
||||||
parseException = e;
|
if (this.retryOnParsingError(split.context())) {
|
||||||
split.context().getGraph().add("⚠");
|
parseAttempts = 0;
|
||||||
LOGGER.warn("{} Cannot parse response: {}", this.name(), completion.response(), e);
|
toolCallRounds = 0;
|
||||||
split.context().setSkipCache(true);
|
retryOuterLoop = true;
|
||||||
parseAttempts++;
|
continue;
|
||||||
continue;
|
}
|
||||||
|
LOGGER.warn("{} Response is still not parseable after last attempt", this.name());
|
||||||
|
split.context().getGraph().add("☢");
|
||||||
|
split.context().setError(AgentErrorType.PARSING, parseException);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
} while (retryOuterLoop);
|
||||||
if (!parseable) {
|
}
|
||||||
if (this.retryOnParsingError(split.context())) {
|
|
||||||
parseAttempts = 0;
|
|
||||||
toolCallRounds = 0;
|
|
||||||
retryOuterLoop = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
LOGGER.warn("{} Response is still not parseable after last attempt", this.name());
|
|
||||||
split.context().getGraph().add("☢");
|
|
||||||
split.context().setError(AgentErrorType.PARSING, parseException);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} while (retryOuterLoop);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.postProcess(split.context());
|
this.postProcess(split.context());
|
||||||
@@ -465,8 +468,8 @@ public interface BaseAgent {
|
|||||||
return conversation;
|
return conversation;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void appendLastCompletionToConversation(final AgentContext context, final boolean skipCompletion) {
|
private void appendLastCompletionToConversation(final AgentContext context, final AgentSkipCompletion skipCompletion) {
|
||||||
if (skipCompletion || context.getConversation() == null || context.getLastCompletion() == null) {
|
if (context.getConversation() == null || context.getLastCompletion() == null || (skipCompletion != null && skipCompletion != AgentSkipCompletion.NO_SKIP)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final var currentAgent = context.getAgents().get(context.getCurrentAgentName());
|
final var currentAgent = context.getAgents().get(context.getCurrentAgentName());
|
||||||
@@ -496,7 +499,7 @@ public interface BaseAgent {
|
|||||||
context.setValue("_toolCallData", null);
|
context.setValue("_toolCallData", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void nextAgent(final AgentContext context, final boolean skipCompletion) {
|
private void nextAgent(final AgentContext context, final AgentSkipCompletion skipCompletion) {
|
||||||
context.setNextAgent(this.chooseNextAgent(context));
|
context.setNextAgent(this.chooseNextAgent(context));
|
||||||
|
|
||||||
if (context.getNextAgent() != null) {
|
if (context.getNextAgent() != null) {
|
||||||
|
|||||||
Reference in New Issue
Block a user