diff --git a/README.md b/README.md index 5c57280..ab9ea83 100644 --- a/README.md +++ b/README.md @@ -1069,7 +1069,13 @@ If `context.setStreaming(true)` is enabled and the handler implements `StreamedA ### 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 diff --git a/src/main/java/de/extio/lmlib/agent/AgentContext.java b/src/main/java/de/extio/lmlib/agent/AgentContext.java index e91a5de..d24ba82 100755 --- a/src/main/java/de/extio/lmlib/agent/AgentContext.java +++ b/src/main/java/de/extio/lmlib/agent/AgentContext.java @@ -27,7 +27,7 @@ public final class AgentContext { private volatile Exception errorException; - private volatile boolean skipNextCompletion; + private volatile AgentSkipCompletion skipNextCompletion; private volatile boolean skipCache; @@ -235,11 +235,11 @@ public final class AgentContext { this.errorException = null; } - public boolean isSkipNextCompletion() { + public AgentSkipCompletion isSkipNextCompletion() { return this.skipNextCompletion; } - public void setSkipNextCompletion(final boolean skipNextCompletion) { + public void setSkipNextCompletion(final AgentSkipCompletion skipNextCompletion) { this.skipNextCompletion = skipNextCompletion; } diff --git a/src/main/java/de/extio/lmlib/agent/AgentSkipCompletion.java b/src/main/java/de/extio/lmlib/agent/AgentSkipCompletion.java new file mode 100644 index 0000000..5358bad --- /dev/null +++ b/src/main/java/de/extio/lmlib/agent/AgentSkipCompletion.java @@ -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 +} diff --git a/src/main/java/de/extio/lmlib/agent/BaseAgent.java b/src/main/java/de/extio/lmlib/agent/BaseAgent.java index 394687b..dd1cbd0 100644 --- a/src/main/java/de/extio/lmlib/agent/BaseAgent.java +++ b/src/main/java/de/extio/lmlib/agent/BaseAgent.java @@ -134,7 +134,7 @@ public interface BaseAgent { context.setError(AgentErrorType.GENERAL, ex); return List.of(context); } - final boolean skipCompletion = context.isSkipNextCompletion(); + final AgentSkipCompletion skipCompletion = context.isSkipNextCompletion(); splits.addAll(this.applyTemplate(context)); final var tasks = new ArrayList>(splits.size()); @@ -142,9 +142,10 @@ public interface BaseAgent { tasks.add(CompletableFuture.runAsync(() -> { try { split.context().clearError(); - final boolean skipCompletionInSplit = skipCompletion || this.agentType(split.context()) == AgentType.PROCESSING_ONLY; - if (skipCompletionInSplit) { - split.context().setSkipNextCompletion(false); + final boolean skipCompletionInSplitEntirely = skipCompletion == AgentSkipCompletion.SKIP || this.agentType(split.context()) == AgentType.PROCESSING_ONLY; + final boolean skipCompletionInSplitSetupConversation = skipCompletion == AgentSkipCompletion.SETUP_CONVERSATION_ONLY; + if (skipCompletionInSplitEntirely || skipCompletionInSplitSetupConversation) { + split.context().setSkipNextCompletion(AgentSkipCompletion.NO_SKIP); split.context().getGraph().add("○"); } else { @@ -153,133 +154,135 @@ public interface BaseAgent { } split.context().getGraph().add(this.name()); - if (!skipCompletionInSplit) { + if (!skipCompletionInSplitEntirely) { final var completionClient = Objects.requireNonNull(client); final boolean skipCache = split.context().isSkipCache() || split.context().isAlwaysSkipCache(); split.context().setSkipCache(false); final var conversation = this.setupConversation(split); final var requestedModelProfile = this.modelProfile(split.context()); - final var supportsToolCalling = requestedModelProfile != null ? completionClient.supportsToolCalling(requestedModelProfile) : completionClient.supportsToolCalling(this.modelCategory(split.context())); - var toolCallData = supportsToolCalling ? this.toolCallData(split.context()) : null; - this.rememberToolCallData(split.context(), toolCallData); - - boolean retryOuterLoop; - boolean parseable = false; - Exception parseException = null; - int parseAttempts = 0; - int toolCallRounds = 0; - do { - retryOuterLoop = false; - while (parseAttempts < 2) { - LOGGER.debug("Conversation: {}", conversation); - - final var modelProfile = this.modelProfile(split.context()); - final var responseHandler = this.responseHandler(split.context()); - long requestStart = 0; - Completion completion = null; - if (split.context().isStreaming()) { - if (responseHandler instanceof final StreamedAgentResponseHandler streamedResponseHandler) { - streamedResponseHandler.beforeStream(split.context()); - } - final Consumer chunkConsumer = chunk -> { + if (!skipCompletionInSplitSetupConversation) { + final var supportsToolCalling = requestedModelProfile != null ? completionClient.supportsToolCalling(requestedModelProfile) : completionClient.supportsToolCalling(this.modelCategory(split.context())); + var toolCallData = supportsToolCalling ? this.toolCallData(split.context()) : null; + this.rememberToolCallData(split.context(), toolCallData); + + boolean retryOuterLoop; + boolean parseable = false; + Exception parseException = null; + int parseAttempts = 0; + int toolCallRounds = 0; + do { + retryOuterLoop = false; + while (parseAttempts < 2) { + LOGGER.debug("Conversation: {}", conversation); + + final var modelProfile = this.modelProfile(split.context()); + final var responseHandler = this.responseHandler(split.context()); + long requestStart = 0; + Completion completion = null; + if (split.context().isStreaming()) { if (responseHandler instanceof final StreamedAgentResponseHandler streamedResponseHandler) { - if (streamedResponseHandler.handleChunk(chunk, split.context()) && split.context().getAgentContextUpdateConsumer() != null) { - split.context().getAgentContextUpdateConsumer().accept(split.context()); - } + streamedResponseHandler.beforeStream(split.context()); + } + final Consumer 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 { - requestStart = System.currentTimeMillis(); - completion = completionClient.streamConversation(this.modelCategory(split.context()), conversation, chunkConsumer, toolCallData, skipCache); + if (modelProfile != null) { + requestStart = System.currentTimeMillis(); + completion = completionClient.conversation(modelProfile, conversation, toolCallData, skipCache); + } + else { + requestStart = System.currentTimeMillis(); + completion = completionClient.conversation(this.modelCategory(split.context()), conversation, toolCallData, skipCache); + } } - } - else { - if (modelProfile != null) { - requestStart = System.currentTimeMillis(); - completion = completionClient.conversation(modelProfile, 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; + } } - 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("⏹"); + + if (!completion.toolCalls().isEmpty() && responseHandler instanceof final ToolCallingAgentResponseHandler toolCallingAgentResponseHandler) { + if (toolCallRounds >= MAX_TOOL_CALL_ROUNDS) { + parseException = new IllegalStateException("Too many tool call rounds"); break; - case CONTENT_FILTERED: - split.context().getGraph().add("⛔"); - break; - case TOOL_CALLS: - split.context().getGraph().add("🔧"); - break; - case ERROR: - split.context().getGraph().add("❌"); + } + 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; - default: - break; + } } - } - - if (!completion.toolCalls().isEmpty() && responseHandler instanceof final ToolCallingAgentResponseHandler toolCallingAgentResponseHandler) { - if (toolCallRounds >= MAX_TOOL_CALL_ROUNDS) { - parseException = new IllegalStateException("Too many tool call rounds"); + + 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; } - 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())) { + catch (final Exception e) { + parseException = e; 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); parseAttempts++; continue; } - - parseable = true; - break; } - catch (final Exception e) { - parseException = e; - split.context().getGraph().add("⚠"); - LOGGER.warn("{} Cannot parse response: {}", this.name(), completion.response(), e); - split.context().setSkipCache(true); - parseAttempts++; - continue; + 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; } - } - 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); + } while (retryOuterLoop); + } } this.postProcess(split.context()); @@ -465,8 +468,8 @@ public interface BaseAgent { return conversation; } - private void appendLastCompletionToConversation(final AgentContext context, final boolean skipCompletion) { - if (skipCompletion || context.getConversation() == null || context.getLastCompletion() == null) { + private void appendLastCompletionToConversation(final AgentContext context, final AgentSkipCompletion skipCompletion) { + if (context.getConversation() == null || context.getLastCompletion() == null || (skipCompletion != null && skipCompletion != AgentSkipCompletion.NO_SKIP)) { return; } final var currentAgent = context.getAgents().get(context.getCurrentAgentName()); @@ -496,7 +499,7 @@ public interface BaseAgent { 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)); if (context.getNextAgent() != null) {