Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* Copyright 2024-2025 Embabel Software, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.embabel.agent.api.thinking;

import com.embabel.agent.api.common.PromptRunner;
import com.embabel.agent.api.common.thinking.ThinkingPromptRunnerOperations;
import com.embabel.common.core.thinking.ThinkingResponse;

/**
* Builder pattern to provide Java equivalent of Kotlin's withThinking() extension function.
* Solves the problem that Java cannot directly call Kotlin extension functions.
*
* <p>This builder enables Java developers to access thinking-aware prompt operations
* that extract LLM reasoning blocks alongside converted results.</p>
*
* <h2>Usage</h2>
*
* <pre>{@code
* // Java usage
* ThinkingPromptRunnerOperations thinkingOps = new ThinkingPromptRunnerBuilder(promptRunner)
* .withThinking();
*
* ThinkingResponse<Person> result = thinkingOps.createObject("analyze this", Person.class);
* Person person = result.getResult(); // The converted object
* List<ThinkingBlock> thinking = result.getThinkingBlocks(); // LLM reasoning blocks
* }</pre>
*
* <h2>Thinking Extraction</h2>
*
* <p>When supported by the underlying LLM operations (e.g., OperationContextPromptRunner
* with ChatClientLlmOperations), automatically extracts thinking content in various formats:</p>
*
* <ul>
* <li>Tagged thinking: {@code <think>reasoning here</think>}, {@code <analysis>content</analysis>}</li>
* <li>Prefix thinking: {@code //THINKING: reasoning here}</li>
* <li>Untagged thinking: raw text content before JSON objects</li>
* </ul>
*
* <p>For implementations that don't support thinking extraction, returns graceful degradation
* with empty thinking blocks while preserving the original response content.</p>
*
* @see ThinkingPromptRunnerOperations for available thinking-aware methods
* @see ThinkingResponse for response structure
* @see com.embabel.common.core.thinking.ThinkingBlock for thinking content details
*/
public record ThinkingPromptRunnerBuilder(PromptRunner runner) {

/**
* Java equivalent of Kotlin's withThinking() extension function.
*
* <p>Creates thinking-aware prompt operations that return both converted results
* and the reasoning content that LLMs generate during processing.</p>
*
* <p><strong>Implementation Behavior:</strong></p>
*
* <ul>
* <li><strong>OperationContextPromptRunner (Production):</strong> Real thinking extraction
* using ChatClientLlmOperations with populated thinking blocks</li>
* <li><strong>Other Implementations (Graceful Degradation):</strong> Returns wrapper with
* empty thinking blocks while preserving original response content</li>
* </ul>
*
* @return Enhanced prompt runner operations with thinking block extraction capabilities
* @throws UnsupportedOperationException if the underlying LLM operations don't support
* thinking extraction and require ChatClientLlmOperations (only for specific implementations)
*
*/
public ThinkingPromptRunnerOperations withThinking() {
return runner.withThinking();
}

/**
* Factory method for creating a builder instance.
*
* <p>Provides a fluent interface for Java developers who prefer static factory methods:</p>
*
* <pre>{@code
* ThinkingPromptRunnerOperations thinkingOps = ThinkingPromptRunnerBuilder
* .from(promptRunner)
* .withThinking();
* }</pre>
*
* @param runner The prompt runner operations to enhance with thinking capabilities
* @return A new ThinkingPromptRunnerBuilder instance
*/
public static ThinkingPromptRunnerBuilder from(PromptRunner runner) {
return new ThinkingPromptRunnerBuilder(runner);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package com.embabel.agent.api.common
import com.embabel.agent.api.annotation.support.AgenticInfo
import com.embabel.agent.api.common.nested.ObjectCreator
import com.embabel.agent.api.common.nested.TemplateOperations
import com.embabel.agent.api.common.thinking.ThinkingPromptRunnerOperations
import com.embabel.agent.api.tool.Tool
import com.embabel.agent.core.Agent
import com.embabel.agent.core.AgentPlatform
Expand All @@ -27,6 +28,7 @@ import com.embabel.agent.spi.LlmUse
import com.embabel.chat.AssistantMessage
import com.embabel.chat.Message
import com.embabel.common.ai.model.LlmOptions
import com.embabel.common.ai.model.Thinking
import com.embabel.common.ai.prompt.PromptContributor
import com.embabel.common.ai.prompt.PromptElement
import com.embabel.common.core.streaming.StreamingCapability
Expand Down Expand Up @@ -389,6 +391,55 @@ interface PromptRunner : LlmUse, PromptRunnerOperations {
)
}

/**
* Check if thinking extraction capabilities are supported by the underlying implementation.
*
* Thinking capabilities allow extraction of thinking blocks (like `<think>...</think>`)
* from LLM responses and provide access to both the result and the extracted thinking content.
* Always check this before calling thinking() to avoid exceptions.
*
* Note: Thinking and streaming capabilities are mutually exclusive.
*
* @return true if thinking extraction is supported, false if thinking is not available
*/
fun supportsThinking(): Boolean = false


/**
* Create a thinking-enhanced version of this prompt runner.
*
* Returns a PromptRunner where all operations (createObject, generateText, etc.)
* return ThinkingResponse<T> wrappers that include both results and extracted
* thinking blocks from the LLM response.
*
* Always check supportsThinking() first and ensure LlmOptions includes thinking configuration
* via withLlm(LlmOptions.withThinking(Thinking.withExtraction())).
*
* Note: Thinking and streaming capabilities are mutually exclusive.
*
* @return ThinkingCapability instance providing access to thinking-aware operations
* @throws UnsupportedOperationException if thinking is not supported by this implementation
* @throws IllegalArgumentException if thinking is not enabled in LlmOptions configuration
*/
fun withThinking(): ThinkingPromptRunnerOperations {
if (!supportsThinking()) {
throw UnsupportedOperationException(
"Thinking not supported by this PromptRunner implementation. " +
"Check supportsThinking() before calling withThinking()."
)
}

val thinking = llm?.thinking
require(thinking != null && thinking != Thinking.NONE) {
"Thinking capability requires thinking to be enabled in LlmOptions. " +
"Use withLlm(LlmOptions.withThinking(Thinking.withExtraction()))"
}

// For implementations that support thinking but haven't overridden withThinking(),
// they should provide their own implementation
error("Implementation error: supportsThinking() returned true but withThinking() not overridden")
}

override fun respond(
messages: List<Message>,
): AssistantMessage =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import com.embabel.agent.api.common.streaming.StreamingPromptRunner
import com.embabel.agent.api.common.streaming.StreamingPromptRunnerOperations
import com.embabel.agent.api.common.support.streaming.StreamingCapabilityDetector
import com.embabel.agent.api.common.support.streaming.StreamingPromptRunnerOperationsImpl
import com.embabel.agent.api.common.thinking.ThinkingPromptRunnerOperations
import com.embabel.agent.api.common.thinking.support.ThinkingPromptRunnerOperationsImpl
import com.embabel.agent.api.tool.Tool
import com.embabel.agent.core.ProcessOptions
import com.embabel.agent.core.ToolGroup
Expand All @@ -41,6 +43,7 @@ import com.embabel.chat.ImagePart
import com.embabel.chat.Message
import com.embabel.chat.UserMessage
import com.embabel.common.ai.model.LlmOptions
import com.embabel.common.ai.model.Thinking
import com.embabel.common.ai.prompt.PromptContributor
import com.embabel.common.core.types.ZeroToOne
import com.embabel.common.util.loggerFor
Expand Down Expand Up @@ -335,4 +338,48 @@ internal data class OperationContextPromptRunner(
action = action,
)
}

/**
* Create thinking-aware prompt operations that extract LLM reasoning blocks.
*
* This method creates ThinkingPromptRunnerOperations that can capture both the
* converted results and the reasoning content that LLMs generate during processing.
*
* @return ThinkingPromptRunnerOperations for executing prompts with thinking extraction
* @throws UnsupportedOperationException if the underlying LLM operations don't support thinking extraction
*/
override fun supportsThinking(): Boolean = true

override fun withThinking(): ThinkingPromptRunnerOperations {
val llmOperations = context.agentPlatform().platformServices.llmOperations

if (llmOperations !is ChatClientLlmOperations) {9
throw UnsupportedOperationException(
"Thinking extraction not supported by underlying LLM operations. " +
"Operations type: ${llmOperations::class.simpleName}. " +
"Thinking extraction requires ChatClientLlmOperations."
)
}
11
// Auto-enable thinking extraction when withThinking() is called
val thinkingEnabledLlm = llm.withThinking(Thinking.withExtraction())

return ThinkingPromptRunnerOperationsImpl(
chatClientOperations = llmOperations,
interaction = LlmInteraction(
llm = thinkingEnabledLlm,
toolGroups = toolGroups,
toolCallbacks = safelyGetToolCallbacks(toolObjects) + otherToolCallbacks,
promptContributors = promptContributors + contextualPromptContributors.map {
it.toPromptContributor(context)
},
id = interactionId ?: InteractionId("${context.operation.name}-thinking"),
generateExamples = generateExamples,
propertyFilter = propertyFilter,
),
messages = messages,
agentProcess = context.processContext.agentProcess,
action = action,
)
}
}
Loading
Loading