Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 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.PromptRunnerOperations;
import com.embabel.agent.api.common.thinking.ThinkingExtensionsKt;
import com.embabel.agent.api.common.thinking.ThinkingPromptRunnerOperations;

/**
* 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();
*
* ChatResponseWithThinking<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 com.embabel.chat.ChatResponseWithThinking for response structure
* @see com.embabel.common.core.thinking.ThinkingBlock for thinking content details
*/
public record ThinkingPromptRunnerBuilder(PromptRunnerOperations 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)
* @see ThinkingExtensionsKt#withThinking(PromptRunnerOperations) for Kotlin equivalent
*/
public ThinkingPromptRunnerOperations withThinking() {
return ThinkingExtensionsKt.withThinking(runner);
}

/**
* 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(PromptRunnerOperations runner) {
return new ThinkingPromptRunnerBuilder(runner);
}
}
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,46 @@ 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
*/
internal fun withThinking(): ThinkingPromptRunnerOperations {
val llmOperations = context.agentPlatform().platformServices.llmOperations

if (llmOperations !is ChatClientLlmOperations) {
throw UnsupportedOperationException(
"Thinking extraction not supported by underlying LLM operations. " +
"Operations type: ${llmOperations::class.simpleName}. " +
"Thinking extraction requires ChatClientLlmOperations."
)
}

// 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,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* 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.common.thinking

import com.embabel.agent.api.common.PromptRunnerOperations
import com.embabel.agent.api.common.support.OperationContextPromptRunner

/**
* Enhance a prompt runner with thinking block extraction capabilities.
*
* This extension method provides access to thinking-aware prompt operations
* that return both converted results and the reasoning content that LLMs
* generate during their processing.
*
* ## Usage
*
* ```kotlin
* val result = promptRunner.withThinking().createObject("analyze this", Person::class.java)
* val person = result.result // The converted Person object
* val thinking = result.thinkingBlocks // List of reasoning blocks
* ```
*
* ## Implementation Behavior
*
* ### OperationContextPromptRunner (Production)
* - **Real thinking extraction**: Uses ChatClientLlmOperations to extract thinking blocks
* - **Full functionality**: Returns actual thinking content from LLM responses
* - **Return type**: `ChatResponseWithThinking<T>` with populated thinking blocks
*
* ### StreamingPromptRunner (Graceful Degradation)
* - **Empty thinking blocks**: Returns wrapper with `thinkingBlocks = emptyList()`
* - **Preserved results**: Original response content is maintained
* - **Return type**: `ChatResponseWithThinking<T>` with empty thinking blocks
* - **Correct alternative**: Use `StreamingPromptRunnerOperations.createObjectStreamWithThinking()`
* which returns `Flux<StreamingEvent<T>>` including thinking events
*
* ### Other Implementations (Fallback)
* - **Empty thinking blocks**: Returns wrapper with `thinkingBlocks = emptyList()`
* - **Preserved results**: Original response content is maintained
* - **Use cases**: Testing (FakePromptRunner), custom implementations
*
* ## Thinking Extraction Formats
*
* When supported, automatically extracts thinking content in various formats:
* - Tagged thinking: `<think>reasoning here</think>`, `<analysis>content</analysis>`
* - Prefix thinking: `//THINKING: reasoning here`
* - Untagged thinking: raw text content before JSON objects
*
* ## Performance
*
* The thinking extraction process adds minimal overhead as it reuses existing
* content parsing logic and only activates when thinking blocks are detected.
*
* @return Enhanced prompt runner operations with thinking block extraction.
* For OperationContextPromptRunner: real thinking extraction.
* For other implementations: graceful degradation with empty thinking blocks.
*
* @see ThinkingPromptRunnerOperations for available thinking-aware methods
* @see com.embabel.chat.ChatResponseWithThinking for response structure
* @see com.embabel.common.core.thinking.ThinkingBlock for thinking content details
* @see com.embabel.agent.api.common.streaming.StreamingPromptRunnerOperations.createObjectStreamWithThinking for streaming alternative
*/
fun PromptRunnerOperations.withThinking(): ThinkingPromptRunnerOperations {
if (this is OperationContextPromptRunner) {
return this.withThinking()
}

// For other PromptRunnerOperations implementations, fall back to wrapper
return ThinkingPromptRunner(this)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* 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.common.thinking

import com.embabel.agent.api.common.PromptRunnerOperations
import com.embabel.chat.AssistantMessage
import com.embabel.chat.ChatResponseWithThinking
import com.embabel.chat.Message
import com.embabel.common.core.types.ZeroToOne

/**
* Fallback wrapper implementation for thinking-aware prompt operations.
*
* This class wraps a standard [PromptRunnerOperations] to provide thinking block
* extraction capabilities. It delegates to the underlying runner and provides
* graceful degradation with empty thinking blocks when thinking extraction
* is not available.
*
* Used only for non-OperationContextPromptRunner implementations.
*
* @param base The underlying prompt runner to enhance with thinking capabilities
*/
internal class ThinkingPromptRunner(
private val base: PromptRunnerOperations,
) : ThinkingPromptRunnerOperations {

override fun <T> createObjectIfPossible(
messages: List<Message>,
outputClass: Class<T>,
): ChatResponseWithThinking<T?> {
val result = base.createObjectIfPossible(messages, outputClass)
return ChatResponseWithThinking(
result = result,
thinkingBlocks = emptyList()
)
}

override fun <T> createObject(
messages: List<Message>,
outputClass: Class<T>,
): ChatResponseWithThinking<T> {
val result = base.createObject(messages, outputClass)
return ChatResponseWithThinking(
result = result,
thinkingBlocks = emptyList()
)
}

override fun respond(
messages: List<Message>,
): ChatResponseWithThinking<AssistantMessage> {
val result = base.respond(messages)
return ChatResponseWithThinking(
result = result,
thinkingBlocks = emptyList()
)
}

override fun evaluateCondition(
condition: String,
context: String,
confidenceThreshold: ZeroToOne,
): ChatResponseWithThinking<Boolean> {
val result = base.evaluateCondition(condition, context, confidenceThreshold)
return ChatResponseWithThinking(
result = result,
thinkingBlocks = emptyList()
)
}
}
Loading
Loading